Skip to content

Track 1 Agents, Track 2 AI clone#9287

Open
UtkarshTewari24 wants to merge 42 commits into
BasedHardware:mainfrom
UtkarshTewari24:ai-clone-imessage-contacts
Open

Track 1 Agents, Track 2 AI clone#9287
UtkarshTewari24 wants to merge 42 commits into
BasedHardware:mainfrom
UtkarshTewari24:ai-clone-imessage-contacts

Conversation

@UtkarshTewari24

@UtkarshTewari24 UtkarshTewari24 commented Jul 8, 2026

Copy link
Copy Markdown

What this is

Two things, built end-to-end : AI Clone (Track 2) and Multi-Agent Routing (Track 1).


Track 1 — Multi-Agent Routing

Omi can route a task to Codex, Hermes, OpenClaw, or Claude Code — by explicit request or automatic selection — with real fallback and real install-help.

What's real:

  • Say "use Codex to..." / "use Hermes to..." / "use OpenClaw to..." / "use Claude Code to..." via push-to-talk or the floating bar, and it actually routes there and executes (verified with real proof-file evidence per agent, not just "no error shown")
  • No agent named → an LLM classifies the task and picks the best connected agent (code/terminal-shaped → Codex; browser-dependent → OpenClaw, since it's the only one of the four with real browser control; otherwise whichever general adapter is connected) — with a single automatic retry on a different adapter if the first one fails
  • If a mentioned agent isn't installed: real install-help, not a dead end — shows the exact install command, a confirm step before running anything, and re-checks/auto-retries your original task afterward
  • Hermes specifically: a real device-code OAuth flow (no manual terminal steps), auto-provisioned onto a free model so it works without paid API credits
  • OpenClaw specifically: a fresh install fully self-completes (binary + non-interactive onboarding + Gateway daemon) — no manual openclaw onboard step left for the user
  • A real, previously-undiscovered bug fixed along the way: Claude Code's own MCP tool schemas were being rejected by the Anthropic API (invalid anyOf/allOf combinators) — every explicit "use Claude Code" task was silently failing before this fix

Known limitations:

  • OpenClaw's auto-onboarding currently authenticates via the user's existing Claude Code sign-in — if someone has never signed into Claude Code at all, OpenClaw's onboarding would need a different auth path (not yet built)
  • Hermes runs on a free-tier model by default (no paid Nous credits on the test account) — swap-able once credits are added

Track 2 — AI Clone

Omi learns your real texting voice from real message history (iMessage, Telegram, WhatsApp) and can respond on your behalf — safely, with a hard pause switch on by default.

What's real:

  • Import from iMessage (local, read-only, handles both plain-text and legacy attributedBody/typedstream-encoded messages), Telegram (official JSON export), and WhatsApp (official chat export)
  • Per-contact persona generation using verbatim slang/tone/burst-pattern examples pulled from real history — not a generic description of "casual texting style"
  • A real eval loop: held-out real (them → you) message pairs, an LLM judge scoring voice-plausibility (not just topic similarity keeps running nonstop until 80% accuracy 3 times in a row (extremely rigorous testing, maximum accuracy) — a topic change in your voice scores well, a generic reply in the right topic doesn't), iterative persona refinement against the worst-scoring pairs. Open Run Backtest on any trained contact to see live scores + per-pair judge reasoning.
  • Live send: iMessage (AppleScript) and Telegram (real TDLib/MTProto session, device-code login) both actually send messages, not just draft them
  • Safety-gated automation: per-contact Manual / Draft-Review / Autonomous modes, plus a global pause switch that defaults to ON — nothing sends autonomously unless both the contact-level mode and the global switch are explicitly set
  • Commitment tracking: scans real message history for things you said you'd do ("I'll send you those notes") and surfaces them as real Tasks, so nothing you promised gets silently forgotten

Known limitations (stated plainly, not hidden):

  • WhatsApp is import/train/backtest only — no live send yet (would need an unofficial Baileys-based sidecar; scoped but not built)
  • No Track 3 submission from this branch — time went into making 1 and 2 solid instead of spreading thin

Try it

  • Push-to-talk: "Use Codex to write a haiku to a file" → open the file, it's real
  • Push-to-talk, no agent named, something that needs a live web lookup → watch it correctly pick the one agent with real browser control
  • AI Clone tab → pick a trained contact → Preview Chat → Run Backtest to see real scores and reasoning

Build is clean (release build passes), full test suite green.

Review in cubic

UtkarshTewari24 and others added 30 commits July 1, 2026 03:09
Replace the hardcoded Mom/Alex/Jordan placeholder list on the AI Clone
page with the user's real top iMessage correspondents, read locally via
IMessageReaderService.topContacts(limit: 20).

- Rank contacts by message count, showing handle + count per row
- Add an "auto-select top N" stepper (default 5) that pre-selects the
  top N on load; per-row toggles override the selection independently
- Handle loading (spinner), Full Disk Access-denied (opens System
  Settings), empty, and error states
- Keep the per-contact Train button as a no-op stub (TODO: training
  pipeline)
- Match the dark OmiColors theme with white/neutral accents (no purple)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce shared, reader-neutral types so the persona/backtest pipeline is
not locked to iMessage-specific structs (prep for Telegram and other sources).

- New AICloneModels.swift: `ImportedMessage` (isFromMe/text/date) and
  `ImportedContact` (id/displayName/messageCount/platform), both Sendable.
- IMessageReaderService: add `asImportedContact()` / `asImportedMessage()`
  conversions (stamped platform: "imessage"); the iMessage reader types stay
  intact and keep their chat.db-specific logic.

Downstream services consume these generic shapes; concrete readers only need
to emit them.
Build the AI Clone modeling pipeline on top of the platform-agnostic types.

Persona generation (AIClonePersonaService):
- Generate a per-contact persona (voice, slang, emoji, burst style) from real
  history via the agent bridge; bakes 3-5 verbatim few-shot example exchanges
  (capped/deduped at 5) and hard "stay in character, never reveal you're an AI"
  rules into the system prompt.
- respond(as:to:context:) is context-aware: it takes the preceding few turns so
  the clone replies in the flow of the conversation, and emits multi-bubble
  bursts via a delimiter that is parsed back out (never leaks into text).

Backtest + training loop (AICloneBacktestService):
- runBacktest holds out real (their-message -> my-reply) pairs (excluding the
  training examples), predicts with the clone, and scores each with an LLM judge.
- Judge rubric rates VOICE plausibility, not topic match: an in-voice topic jump
  scores high, a correct-topic-but-off-voice reply scores low; each verdict is
  tagged [topic match]/[topic CHANGED] with one-sentence reasoning.
- trainToTarget iterates generate -> backtest -> refine, feeding the worst pairs
  (with the judge's structural complaint spelled out) back into refinement, and
  keeps the best-scoring persona across iterations. Target calibrated to 0.80.

UI:
- AIClonePage: per-contact Train, Preview Chat sheet, and Run Backtest with live
  iteration/score progress and an expandable held-out-pair inspector (their
  message / clone predicted / actually said / score / judge reasoning).
- Dashboard: add an "AI Clone" card (trained-persona count) as the real entry
  point in the new home design, and keep index 11 out of the tier-gated redirect.

All services consume ImportedContact/[ImportedMessage]; call sites convert
iMessage results at the boundary, so the whole pipeline is source-agnostic.
…hment sanitization

- ai_clone_contacts / ai_clone_run / ai_clone_respond automation actions run the
  full train->backtest pipeline without the UI and dump JSON reports
- non-prod builds honor aiCloneChatDbPathOverride so FDA-less named test bundles
  can read a chat.db snapshot
- attachment-only messages decode to an explicit [attachment] placeholder instead
  of leaking object-replacement glyphs into training text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…miter bug fix

Replaces the single-shot persona reply path:
- measured style card computed algorithmically from real messages (lengths,
  bursts, casing, punctuation, emoji, vocabulary) instead of LLM impressions
- per-message dynamic few-shot retrieval over embedded (them->me) history pairs
  (Gemini embeddings, lexical fallback), leak-safe against backtest holdouts
- respond() generates 3 diverse candidates as structured JSON bubbles, scores
  them deterministically against measured style, and a critic pass picks/pins
  the most authentic one
- removes the fragile '---' bubble delimiter protocol entirely (root cause of
  the 'two answers' leak); bursts are explicit bubble arrays end to end and
  render as separate bubbles in Preview Chat
- backtests: session-gap-aware pair extraction, seeded/pinnable eval sets,
  training iterations exclude eval pairs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The persona synthesis/refine LLM picks verbatim example exchanges from the
transcript; without exclusion it can bake a held-out eval pair's real answer
into the system prompt, inflating measured scores (observed once in 12 pairs).
Harness/backtest now pass eval pair-keys through generate/refine so measured
runs stay leak-free (production behavior unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add session-scoped importers for Telegram Desktop JSON exports and
WhatsApp "Export Chat" .txt files, mirroring IMessageReaderService's
actor + ImportedContact/ImportedMessage shape so AIClonePage treats
all platforms uniformly. Each importer aggregates contacts, resolves
the user's own sender identity (auto-detected from Telegram's
personal_information when present, otherwise via a "which one is you"
picker), and converts history into training messages.

Includes 7 review fixes, E2E-verified via real-format exports through
the import -> self-identity picker -> messages() -> persona training path
(0 isFromMe attribution mismatches on both platforms):

- Add hasSelfIdentity() to both importers.
- Gate train()/runBacktest() on a resolved self-identity: show the
  picker and back out (clearing the row's training/backtest state)
  instead of attributing every message to the wrong side.
- Fix Telegram attribution: require senderID != nil before comparing
  to selfID, killing the nil == nil "everything is mine" bug.
- Guard senderID != nil in currentSenders() so an unmatchable sender
  can never be picked as self.
- Throw WhatsAppImportError.unrecognizedFormat when no selected file
  yields messages, with guidance on how to export correctly.
- Refresh WhatsApp contacts on both success and mid-batch failure so
  partial imports still appear.
- Re-fetch imported contacts immediately before finishLoad() so an
  import completed during the initial load spinner isn't clobbered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the previously-standalone Telegram + new iMessage send services into the
AI Clone UI behind a single per-contact send-mode coordinator.

- IMessageSendService: AppleScript send + chat.db poll listener, mirroring
  TelegramSendService's start/stopListening + send surface.
- AICloneSendModeService: per-contact SendMode (manual/draftReview/autonomous,
  persisted), a global autonomous kill switch (isPaused, default TRUE), a
  persisted sent-message log with a read method, and a pending-draft queue.
  Autonomous auto-send is hard-gated on !isPaused and degrades to a draft when
  paused; Telegram listener only starts when a ready session exists (no spurious
  failure banners).
- AIClonePage: always-visible warning-colored Autonomous PAUSED/ACTIVE toggle,
  per-contact mode picker, Draft-Review approval queue (Approve/Edit/Reject),
  Recent Sent Messages sheet, and real manual send from Preview Chat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract the incoming-action decision into a pure nonisolated
  action(for:isPaused:) so the safety rule (Autonomous never sends while
  paused) is unit-testable; 10 tests cover routing, the pause gate, and
  Codable round-trips.
- AICloneSendModeHarness: non-prod bridge actions (status, set_paused,
  set_mode, simulate_incoming, pending, sent, send_routed) to drive and
  inspect the pipeline headlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SwiftUI Menu applies the system accent color to its label, which rendered the
per-contact mode picker in purple. Pin an explicit foregroundStyle + tint
(neutral for manual/draft, warning-amber for autonomous) per AGENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ents)

Local-only Node process wrapping Baileys (WhatsApp Linked Devices protocol),
spawned/owned by the desktop app. Endpoints: link/status + link/start (QR as
PNG data URL), send, events polling, name->JID resolve, logout. Session
persists via useMultiFileAuthState; exits when the parent app dies (stdin
tether + parent-PID watchdog). Verified standalone: real QR generated, all
endpoints exercised, no orphaned processes after parent SIGKILL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…idecar

WhatsAppSendService mirrors IMessage/Telegram send services (send, listen,
stop) over the local sidecar HTTP API, owning the node process lifecycle
(spawn, ready handshake, terminate-on-quit; sidecar self-tethers too).
Send-mode coordinator now routes whatsapp: contacts for real: direct
phone/JID ids send as-is, imported-export ids resolve via a learned
phone mapping or the linked account's contact list. Incoming live messages
match contacts by id, learned phone, or unique push-name. Autonomous mode
for WhatsApp contacts is additionally gated behind a one-time explicit
unofficial-connection risk acknowledgment (setMode returns false until
acknowledged). Global autonomous kill switch untouched (defaults paused).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Link WhatsApp button opens a sheet that starts the sidecar, renders the QR
(scan from WhatsApp > Linked Devices), polls to the linked state, and offers
Unlink. Selecting Autonomous for a WhatsApp contact now requires a one-time
explicit confirmation spelling out the unofficial-connection/account-flagging
risk; Manual and Draft-Review are unaffected. Mode picker no longer shows
'import only' for WhatsApp. Tests: gate matrix (mode x platform x ack),
send-target resolution, incoming-contact resolution (direct/learned/name,
ambiguity refused), sidecar status JSON mapping — 31 passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New AICloneContextService: cached, best-effort fetch of the user's saved
  Memories (APIClient.getMemories) and upcoming calendar (CalendarReaderService,
  read-only) rendered as bounded prompt blocks with hard grounding rules
  (never volunteer facts; never claim anything was booked/confirmed).
- respond() gains includeLiveContext (default on): memory facts always,
  calendar only when the message looks scheduling-related (cheap heuristic).
  Failures degrade to the exact pre-existing behavior.
- Backtest passes includeLiveContext:false so evals against historical
  replies aren't polluted by anachronistic live data.
- ai_clone_respond harness action gains live_context param for before/after.
- No changes to send-mode routing, pause switch, or kill-switch guarantees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live testing surfaced a hang: CalendarReaderService's cookie-decryption path
runs 'security find-generic-password' with no timeout, so an unanswered
keychain prompt (locked screen, background fetch) blocked respond() forever.

- Terminate the security subprocess after 15s (same pattern as the existing
  Python fetch timeout).
- Cap the whole calendar fetch in AICloneContextService at 45s via a task
  race; timeout/failure degrades to replying without calendar context.
- Changelog fragment for the Memories+Calendar context feature.

Verified live in the omi-clone-lab bundle: scheduling reply grounded in
calendar data; unanswerable keychain prompt now degrades in ~31s instead of
hanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Chat button now opens a two-tab sheet: Live shows the actual
conversation with the contact (recent history, 3s-poll updates, clone-
suggested replies you can edit/redo/send, and a composer for your own
messages), while Practice keeps the original type-as-the-contact
simulator. New AICloneChatHarness bridge actions (ai_clone_open_chat /
chat_state / chat_suggest / chat_close) drive and verify the sheet
headlessly; presentation state is owned by the page since macOS caches
dismissed sheet hosts, so the poll loop and triggers gate on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sendBubbles() splits a newline-joined clone reply into its bubbles and
dispatches them one at a time with a short human-ish pause, matching how
the reply renders in the UI and how real bursts look to the recipient.
Wired into autonomous sends, approved drafts, the live chat's suggestion
send, and the ai_clone_send_routed harness action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ous replies

- Draft-Review/Autonomous now work from app launch: send-mode routing is
  rebuilt from persisted personas in applicationDidFinishLaunching and the
  page no longer stops listeners on disappear (the root cause of autonomous
  "not working" — it only ran while the AI Clone page stayed open).
- Replies are faster: one persistent AgentBridge client reused across all
  clone LLM calls instead of per-call registration + forced token refresh;
  cached-quota failures retry instantly on a fresh client.
- Live replies get rolling conversation context and a rebuilt retrieval
  index after cold launch (previously they replied to the single incoming
  line with no few-shot retrieval post-restart).
- Autonomous replies are human-paced: reading delay scaled to the incoming
  message, WhatsApp read receipts + composing presence (new sidecar /read
  and /presence endpoints), Telegram typing chat action, and per-bubble
  typing delays scaled to reply length (AICloneHumanizer, unit-tested).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…asks

Adds a "Scan for Commitments" action per contact on the AI Clone page that
reads a bounded recent window of imported iMessage/Telegram/WhatsApp history
and asks the model to surface explicit, still-open promises the user made
(e.g. "I'll send you the deck", "let me get back to you") that don't appear
fulfilled later in the visible thread.

Each finding above 0.6 confidence becomes a real Task through the SAME staged
pipeline the screen extractor uses — StagedTaskStorage + APIClient.createStagedTask
+ relevance scoring + TaskPromotionService — so commitments promote to
action_items and Omi surfaces/nags them like any other detected task. Tagged
source="commitment" / category="commitment_tracking" to stay distinguishable.
Normalized-title dedup against existing staged + action items means re-scanning
the same contact never re-creates a task for the same commitment.

Read-only analysis: never sends messages, fully separate from AI-Clone send mode.
Adds an ai_clone_commitments headless bridge action for verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scanner only extracted the user's own outgoing promise sentences ("I'll
send you X"), so the most common real trigger — a friend chasing something the
user owes ("yo can u get me the notes u promised", "did you send it?", "can you
send me the order script") — produced no task when the user's original promise
wasn't in the visible window. Broadened the extractor to treat an unfulfilled
request/reminder from the other person for a concrete deliverable the user owes
as an open obligation too, anchored to that verbatim line. Stays conservative
(named deliverable + not already delivered) and keeps the 0.6 confidence floor.

Verified on real history: "Yo can u get me the notes u promised" now yields the
task "Send <contact> the notes you promised" at 0.95 and promotes to the Tasks
page; before this it returned nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
iMessage contacts showed raw phone numbers / emails because chat.db stores only
handles — names live in the Contacts database. Added ContactNameResolver, which
reads the local AddressBook stores (using the Full Disk Access this feature
already requires — no new permission, and no Contacts entitlement the sandbox
would otherwise need) and maps handles to names with country-code-tolerant
phone matching. IMessageReaderService.topContacts now resolves each handle, so
names flow through the whole AI Clone surface: the contact list, chat sheets,
personas, and commitment task titles ("Get Alex the notes you promised" instead
of "Send +1425… the notes"). Unmatched handles fall back to the raw handle.

Adds ai_clone_commitments_reset harness action for cleaning up test tasks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the manual scan button and its inline state/handlers from the contact
row. The commitment extraction service stays (still reachable via the dev
harness), so the feature can be re-surfaced or wired to run automatically later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…builds

Local dev/test builds (Omi Dev / omi-* bundles) no longer enforce the optimistic
client-side chat-usage limit, so development on a dev machine isn't interrupted by
the free-tier quota — this applies to every piMono LLM surface (Ask Omi chat
included), not just the AI Clone paths that already sidestepped it by rebuilding
the bridge on quota errors. The shipped production bundle (com.omi.computer-macos,
which is also Omi Beta) still enforces the limit, so billing behavior for real
users is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UtkarshTewari24 and others added 12 commits July 5, 2026 23:20
The bridge inherited OMI_*_ADAPTER_COMMAND from Swift at process start, so
an agent installed mid-session (e.g. through the install-help flow) stayed
"not available" until Omi restarted. Re-scan the same search directories
Swift uses when an external adapter is requested and seed the env command
on the fly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Claude Code SDK forwards omi-tools-stdio schemas verbatim to the
Anthropic API, which rejects top-level oneOf/allOf/anyOf — every explicit
Claude Code task died with a 400 (tools.N input_schema). Drop the
combinators at the stdio projection boundary; the preconditions remain in
prompt guidelines and are enforced by the zod schemas at call time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hermes has no API-key auth for Nous (device-code OAuth only), so wrap
the CLI's own flow: run `hermes auth add nous --type oauth --no-browser`,
parse the verification URL and user code from stdout, open the browser,
and wait for the CLI to confirm approval.

- HermesConnectService drives the flow and exposes phases
  (starting/waitingForApproval/connected/failed) to both surfaces
- Settings > Advanced > AI Setup gains a Hermes card with live status,
  Connect/Cancel/Retry, and the user code while waiting
- Installed-but-signed-out Hermes now routes to this sign-in prompt
  (LocalAgentProviderDetector.needsAuthentication via HermesAuthProbe)
  instead of dead-ending in an opaque runtime error; on success the
  original request retries automatically
- Automation bridge actions for headless verification
  (hermes_connect_state/start/cancel, agent_install_prompt_trigger)

Verified live against Omi Dev: full OAuth approval in Safari, tokens in
~/.hermes/auth.json, and a real task completed through hermes acp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two independent blockers kept the Hermes connected agent from completing
any task; both are fixed here so it works in a live demo on the free tier.

1. Free model (app-provisioned). Hermes' CLI writes model.default to a paid
   Nous model (qwen/qwen3-235b-a22b-2507) that 404s on a zero-credit account
   ("requires available credits"). Add HermesModelProvisioner, which pins
   model.default to a free model (stepfun/step-3.7-flash:free) in
   ~/.hermes/config.yaml. HermesConnectService now calls it on connect
   success and on connection refresh, so both fresh sign-ins and already-
   connected installs land on the free model. The rewrite is line-based,
   idempotent, and only touches model.default.

2. Tool-call permission parity with OpenClaw. Both Hermes and OpenClaw are
   external ACP adapters, but OpenClaw self-authorizes its tools and never
   sends session/request_permission, so its tools always run. Hermes follows
   the ACP handshake and asks before every tool call — and external_constrained
   auto-picked the deny/reject option, so its terminal/write_file calls were
   blocked and it could only reply that it was blocked. Give Hermes the same
   effective autonomy: AUTONOMOUS_EXTERNAL_ADAPTERS (hermes, openclaw) now
   resolve to the allow option under a new external_autonomous policy. Unknown
   external adapters keep the conservative external_constrained deny default.

Verified live (Omi Dev, prod backend): Hermes now returns real model output
(no 404) and its terminal tool executes, writing a proof file end-to-end;
Codex, OpenClaw, and Claude Code still pass the same proof-file test with no
regression. Agent suite 299/299, new provisioner tests 7/7, Hermes connect
19/19, agent-logic harness green, clean release build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app installed the OpenClaw binary with `--no-onboard` and stopped there,
so a genuinely fresh user was left with an installed-but-non-functional
OpenClaw: no Gateway daemon, no model auth. "use OpenClaw" then failed
opaquely at agent-run time until the user discovered `openclaw onboard` and
ran it by hand in a terminal.

Mirror the Hermes pattern (detect a needs-setup state, then run setup for the
user) — except OpenClaw's onboarding is fully non-interactive, so the app can
complete it end-to-end with no browser/prompt:

- OpenClawOnboardProbe: file-based check for an onboarded config (Gateway port
  + default model), distinct from "binary present".
- LocalAgentProviderDetector: a present-but-not-onboarded OpenClaw now reports
  `.needsAuthentication` instead of a silently-broken `.available`.
- OpenClawConnectService: runs `openclaw onboard --non-interactive
  --accept-risk --auth-choice anthropic-cli --install-daemon --flow quickstart
  --skip-*`, which installs/starts the Gateway daemon and wires the default
  model to the user's local Claude sign-in (the app's native default agent).
- Install-help flow: a not-onboarded OpenClaw gets an authenticate-kind connect
  prompt ("Connect OpenClaw", non-browser copy), and a fresh binary install
  auto-continues into onboarding; on success the original request is retried.
- openclaw_connect_state / openclaw_connect_start bridge actions for testing.

Verified live (Omi Dev): tore OpenClaw down to a genuinely fresh state
(removed config, auth, Gateway LaunchAgent; hid the binary), then the app's own
flow detected install → onboarding, ran onboarding, brought up the Gateway on
:18789, and completed a real task (proof file written). Restored the original
state (same Gateway token) and confirmed OpenClaw still works. Swift tests
34/34, agent suite 299/299, harness green, clean release build passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AI Clone tile sat alone in a third row next to an invisible spacer.
The five tiles now form a 3 + 2 grid with the bottom pair centered at the
same tile width, so nothing is orphaned or stretched. Also removed ~28
unused private views left behind by earlier dashboard redesign iterations
(1,600 lines of dead code — no referenced view changed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Title Case to match the page's button family (Suggest Reply, Send for
Real), proper pluralization in the backtest tooltip, and macOS 'click'
instead of iOS 'tap'. Display-only — no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssing

Auto-onboarding used --auth-choice anthropic-cli, which needs an existing
Claude Code sign-in — a user without one hit a dead end. Now, when the
Claude Code probe (config token, keychain credential, binary) comes up
empty at connect time, the install prompt switches to a manual-key path:
it explains OpenClaw needs its own model key, shows the real command
(--auth-choice openrouter-api-key, confirmed against openclaw onboard
--help; OpenRouter is the broadest single-key option, mirroring the
Hermes BYOK fallback), and offers an Open Terminal button that pre-types
the command via zsh print -z — the user pastes their own key and presses
return, so the key never passes through the app. A bounded watch polls
the onboarded config and resumes the original request automatically once
setup lands (verified live: config appearing flipped the prompt to
connected and retried the request).

Also: openclaw_connect_cancel / openclaw_set_claude_probe bridge actions
for testing, a precise trigger label in the automation reply, and repairs
to two detector tests left stale by the auto-onboard change (a bare
openclaw binary now reports needsAuthentication, so the fixtures write an
onboarded config).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, per-person blocklist

Batch of send-mode/persona work from this week's sessions: the clone keeps
conversations going and stays on topic, messages can become tasks, and a
Settings blocklist stops specific people's texts from ever turning into
tasks (Settings → Advanced → Tasks → Blocked People, backed by
TaskAssistantSettings.blockedContactIds with contacts sourced from the
clone's trained list).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drops the simulated texting pace: no more reading delay before
generation, no per-bubble typing delay or typing-indicator hold, and no
random pause between bubbles — bubbles dispatch sequentially (order
preserved) as soon as the reply exists. AICloneHumanizer and its tests
are gone with it. WhatsApp read receipts stay (instant, not pacing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 issues found across 117 files

Confidence score: 2/5

  • desktop/macos/agent/src/runtime/kernel.ts fallback handling can resurrect runs already in "cancelling" after a terminal adapter failure, which risks executing work the user explicitly stopped and breaking run-state guarantees—add a cancellation-state check before fallback continuation and block resume when cancelling.
  • desktop/macos/Desktop/Sources/AICloneHarness.swift uses contacts[rank - 1] without enforcing rank >= 1, so rank 0/negative can bypass the count guard and crash on out-of-bounds access—validate rank lower bounds before subscripting.
  • Import identity/state handling in desktop/macos/Desktop/Sources/WhatsAppImportService.swift, TelegramImportService.swift, and AICloneRetrievalService.swift can reuse or collide IDs (filename-only chat IDs, stale global selfID, stale retrieval index), causing silent overwrite or wrong-association of conversations—scope IDs to full export identity, re-derive/validate per import, and clear stale indexes when no valid pairs exist.
  • desktop/macos/Desktop/Sources/IMessageSendService.swift, AICloneSendModeService.swift, and desktop/macos/whatsapp-sidecar/src/index.js have liveness/resource risks (waitUntilExit() actor blocking, listener stuck after a not-ready check, body-limit reject without stopping stream) that can freeze messaging paths or waste memory under load—switch to non-blocking process handling, allow readiness retries, and terminate/destroy oversized request streams.
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/macos/agent/src/runtime/kernel.ts">

<violation number="1" location="desktop/macos/agent/src/runtime/kernel.ts:709">
P1: A cancelled run can be silently resurrected on a fallback adapter. When the adapter returns a failed terminal result, the fallback path does not check whether the run is already in `"cancelling"` state. The subsequent `continue` calls `createAttempt()`, which resets the run status to `"starting"`, overriding the user's cancellation request. The catch block below already guards fallback with `wasCancelling`; the success path should do the same.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneHarness.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneHarness.swift:40">
P1: User-supplied `rank` is parsed from automation params, but there's no `rank >= 1` guard before subscripting `contacts[rank - 1]`. When `rank` is `0` or negative, `contacts.count >= rank` is always true, so the guard is ineffective and the negative index crashes at runtime. The same unvalidated pattern is repeated across `ai_clone_respond`, `ai_clone_commitments`, and `executeRun`. Adding a single `guard rank >= 1, contacts.count >= rank` check at each call site would prevent this crash.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/WhatsAppImportService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/WhatsAppImportService.swift:248">
P1: Chat IDs are derived only from `url.lastPathComponent`, so importing two different exported chats that share a filename (e.g., `chat.txt` or `WhatsApp Chat with X.txt` from different directories) silently overwrites the earlier import because they map to the same `chatsByID` key. Consider including a path-based or hashed component in the ID to keep distinct files distinct.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift:87">
P1: The `envFileHasAPIKey` method parses variable names from a `.env` file without trimming whitespace from the key segment. When a line contains spaces around `=` (e.g., `OPENAI_API_KEY = mykey`), the extracted `name` retains a trailing space, so the `hasSuffix("_API_KEY")` and `== "HF_TOKEN"` checks silently fail. This produces a false negative and can incorrectly send users through the connect flow even when valid API keys are present.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift:252">
P2: The `terminationHandler` never flushes the remaining partial line in `lineBuffer`, so any trailing stdout content without a trailing `\n` is silently dropped. If the Hermes CLI emits the verification URL or a final error without a newline, the parser won't see it and the browser won't open. To fix, process the remaining `lineBuffer` through `handleOutput` inside the `@MainActor` block in the `terminationHandler` before calling `handleExit`.</violation>

<violation number="3" location="desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift:288">
P2: `refreshConnectionState()` is documented as re-checking auth on demand, but the `.failed` guard prevents it from actually verifying auth when a prior attempt failed. This leaves stale error UI if the user resolves the issue externally (e.g., installing Hermes or completing sign-in outside this flow). Since `.failed` is not `.isBusy`, the guard is unnecessary for concurrency safety — removing it lets the method correctly transition to `.connected` or `.idle` based on the real auth state, aligning with its documented purpose.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/IMessageSendService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/IMessageSendService.swift:90">
P1: Blocking `process.waitUntilExit()` inside this actor-isolated `async` method risks stalling the entire `IMessageSendService` actor. If `osascript` is slow (e.g., waiting for an Automation permission prompt or Messages.app to respond), the actor's serial executor thread is occupied and other actor-isolated work — including the 3-second `poll()` loop for live message detection and any concurrent sends — will queue up behind it.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/IMessageSendService.swift:151">
P2: When the `message` table is empty at startup, the baseline-cursor seeding forces `lastRowID = max(1, maxRow)`, which makes `lastRowID = 1` because `MAX(ROWID)` is NULL on an empty table. The next poll then queries `WHERE m.ROWID > 1`, but the first actual insert into an empty SQLite table receives `ROWID = 1`, so that very first message is permanently skipped. Removing the `max(1, ...)` guard lets the empty-table case simply re-seed on the next poll tick; once a row exists, `lastRowID` will correctly track the true max and subsequent messages won’t be lost.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/TelegramImportService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/TelegramImportService.swift:255">
P1: `selfID` is loaded from a global, persistent UserDefaults key and reused across imports without validating it against the newly loaded export. When a later import lacks `personal_information` (or belongs to a different account), the stale `selfID` suppresses the disambiguation prompt and causes `messages(for:)` to misclassify `isFromMe`.

Reset `selfID` at the start of each `importExport` call so identity is derived fresh from the current export rather than carried over from a prior one.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/TelegramImportService.swift:286">
P2: Passing a negative `limit` to Swift's `prefix` or `suffix` triggers a `_precondition` trap and crashes the app. The `topContacts(limit:)` and `messages(for:limit:)` methods accept an unguarded `Int` and pass it directly to `.prefix(limit)` and `.suffix(limit)`. Even if current callers pass positive values, this service API is brittle: any buggy state or future caller that produces a negative value will cause a runtime crash. Consider guarding `limit` to be non-negative (e.g., `max(0, limit)`) at the call sites.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneRetrievalService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneRetrievalService.swift:39">
P2: The `indices` dictionary in `AICloneRetrievalService` caches per-contact retrieval data but never evicts or removes entries. Over a long-running desktop session, memory usage can grow unbounded because each index stores token sets and optional embedding vectors with no cleanup path. Consider adding an explicit removal method (e.g., `removeIndex(for:)`), a global clear, or a capacity-bound eviction strategy so the cache doesn't accumulate indefinitely.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/AICloneRetrievalService.swift:58">
P1: When `buildPairs` yields no valid pairs (e.g., after a contact's history is cleared or changed), this early return leaves any previously-built index for `contactId` in place. Because the fingerprint already differs, the stale index is never refreshed, so `hasIndex(for:)` still returns true and `retrieve()` continues surfacing old replies that should no longer be used. Clear `indices[contactId] = nil` before returning when pairs are empty so the service correctly reports that no usable index exists.</violation>
</file>

<file name="desktop/macos/whatsapp-sidecar/src/index.js">

<violation number="1" location="desktop/macos/whatsapp-sidecar/src/index.js:176">
P2: The reconnect policy uses `hasSavedSession()` to decide whether a closed connection is resumable, but `hasSavedSession()` only checks for `creds.json` existence. Baileys's `useMultiFileAuthState` eagerly creates `creds.json` (via `initAuthCreds()`) before a successful link, so a QR-pending or never-linked session appears resumable. When the connection drops in that state, the code retries indefinitely instead of the intended "expire after 3 attempts" path, leaving the sidecar stuck in a reconnect loop and preventing a clean return to the `unlinked` UX state.

Consider adding a persistent marker (e.g., a `.ever-linked` flag file written when `connection === 'open'`) and checking it alongside `creds.json` to distinguish sessions that were successfully linked from those that only have initialized credentials.</violation>

<violation number="2" location="desktop/macos/whatsapp-sidecar/src/index.js:292">
P1: The body-size guard in `readBody` calls `reject(new Error('body too large'))` when `raw.length` exceeds 1 MB, but it never stops the request stream from continuing to buffer data. In Node.js, rejecting the promise does not prevent subsequent `data` events from appending more chunks to `raw`, so a large or streaming request can still cause unbounded memory growth and destabilize the sidecar process. Consider pausing or destroying the request (e.g. `req.pause()`, `req.destroy()`, or removing the listener) immediately after rejecting so the buffer stops growing.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneModels.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneModels.swift:20">
P1: The `newest-first` ordering contract documented in `AICloneMessageLoader.loadMessages` is not actually enforced by the loader. `IMessageReaderService` returns newest-first via `ORDER BY m.date DESC`, but both `TelegramImportService` and `WhatsAppImportService` use `.suffix(limit)` on chronologically ordered parsed data, which returns oldest-first within the newest subset. Downstream consumers (persona service, send mode, commitment extraction) rely on newest-first semantics for windowing and reversal, so inconsistent ordering can send stale context into backtests and live replies. Either sort/normalize in the loader or sort descending in each cross-platform reader before returning.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneSendModeService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneSendModeService.swift:381">
P1: `startListening()` sets `isListening = true` before the async Telegram readiness check, so a single not-ready state permanently blocks all future retries. Telegram incoming handling silently breaks after a late login because no code path resets `isListening` when auth becomes ready.

A practical fix is to add a `startTelegramListenerIfReady()` helper (mirroring the WhatsApp `startWhatsAppListenerIfLinked()` pattern) that can be called independently after login succeeds, or to track per-platform listener state so one platform's failure doesn't block retries.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/AICloneSendModeService.swift:542">
P2: The `lastCommitmentBackstop` cooldown timestamp is persisted before any backstop scanning work actually runs. If the app crashes, is backgrounded, or encounters errors during the scan loop, the 6-hour cooldown is already locked in, suppressing retries and allowing obligations from messages received while closed to go missed. Move the checkpoint inside the `Task` block and only update it after all contacts have been processed, or track per-contact success so temporary failures don't block the entire backstop.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneSendModeHarness.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneSendModeHarness.swift:40">
P1: The `ai_clone_set_paused` action parses the `paused` parameter with `NSString.boolValue`, which silently coerces invalid strings like `"flase"` or `"garbage"` into booleans instead of rejecting them. Because this controls the global autonomous kill switch, a malformed request can silently change safety posture instead of surfacing an error. Consider validating the input against exact allowed literals (e.g., `"true"` / `"false"`) and returning an error for anything else.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/AICloneSendModeHarness.swift:87">
P2: The `ai_clone_simulate_incoming` harness action calls `updateActiveContacts` with a single-element array. `updateActiveContacts` is a replace-all setter that overwrites both the in-memory `activeContacts` dictionary and the persisted known-contacts store, so every simulated message drops all previously registered contacts from the coordinator's routing state. To keep other contacts registered, build the full set from all trained personas first, then call `updateActiveContacts` with the complete list.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneStyleAnalyzer.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneStyleAnalyzer.swift:68">
P2: The `emojiPattern` regex matches individual Unicode scalars rather than full grapheme clusters, so multi-scalar emoji (ZWJ sequences, flags, skin-tone modifiers) are split into fragments. Those fragments are counted in `emojiCounts`, displayed in `topEmoji`, and compared in `styleScore`, producing incorrect style cards and inaccurate scoring.

Use Swift grapheme-cluster enumeration (e.g., iterating `for character in text`, testing each cluster’s scalars) so that a flag or family group is treated as a single emoji throughout the pipeline.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/AICloneStyleAnalyzer.swift:171">
P1: `renderStyleCard` performs unguarded hard subscript access on `f.burstShare[1]` and `f.burstShare[2]`, but `StyleFeatures` is `Codable` and is decoded from persisted `UserDefaults` data in `AIClonePersonaService`. A malformed, truncated, or legacy stored value with fewer than 3 entries in `burstShare` will trap at runtime. Add a count guard or normalize `burstShare` to 7 elements during decoding — note that the very next line (`multiShare`) already has such a guard, which shows this was simply missed.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/Providers/HermesModelProvisioner.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/Providers/HermesModelProvisioner.swift:88">
P1: `rewrite` searches for the first indented `default:` key inside the `model:` block without tracking indentation depth, so a nested `default:` (e.g. under `openrouter:`) can be matched instead of `model.default`. This silently leaves the real `model.default` unchanged, which can still point to a paid model and cause free-tier failures. Consider tracking the exact indent level of the `model:` block and only matching `default:` at that same indent depth.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/Providers/HermesModelProvisioner.swift:114">
P2: The `topLevelKeyIndex` method uses exact-string matching (`== "\(key):"`) for top-level YAML keys. This fails to match lines such as `model: # comment` or `model: someValue`, causing `rewrite(_:)` to treat the file as missing a `model:` block and prepend a duplicate one. This breaks the stated invariant that only `model.default` is modified. Consider using a prefix check (e.g., `trimmed.hasPrefix("\(key):")`) so inline comments or values on the same line are recognized correctly.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/AICloneBacktestService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/AICloneBacktestService.swift:103">
P1: The pinned-pairs branch bypasses the training-key and exclude-key filters by searching `allPairs` instead of the already-filtered `pairs`. This can silently re-include training exchanges and explicitly excluded keys during evaluation, inflating backtest scores and weakening holdout integrity.

Switch the pinned-pair lookup to use `pairs` so the same exclusion rules apply regardless of whether pairs are sampled or pinned.</violation>
</file>

<file name="desktop/macos/agent/src/adapters/codex.ts">

<violation number="1" location="desktop/macos/agent/src/adapters/codex.ts:329">
P1: `resultFromJsonl` returns successfully when `messages.length > 0` even if `errors` were also collected from a `turn.failed` or `error` event. A failed Codex turn that produced partial output is therefore reported as succeeded, which breaks upstream retry/fallback logic that depends on accurate terminal status. Swap the precedence so errors are checked first.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift:54">
P2: `isClaudeCodeAvailableForOnboard` uses `LocalAgentProviderDetector.isAvailable(.claudeCode)`, which returns `true` if the `claude` binary exists in PATH even when the user has no OAuth token cache and no keychain credentials. A user with the binary installed but not signed in will incorrectly take the automated `openclaw onboard --auth-choice anthropic-cli` path, which will fail because there is no reusable credential, and the service will land in `.failed` instead of the intended `.needsManualModelSetup` fallback. The guard should verify that an actual credential exists before trusting the automated onboarding path.</violation>

<violation number="2" location="desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift:132">
P2: The manual OpenClaw fallback instructs the user to replace `YOUR_OPENROUTER_API_KEY` in a Terminal command and execute it. When they do, the real API key becomes part of the shell command line, which exposes it to process-argv visibility (readable via `ps` while the command runs) and persists it in shell history (e.g., `.zsh_history`). The comment frames this as safe because "the app never sees or stores it," but it doesn't acknowledge that the shell *does* see and store it. OpenClaw supports `--secret-input-mode ref`, which can store env-backed credential references and avoid inline key flags when the environment variable is present in the process environment. Consider either running the subprocess directly with the env var set (bypassing shell history), or updating the UI copy to warn users about shell-history exposure so they can take mitigating steps like prefixing the command with a space.</violation>

<violation number="3" location="desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift:157">
P2: Terminal-launch failures in `openTerminalPreloadedWithManualSetup()` are only logged via `NSLog` and never surfaced to the UI. Because the method returns `Void` and does not update the published `phase`, the caller (`FloatingControlBarWindow.swift`) has no way to detect or report a failure such as missing Automation permissions. The user can be left stuck in `.needsManualModelSetup` without actionable feedback. Consider propagating the error back to the main actor and updating `phase` to `.failed(message:)` so the UI can display the issue.</violation>

<violation number="4" location="desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift:159">
P2: `runOnboard` can deadlock when `openclaw onboard` produces more output than the OS pipe buffer can hold. Both `standardOutput` and `standardError` are wired to the same `Pipe`, and `waitUntilExit()` is called before any data is read. If the child fills the pipe buffer, it blocks on write(), the parent blocks on waitUntilExit(), and onboarding hangs indefinitely.

The codebase already handles this exact scenario in `CalendarReaderService.swift` with `readabilityHandler` + `DispatchSemaphore` to drain the pipe asynchronously while the process runs, followed by a post-exit semaphore wait. Apply the same pattern here.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/TaskAgent/TaskChatState.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/ProactiveAssistants/Assistants/TaskAgent/TaskChatState.swift:288">
P1: When `allowAdapterAutoSelection: true` is passed to `bridge.query(...)` (line 288), the backend may choose an adapter different from the client's `currentHarness`. The legacy-resume persistence logic later falls back to `currentHarness` if `queryResult.adapterId` is nil (line 308-309): `let actualAdapterId = queryResult.adapterId ?? currentHarness.flatMap { AgentRuntimeProcess.adapterId(forHarnessMode: $0) }`. Because auto-selection means the requested harness can differ from the actual adapter, a missing `adapterId` would cause the code to misclassify the adapter and either pollute `legacyAcpSessionId` with a non-legacy session or discard a valid legacy one. Consider treating the fallback as invalid when auto-selection is enabled, or otherwise guarding so that a missing `adapterId` defaults to non-legacy persistence behavior instead of the potentially stale harness.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/IMessageReaderService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/IMessageReaderService.swift:129">
P2: Consider validating `limit` before it reaches the SQL `LIMIT` clause. SQLite treats a negative `LIMIT` as "no upper bound," so an unchecked caller could trigger an unbounded fetch and large memory spike (especially in the per-contact messages query). Adding a simple `max(limit, 1)` guard at the top of each public method would prevent that scenario without any change in the expected happy-path behavior.</violation>
</file>

<file name="desktop/macos/agent/src/runtime/adapter-selection.ts">

<violation number="1" location="desktop/macos/agent/src/runtime/adapter-selection.ts:23">
P2: The auto-classification logic in `selectBestAdapterForTask` lacks any browser/web-intent detection. Prompts like "search the web", "open this site", or "look up X online" do not match the code, terminal, or codebase-scope patterns, so they fall through to the `general` category where `GENERAL_TASK_ADAPTER_PRIORITY` prefers Hermes over OpenClaw. Because the PR description explicitly advertises OpenClaw as the browser-capable adapter, browser-dependent tasks will be systematically mis-routed to Hermes instead of being routed to OpenClaw first. Consider adding a browser/web-intent regex pattern and a corresponding priority list (or bumping OpenClaw for general tasks when browser keywords are detected) so browser tasks reach the browser-enabled adapter.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/SettingsContentView+Advanced.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/MainWindow/Pages/Settings/Sections/SettingsContentView+Advanced.swift:75">
P2: Accessibility issue: each contact row creates a `Toggle` with an empty label inside a `ForEach` loop. Because `.labelsHidden()` is applied and there is no `.accessibilityLabel`, VoiceOver will read the toggle as a generic unlabeled switch. SwiftUI does not automatically bind the adjacent `Text(contact.displayName)` to the toggle for assistive tech, so a screen-reader user cannot identify which person they are blocking or unblocking when multiple contacts are listed. Consider passing a descriptive label to the `Toggle` initializer and keeping `.labelsHidden()` for visual consistency, or explicitly adding `.accessibilityLabel("Block messages from \(contact.displayName)")`.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/FloatingControlBar/AgentPill.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/FloatingControlBar/AgentPill.swift:605">
P2: The fallback to `providerObjective(from: originalText)` in the classifier output parser is brittle for the multi-word provider names added in this PR. `providerObjective` uses `\S+` after action verbs, so for a query like "use Claude Code to fix this" the regex matches "Claude" as the provider and returns "Code to fix this" as the task — leaving the provider name partially embedded in the rewritten query. Since multi-word providers ("Claude Code", "Open Claw") are now supported, the fallback can pass polluted/recursive instructions to the child agent. Consider updating `providerObjective` to handle multi-word provider names, or using the same provider-detection approach already present in `literalProviderDirective`.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/TelegramLoginView.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/TelegramLoginView.swift:103">
P2: The `.onSubmit(action)` on the text/secure fields is not guarded by `model.isSubmitting`, so pressing Return repeatedly can enqueue multiple concurrent `Task`s that call `submitPhone`/`submitCode`/`submitPassword` before SwiftUI re-renders to disable the button. `TelegramSendService` has no internal reentrancy guard on those methods, so duplicate TDLib auth requests may trigger rate-limit or invalid-state errors.

Consider wrapping the `.onSubmit` handler with a guard so the field itself respects the in-flight submission state, e.g. `.onSubmit { if !model.isSubmitting { action() } }`. Both `TextField` and `SecureField` are affected.</violation>
</file>

<file name="desktop/macos/whatsapp-sidecar/package.json">

<violation number="1" location="desktop/macos/whatsapp-sidecar/package.json:2">
P2: The new `whatsapp-sidecar` package defines its own `package.json` with runtime dependencies, but it is not registered in the root workspace or any CI/build install flow. In non-workspace setups, nested packages are skipped during root `npm install`, so the sidecar's `node_modules` will be missing in fresh clones and CI. `WhatsAppSendService.swift` already has a manual fallback error telling users to `npm install` inside the directory, which is a strong signal the package is not yet wired into automated builds. Consider adding the sidecar to the root workspace configuration (e.g., `workspaces` in root `package.json`) or wiring it into packaging scripts so dependencies are installed reliably.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/TelegramSendService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/TelegramSendService.swift:156">
P2: In `submitPhone` and `submitCode`, the input trimming uses `.whitespaces`, which does **not** include newline characters. Users often paste phone numbers or device codes from messages, email, or the clipboard, where a trailing newline is extremely common. TDLib validates these inputs strictly, so a hidden trailing newline causes authentication to fail even though the UI text field looks correct.

The same file already uses `.whitespacesAndNewlines` for keychain-value trimming, so switching to the broader character set here keeps the behavior consistent across the file and eliminates a real paste-related login failure.

**Suggestion**: Change `.whitespaces` to `.whitespacesAndNewlines` in both `submitPhone` and `submitCode`.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift:2881">
P2: The `await` on `providerDirective` is now placed before `prepareVisibleQueryState`, so during the LLM/network call (up to a 3s timeout) the user sees no thinking spinner and any in-flight stream may complete before the delayed `provider.isSending` follow-up guard runs. Move the visible-query-state setup before the async call so inline feedback stays immediate and follow-up tracking remains race-free.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/Providers/ChatProvider.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/Providers/ChatProvider.swift:3553">
P2: When runtime adapter auto-selection is enabled, falling back to `activeBridgeHarness` if `queryResult.adapterId` is nil can silently mis-attribute usage to the wrong adapter. Because the bridge picks the adapter at runtime in that mode, the configured harness may not match the actual adapter. If the bridge ever omits `adapterId` (older version, partial rollout, or a bridge-side bug), downstream `recordLlmUsage` and local cumulative-cost tracking will attribute the request to the wrong account. Consider only using the `activeBridgeHarness` fallback when `allowAdapterAutoSelection` was not used for this query, or skip recording and log a diagnostic when the actual adapter is unknown.</violation>
</file>

<file name="desktop/macos/Desktop/Sources/WhatsAppSendService.swift">

<violation number="1" location="desktop/macos/Desktop/Sources/WhatsAppSendService.swift:161">
P2: When the WhatsApp sidecar exits unexpectedly, `handleSidecarExit()` does not cancel the polling listener task or clear the `onNewMessage` callback. The `listenerTask` continues waking every 2.5 s, calls `pollEvents()`, returns early because `process?.isRunning` is now false, and goes back to sleep — a perpetual no-op wake loop that also retains the message callback. The intentional `stopSidecar()` path correctly calls `stopListening()`; the unexpected-exit path should do the same.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

);
});
this.activeExecutions.delete(accepted.run.runId);
if (result.terminalStatus === "failed" && prepareFallback(attempt, result.failure?.code ?? "adapter_execution_failed", result.failure?.userMessage ?? result.text)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A cancelled run can be silently resurrected on a fallback adapter. When the adapter returns a failed terminal result, the fallback path does not check whether the run is already in "cancelling" state. The subsequent continue calls createAttempt(), which resets the run status to "starting", overriding the user's cancellation request. The catch block below already guards fallback with wasCancelling; the success path should do the same.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/agent/src/runtime/kernel.ts, line 709:

<comment>A cancelled run can be silently resurrected on a fallback adapter. When the adapter returns a failed terminal result, the fallback path does not check whether the run is already in `"cancelling"` state. The subsequent `continue` calls `createAttempt()`, which resets the run status to `"starting"`, overriding the user's cancellation request. The catch block below already guards fallback with `wasCancelling`; the success path should do the same.</comment>

<file context>
@@ -643,6 +706,17 @@ export class AgentRuntimeKernel {
           );
         });
         this.activeExecutions.delete(accepted.run.runId);
+        if (result.terminalStatus === "failed" && prepareFallback(attempt, result.failure?.code ?? "adapter_execution_failed", result.failure?.userMessage ?? result.text)) {
+          this.finishAttemptForRetry({
+            attempt,
</file context>
Suggested change
if (result.terminalStatus === "failed" && prepareFallback(attempt, result.failure?.code ?? "adapter_execution_failed", result.failure?.userMessage ?? result.text)) {
const wasCancelling = this.runStatus(accepted.run.runId) === "cancelling";
if (!wasCancelling && result.terminalStatus === "failed" && prepareFallback(attempt, result.failure?.code ?? "adapter_execution_failed", result.failure?.userMessage ?? result.text)) {

params: ["rank", "holdout", "seed", "iterations", "messages", "out", "reuse_persona", "eval_from"]
) { params in
guard !runInFlight else { return ["error": "a run is already in flight"] }
let rank = Int(params["rank"] ?? "") ?? 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: User-supplied rank is parsed from automation params, but there's no rank >= 1 guard before subscripting contacts[rank - 1]. When rank is 0 or negative, contacts.count >= rank is always true, so the guard is ineffective and the negative index crashes at runtime. The same unvalidated pattern is repeated across ai_clone_respond, ai_clone_commitments, and executeRun. Adding a single guard rank >= 1, contacts.count >= rank check at each call site would prevent this crash.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/AICloneHarness.swift, line 40:

<comment>User-supplied `rank` is parsed from automation params, but there's no `rank >= 1` guard before subscripting `contacts[rank - 1]`. When `rank` is `0` or negative, `contacts.count >= rank` is always true, so the guard is ineffective and the negative index crashes at runtime. The same unvalidated pattern is repeated across `ai_clone_respond`, `ai_clone_commitments`, and `executeRun`. Adding a single `guard rank >= 1, contacts.count >= rank` check at each call site would prevent this crash.</comment>

<file context>
@@ -0,0 +1,406 @@
+      params: ["rank", "holdout", "seed", "iterations", "messages", "out", "reuse_persona", "eval_from"]
+    ) { params in
+      guard !runInFlight else { return ["error": "a run is already in flight"] }
+      let rank = Int(params["rank"] ?? "") ?? 1
+      let holdout = Int(params["holdout"] ?? "") ?? 12
+      let seed = UInt64(params["seed"] ?? "") ?? 42
</file context>

let messages = parseWhatsAppExport(text: text)
guard !messages.isEmpty else { continue }
anyProduced = true
let id = "whatsapp:\(url.lastPathComponent)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Chat IDs are derived only from url.lastPathComponent, so importing two different exported chats that share a filename (e.g., chat.txt or WhatsApp Chat with X.txt from different directories) silently overwrites the earlier import because they map to the same chatsByID key. Consider including a path-based or hashed component in the ID to keep distinct files distinct.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/WhatsAppImportService.swift, line 248:

<comment>Chat IDs are derived only from `url.lastPathComponent`, so importing two different exported chats that share a filename (e.g., `chat.txt` or `WhatsApp Chat with X.txt` from different directories) silently overwrites the earlier import because they map to the same `chatsByID` key. Consider including a path-based or hashed component in the ID to keep distinct files distinct.</comment>

<file context>
@@ -0,0 +1,321 @@
+      let messages = parseWhatsAppExport(text: text)
+      guard !messages.isEmpty else { continue }
+      anyProduced = true
+      let id = "whatsapp:\(url.lastPathComponent)"
+      if chatsByID[id] == nil { chatOrder.append(id) }
+      chatsByID[id] = messages
</file context>

for line in content.split(whereSeparator: \.isNewline) {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard !trimmed.hasPrefix("#"), let equals = trimmed.firstIndex(of: "=") else { continue }
let name = String(trimmed[..<equals])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The envFileHasAPIKey method parses variable names from a .env file without trimming whitespace from the key segment. When a line contains spaces around = (e.g., OPENAI_API_KEY = mykey), the extracted name retains a trailing space, so the hasSuffix("_API_KEY") and == "HF_TOKEN" checks silently fail. This produces a false negative and can incorrectly send users through the connect flow even when valid API keys are present.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift, line 87:

<comment>The `envFileHasAPIKey` method parses variable names from a `.env` file without trimming whitespace from the key segment. When a line contains spaces around `=` (e.g., `OPENAI_API_KEY = mykey`), the extracted `name` retains a trailing space, so the `hasSuffix("_API_KEY")` and `== "HF_TOKEN"` checks silently fail. This produces a false negative and can incorrectly send users through the connect flow even when valid API keys are present.</comment>

<file context>
@@ -0,0 +1,355 @@
+        for line in content.split(whereSeparator: \.isNewline) {
+            let trimmed = line.trimmingCharacters(in: .whitespaces)
+            guard !trimmed.hasPrefix("#"), let equals = trimmed.firstIndex(of: "=") else { continue }
+            let name = String(trimmed[..<equals])
+            let value = String(trimmed[trimmed.index(after: equals)...]).trimmingCharacters(in: .whitespaces)
+            guard !value.isEmpty else { continue }
</file context>
Suggested change
let name = String(trimmed[..<equals])
let name = String(trimmed[..<equals]).trimmingCharacters(in: .whitespaces)

}
stdin.fileHandleForWriting.write(Data(script.utf8))
try? stdin.fileHandleForWriting.close()
process.waitUntilExit()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Blocking process.waitUntilExit() inside this actor-isolated async method risks stalling the entire IMessageSendService actor. If osascript is slow (e.g., waiting for an Automation permission prompt or Messages.app to respond), the actor's serial executor thread is occupied and other actor-isolated work — including the 3-second poll() loop for live message detection and any concurrent sends — will queue up behind it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/IMessageSendService.swift, line 90:

<comment>Blocking `process.waitUntilExit()` inside this actor-isolated `async` method risks stalling the entire `IMessageSendService` actor. If `osascript` is slow (e.g., waiting for an Automation permission prompt or Messages.app to respond), the actor's serial executor thread is occupied and other actor-isolated work — including the 3-second `poll()` loop for live message detection and any concurrent sends — will queue up behind it.</comment>

<file context>
@@ -0,0 +1,229 @@
+    }
+    stdin.fileHandleForWriting.write(Data(script.utf8))
+    try? stdin.fileHandleForWriting.close()
+    process.waitUntilExit()
+
+    guard process.terminationStatus == 0 else {
</file context>

Comment on lines +252 to +257
process.terminationHandler = { [weak self] finished in
stdout.fileHandleForReading.readabilityHandler = nil
let status = finished.terminationStatus
Task { @MainActor [weak self] in
self?.handleExit(status: status, transcript: transcript)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The terminationHandler never flushes the remaining partial line in lineBuffer, so any trailing stdout content without a trailing \n is silently dropped. If the Hermes CLI emits the verification URL or a final error without a newline, the parser won't see it and the browser won't open. To fix, process the remaining lineBuffer through handleOutput inside the @MainActor block in the terminationHandler before calling handleExit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/HermesConnectService.swift, line 252:

<comment>The `terminationHandler` never flushes the remaining partial line in `lineBuffer`, so any trailing stdout content without a trailing `\n` is silently dropped. If the Hermes CLI emits the verification URL or a final error without a newline, the parser won't see it and the browser won't open. To fix, process the remaining `lineBuffer` through `handleOutput` inside the `@MainActor` block in the `terminationHandler` before calling `handleExit`.</comment>

<file context>
@@ -0,0 +1,355 @@
+            }
+        }
+
+        process.terminationHandler = { [weak self] finished in
+            stdout.fileHandleForReading.readabilityHandler = nil
+            let status = finished.terminationStatus
</file context>
Suggested change
process.terminationHandler = { [weak self] finished in
stdout.fileHandleForReading.readabilityHandler = nil
let status = finished.terminationStatus
Task { @MainActor [weak self] in
self?.handleExit(status: status, transcript: transcript)
}
process.terminationHandler = { [weak self] finished in
stdout.fileHandleForReading.readabilityHandler = nil
let status = finished.terminationStatus
Task { @MainActor [weak self] in
if !lineBuffer.isEmpty {
self?.handleOutput(line: lineBuffer, parser: &parser)
lineBuffer = ""
}
self?.handleExit(status: status, transcript: transcript)
}
}

let effectiveHarness = activeBridgeHarness
let isPiMonoHarness = effectiveHarness == Self.harnessMode(for: .piMono)
let isUserClaudeHarness = effectiveHarness == Self.harnessMode(for: .userClaude)
let effectiveAdapterId =

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When runtime adapter auto-selection is enabled, falling back to activeBridgeHarness if queryResult.adapterId is nil can silently mis-attribute usage to the wrong adapter. Because the bridge picks the adapter at runtime in that mode, the configured harness may not match the actual adapter. If the bridge ever omits adapterId (older version, partial rollout, or a bridge-side bug), downstream recordLlmUsage and local cumulative-cost tracking will attribute the request to the wrong account. Consider only using the activeBridgeHarness fallback when allowAdapterAutoSelection was not used for this query, or skip recording and log a diagnostic when the actual adapter is unknown.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/ChatProvider.swift, line 3553:

<comment>When runtime adapter auto-selection is enabled, falling back to `activeBridgeHarness` if `queryResult.adapterId` is nil can silently mis-attribute usage to the wrong adapter. Because the bridge picks the adapter at runtime in that mode, the configured harness may not match the actual adapter. If the bridge ever omits `adapterId` (older version, partial rollout, or a bridge-side bug), downstream `recordLlmUsage` and local cumulative-cost tracking will attribute the request to the wrong account. Consider only using the `activeBridgeHarness` fallback when `allowAdapterAutoSelection` was not used for this query, or skip recording and log a diagnostic when the actual adapter is unknown.</comment>

<file context>
@@ -3543,9 +3550,11 @@ BROWSER TABS: when you use the browser (Playwright), on your FIRST browser actio
-            let effectiveHarness = activeBridgeHarness
-            let isPiMonoHarness = effectiveHarness == Self.harnessMode(for: .piMono)
-            let isUserClaudeHarness = effectiveHarness == Self.harnessMode(for: .userClaude)
+            let effectiveAdapterId =
+                queryResult.adapterId
+                ?? AgentRuntimeProcess.adapterId(forHarnessMode: activeBridgeHarness)
</file context>

/// the remaining flags mirror `onboardArguments`.
static let manualModelSetupCommand: String =
"openclaw onboard --non-interactive --accept-risk "
+ "--auth-choice openrouter-api-key --openrouter-api-key \(manualSetupKeyPlaceholder) "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The manual OpenClaw fallback instructs the user to replace YOUR_OPENROUTER_API_KEY in a Terminal command and execute it. When they do, the real API key becomes part of the shell command line, which exposes it to process-argv visibility (readable via ps while the command runs) and persists it in shell history (e.g., .zsh_history). The comment frames this as safe because "the app never sees or stores it," but it doesn't acknowledge that the shell does see and store it. OpenClaw supports --secret-input-mode ref, which can store env-backed credential references and avoid inline key flags when the environment variable is present in the process environment. Consider either running the subprocess directly with the env var set (bypassing shell history), or updating the UI copy to warn users about shell-history exposure so they can take mitigating steps like prefixing the command with a space.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/Providers/OpenClawConnectService.swift, line 132:

<comment>The manual OpenClaw fallback instructs the user to replace `YOUR_OPENROUTER_API_KEY` in a Terminal command and execute it. When they do, the real API key becomes part of the shell command line, which exposes it to process-argv visibility (readable via `ps` while the command runs) and persists it in shell history (e.g., `.zsh_history`). The comment frames this as safe because "the app never sees or stores it," but it doesn't acknowledge that the shell *does* see and store it. OpenClaw supports `--secret-input-mode ref`, which can store env-backed credential references and avoid inline key flags when the environment variable is present in the process environment. Consider either running the subprocess directly with the env var set (bypassing shell history), or updating the UI copy to warn users about shell-history exposure so they can take mitigating steps like prefixing the command with a space.</comment>

<file context>
@@ -0,0 +1,250 @@
+    /// the remaining flags mirror `onboardArguments`.
+    static let manualModelSetupCommand: String =
+        "openclaw onboard --non-interactive --accept-risk "
+        + "--auth-choice openrouter-api-key --openrouter-api-key \(manualSetupKeyPlaceholder) "
+        + "--install-daemon --flow quickstart "
+        + "--skip-channels --skip-search --skip-skills --skip-hooks --skip-ui"
</file context>

func refreshStatus() async -> WhatsAppLinkState {
guard process?.isRunning == true else {
if case .error = currentState { return currentState }
await setState(.stopped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the WhatsApp sidecar exits unexpectedly, handleSidecarExit() does not cancel the polling listener task or clear the onNewMessage callback. The listenerTask continues waking every 2.5 s, calls pollEvents(), returns early because process?.isRunning is now false, and goes back to sleep — a perpetual no-op wake loop that also retains the message callback. The intentional stopSidecar() path correctly calls stopListening(); the unexpected-exit path should do the same.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/WhatsAppSendService.swift, line 161:

<comment>When the WhatsApp sidecar exits unexpectedly, `handleSidecarExit()` does not cancel the polling listener task or clear the `onNewMessage` callback. The `listenerTask` continues waking every 2.5 s, calls `pollEvents()`, returns early because `process?.isRunning` is now false, and goes back to sleep — a perpetual no-op wake loop that also retains the message callback. The intentional `stopSidecar()` path correctly calls `stopListening()`; the unexpected-exit path should do the same.</comment>

<file context>
@@ -0,0 +1,582 @@
+  func refreshStatus() async -> WhatsAppLinkState {
+    guard process?.isRunning == true else {
+      if case .error = currentState { return currentState }
+      await setState(.stopped)
+      return currentState
+    }
</file context>
Suggested change
await setState(.stopped)
stopListening()
await setState(.stopped)

var hasEmbeddings: Bool
}

private var indices: [String: Index] = [:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The indices dictionary in AICloneRetrievalService caches per-contact retrieval data but never evicts or removes entries. Over a long-running desktop session, memory usage can grow unbounded because each index stores token sets and optional embedding vectors with no cleanup path. Consider adding an explicit removal method (e.g., removeIndex(for:)), a global clear, or a capacity-bound eviction strategy so the cache doesn't accumulate indefinitely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/macos/Desktop/Sources/AICloneRetrievalService.swift, line 39:

<comment>The `indices` dictionary in `AICloneRetrievalService` caches per-contact retrieval data but never evicts or removes entries. Over a long-running desktop session, memory usage can grow unbounded because each index stores token sets and optional embedding vectors with no cleanup path. Consider adding an explicit removal method (e.g., `removeIndex(for:)`), a global clear, or a capacity-bound eviction strategy so the cache doesn't accumulate indefinitely.</comment>

<file context>
@@ -0,0 +1,171 @@
+    var hasEmbeddings: Bool
+  }
+
+  private var indices: [String: Index] = [:]
+
+  /// Stable key for one historical pair instance (text + turn timestamp), so a
</file context>

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