Skip to content

Fix what the Channels QA found: attachments, live resume, delegated answers, approvals, contacts, and a 1 MiB socket ceiling - #1010

Merged
anukuljha merged 60 commits into
mainfrom
fix/channels-qa
Oct 20, 2026
Merged

anukuljha merged 60 commits into
mainfrom
fix/channels-qa

Conversation

@anukuljha

@anukuljha anukuljha commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What changes

Fixes what a local end-to-end QA of Channels v2 on main (#1000/#1001/#1004) found, and lands the product calls made from it.

Blockers and highs

Was Now
B1 An image attachment bricked its channel for everyone ("This page couldn't load"). The generated TS client read every non-JSON body as text, so a file came back as a string. Binary responses come back as a Blob (patched in the generator script, so regeneration keeps it). channels.file goes through the byte path. A part that can't be shown is a chip that says so, and an error boundary keeps the rest of the timeline drawing.
B2 Resuming a live socket after more than 200 changes silently dropped lines. A cap inside changed_since defeated the too_many probe. Every line arrives, or an honest resync too_many is sent. A batch never runs past its own cursor.
H1 An agent asked a teammate something for a member; the answer never reached the member. The follow-up reply lands in the member's channel.
H2 An approval line hid what it approved (only the agent's title). The line shows the tool and its arguments, with secrets masked and long values clipped.
H3 A member assigned to a contact was never told when the contact wrote. Assigning seats the assignee, and contact lines reach them by push, unread count and inbox.
H4 A failed contact run was silent for everyone, and the contact still counted as answered. The channel asks for a person and the visitor sees "someone will get back to you". The contact isn't counted as answered.
H5 Two quick top-level lines in a group were merged into one run, and the answer landed in the wrong thread. Each line gets its own answer in its own thread.
H6 Edited and deleted lines lived on in the agent's context. The model reads lines as they are now; a deleted line is marked as deleted.
H7 Agent answers in a group were invisible to everyone but the asker, with no explanation. They stay private by default. The asker sees "Only you can see this — it used your access" and can Share with channel. Others see "Bolt answered Priya privately".

Mediums and lows, grouped by area:

  • Core API:
    • NUL bytes, unknown agent mentions and out-of-range numbers are 422s, not 500s.
    • client_message_id is echoed as the client sent it.
    • Read state with threads: thread replies don't count against the timeline, mentions are read where they were said, a reply reads its thread, not the channel, and joining a group starts at its newest line.
    • The last owner can't leave a group ownerless or empty.
    • Thread reply_count is correct, and threads page newest-first.
    • Notices are rate-limited.
    • Idempotency is consistent across routes, and error codes are semantic.
  • Live socket:
    • The inbox stays quiet for readers whose counts didn't move.
    • Pod access is rechecked on open sockets.
    • Bad cursors resync instead of being echoed back.
    • Frames are counted in bytes.
    • Late questions in a burst are still announced.
    • The Python live client handles any topic.
  • Agents:
    • Failed, stopped and refused runs post a notice.
    • A typed reply supersedes a pending ask rather than being recorded as a "deny".
    • Form lines carry their node label.
    • An ask refuses a choice that isn't one of its options.
    • The withheld-lines note gives the real reason.
  • Contacts and deliveries:
    • Delivery status is shown to every reader of the line.
    • Presence holds a push until the line is read, rather than dropping it.
    • The inbox lists contacts waiting on a person.
    • A held contact raises attention.
    • Opt-outs are visible, and a STOP leaves a note.
    • A merge keeps both browsers' chats.
    • Refused bot credentials are surfaced, and a failed surface create no longer blocks a retry.
    • Contacts are searchable by name, email or phone.
    • Assorted WhatsApp fixes.
  • UI:
    • "Reply in thread" focuses the thread box.
    • The edit box has the @mention picker.
    • The answered question shows its answer.
    • The badge matches the bell.
    • Search snippets are clean and highlighted.
    • The About panel and the mobile layout are fixed.
  • CLI: timelines show who said what, user text is never read as Rich markup, and error hints are honest.

Product decisions made in review

  • Channel managers (owner, pod editor or admin) can delete anyone's line, but not edit it; the line shows "Removed by …".
  • Search uses English stemming plus prefix matching (type-ahead), still newest-first.
  • One 1 MiB websocket message ceiling in every launcher (WS_MAX_BYTES; test_ws_max_size_everywhere holds every launcher to it). Bulk bytes travel over the HTTP API:
    • File data: Agent Host file reads and writes move through one-shot, host-scoped HTTP transfers (GET/PUT /agent-host/transfers/{id}), with size and SHA-256 enforced, single use and expiry.
    • Images: run-event images are uploaded as blobs (PUT /agent-host/runs/{run}/blobs/{sha256}) and referenced by digest.
    • What stays on the socket: event batches are capped at 512 KiB, and inline process I/O at 256 KiB.
    • Proof: the real-binary make desktop-agent-host-e2e (17/17) passes a 2 MiB file round trip and a 3 MiB tool-result image reaching the agent whole, both under the 1 MiB cap.
  • Viewers stay read-only everywhere. No email fallback for web contacts, no support-only role, no member names in web chat, and no separate WAITING delivery status.

Why

The QA list, with repro steps and evidence, is summarised above.

How to verify

make quality
make quality-frontend
cd lemma-backend && uv run pytest -n 4 app/modules/channels/tests app/modules/contacts/tests app/modules/agent_surfaces/tests/e2e app/modules/agent/tests/e2e/test_channel_answer_outcomes_e2e.py app/modules/agent/tests/e2e/test_channel_context_e2e.py app/modules/workflow/tests/e2e/test_workflow_work_e2e.py app/modules/datastore/tests/e2e/test_live_ws_e2e.py
cd lemma-frontend && npm test
cd lemma-typescript && npx vitest run
cd lemma-python && uv run pytest tests -m "not integration"
cd lemma-cli && uv run pytest -m "not e2e"

Results on the final branch, after merging main:

  • make quality: 32/32 gates. make quality-frontend and make format-check pass.
  • Backend unit tests: 6,554 passed.
  • Channel, contacts and agent_surfaces e2e: 841/842 before the last fixes. The one failure, a read budget, was then fixed.
  • Agent Host transfers and live e2e: 60 passed.
  • Frontend 1,672, TS SDK 472, Python SDK 204, CLI 899.
  • make desktop-agent-host-e2e with a freshly built host binary: 17/17. cargo test and make desktop-lint pass.

Live re-check on a fresh local stack running this branch:

  • 34 QA items pass, including B1 (an image posts and the channel reloads cleanly), B2 (320-line resume complete; a 620-line gap gives resync too_many), and H1–H7, moderation, search, M1–M11, H3/H4, the WhatsApp window, STOP and the 1 MiB cap.
  • Five findings from that re-check are fixed in this PR as well: a stopped run being finished by the next line, ask-choice casing, the Telegram setup error, an unsubscribe 500 for a deleted pod, and the clipped @ menu.

Checklist

  • Tests cover the behavioral change. Each fix has a regression test through the real path, and the new e2e tests were checked to fail without their fix.
  • Lint, typecheck, and architecture gates pass locally
  • Migrations: no new revision. The unreleased 0058_channels is edited in place (owner's decision; no database has applied it). Its upgrade/downgrade e2e passes.
  • API surface: OpenAPI spec, both SDKs and lemma-client.js regenerated and committed.
  • Compatibility: see below.
  • Security: approval arguments are redacted before display. Shared answers stay hidden from contacts. Socket pod access is rechecked. No new secrets in logs or responses.
  • Rollback: a plain revert. 0058 is edited in place, so a revert means resetting any database created from this branch.

Compatibility and rollback notes

  • 0058_channels edited in place. It adds channel_messages.shared_at, deleted_by_user_id and the search expression; channel_mentions.read_at; channels.replies and channel_read_state.read_replies; and client_message_id becomes VARCHAR(128). No database has applied 0058, per the owner.
  • New and changed API fields:
    • ChannelMessageResponse: visibility, can_share, shared_at, removed_by, and delivered on every readable line.
    • ChannelDetailResponse.opted_out.
    • InboxResponse.contacts.
    • ContactResponse.label and ContactIdentityResponse.opted_out.
    • AgentSurfaceResponse.problem.
    • The thread endpoint gains newest/before_seq and next_before_seq.
    • ReadRequest.thread_root_id.
  • New routes: channel.message.share, and q on GET /contacts.
  • Errors: channel error bodies carry semantic codes; notify_via only accepts known platforms; POST …/members rejects unknown fields; notices can return 429.
  • The 1 MiB websocket ceiling, accepted by the owner. Desktop Agent Hosts built before this change:
    • Keep working: file reads and writes. Their link session is served inline in 256 KiB pieces.
    • Close with 1009: a command printing more than about 750 KB between two reads, or an event batch carrying large images. The host then resends the same batch on every reconnect until Desktop is updated.
    • Both directions are capped. Tool results and command payloads Lemma sends a host above 512 KiB are staged as lemma_staged references, which the host fetches over HTTP and checks against their digest. Commands are packed into frames of at most 960 KiB. One guard in the link writer refuses any frame over 1 MiB with FRAME_TOO_LARGE and keeps the link open. Older hosts (no staged_bodies in hello) get oversized tool-result images dropped, with a note to the agent; an oversized command isn't sent to them, so that run fails at its dispatch deadline.
  • Visitor web stream: a new {"type":"error"} frame, always followed by done.

Left for later, by decision or out of scope:

  • per-path socket ceilings;
  • the email fallback for offline web contacts;
  • a support-only role;
  • rank-ordered search;
  • facts an agent wrote into its own notes before a line was edited or deleted (H6's remaining part: the channel itself now reaches the model as it reads now).

🤖 Generated with Claude Code

anukuljha and others added 30 commits October 20, 2026 12:07
The generated client read every non-JSON body with response.text(), so a
channel line's image arrived as a mangled string typed Blob and the channel
timeline threw on URL.createObjectURL (QA B1). Patch the generator's
getResponseBody (via patch_generated_imports.mjs, so regeneration keeps it)
to return a Blob for anything that is not JSON or text, and route
channels.file through HttpClient.requestBytes like the other file reads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A shared image whose bytes come back as anything but a Blob no longer
throws out of URL.createObjectURL and takes the channel route down
(QA B1): the chip says it could not show the image and still offers the
file, and a boundary around each line's parts keeps the rest of the
timeline drawn if one part cannot be.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pretty output interpreted [..] in user content: a channel titled "[live] x"
lost its prefix, and a member could style text or inject links into another
member's terminal. Values are now escaped where they meet markup in the shared
renderer (lists, details, transcripts, schemas), in fail(), and in the other
renderers that print server-sourced names and messages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
messages list/thread, inbox and search printed the generic table: id, channel,
time, seq, deleted -- no sender, no text. They now show seq, time, sender name
and kind, the text on one line (cut at 100 chars unless --full), and what is
true of the line: replies, edited, deleted, withheld, members only, a question
and its state. --json is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A malformed id printed Python's "badly formed hexadecimal UUID string";
as_uuid now raises LemmaInvalidIdError (a LemmaError and a ValueError) naming
the value, before any request. Every 409 got "Something with that name already
exists", including answering a closed question; that hint is now kept for
409s whose code or message says duplicate, and the rest show the server's
message alone.

Also pins, over the real HTTP layer, that channels.update(topic=None) sends an
explicit null and channels.file() returns the bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The @ menu moves out of the composer into useMentionPicker, which the box
a line is changed in now uses too. The box opens with the caret after the
last word. A change to who the line names sends the parts with it -- the
mentions, and the files and links it already had -- so the newly named are
told, as on a post; a change to the words alone still sends only the words.
The SDK's channels.edit takes those parts (the generated type flattened
them to null).

A space typed straight after a pick is the one the pick already wrote, so
'@bolt ' then ' what' no longer posts a double space.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…kips

- Assigning a contact seats the assignee, and a contact's line is pushed to
  its assignee even when they are not in the channel (H3).
- /inbox lists contacts waiting on a reply: asking for a person, or assigned
  to the reader with the contact's word last (M16).
- A contact held by a member who has not written yet is acknowledged, and a
  held contact's channel asks for a reply until the assignee writes (M17).
- A push is skipped only once the member read the line; having the app open
  holds it a few minutes and it goes if still unread (M15).
- Every reader of a line sees where it went outside Lemma; pushes stay the
  recipient's (M14).
- Delivery retry keeps its attempt count, clears the old reason, and a push
  is retried by its recipient.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- GET /contacts and /contacts/{id} say which handles opted out; a contact's
  channel detail carries opted_out (M18).
- A STOP leaves a members-only note in the contact's channel, naming its
  assignee (M28).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- New channel: the pod's members are asked for again when the dialog
  opens (someone who joined since the space opened was missing), and each
  member checkbox is named for its person.
- An answered agent question says what was answered: answerSummary reads
  the {answers: {header: value}} the API stores, and {approved}.
- About panel: the people picker sits under the people and the agent
  picker under the Agents heading; its selects are painted for the dark
  theme; a truncated name has its full name as a tooltip.
- Mobile: the contact filter chips wrap onto a row of their own, and the
  channel list leaves room under its last row for the floating Ask pill.
- The sidebar's Channels badge counts what the bell counts -- questions
  waiting, mentions, new lines in your own channel -- so the two agree; a
  public group with new lines and nothing for you no longer counts.
- Search results show the line's own words with markdown read as plain
  text, cut at word edges, the match marked; the server's cut only where
  the line's text is withheld.
- The reader's own line is matched by its server id, so a changed
  client_message_id never shows it twice (a test pins it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ere it waits

- A conversation unseated when its contact was merged is folded into the one
  that kept the seat: the agent and the visitor's other browser read it
  (M12; edit in agent/contracts/contact_conversations.py).
- A member's line to a web contact is SENT with a reason saying it waits in
  their chat until they next open it (M13).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ead, say why when there is none

- H1: a run woken by the answers of people the agent asked for the member
  (message_replies / pod-ask answers) posts its answer in the member's
  Lemma channel, after the line the conversation took up last.
- H5: in a group, a line in another thread than the run its agent is
  working on waits (Defer) and is answered by its own run in its own thread.
- H6: the prompt reads a member's line as it says now: edited text, or that
  it was deleted -- never the stored copy.
- H4/M20: a failed or stopped run, or one refused before it starts, says so
  where it was asked. A contact's failed run calls for a person and, if no
  run ever got further, uncounts the contact. Visitor stream gets an
  "error" frame (then "done"); the widget renders it. A members' note naming
  an agent is answered, as a note.
- H2: approval lines show the tool and its arguments, secrets masked, clipped.
- AG-8: the withheld-lines note gives the reason of the view it was read for.
- AG-9: a question written past is CANCELLED, not ANSWERED {"decision":"DENY"}.
- AG-10: a workflow form line carries its step's label.
- L12: an ASK choice must be one of the options (free text goes as text).
- messaging.md: "send everything first" (no wait wording).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ccess on open sockets

- B2: LineRepository.changed_since no longer caps at MAX_LINES, so the
  limit+1 probe can see "too many": a resume or a drain past one read now
  sends every line, or resyncs too_many past LIVE_MAX_CHANGES_PER_BATCH.
- L8: a batch's lines are bounded by the head read for it (up_to_rev): no
  line past its own cursor, no steady-state duplicate line.created.
- L9: subscribed echoes `since` only when it resumes from it; a cursor whose
  line is ahead of its change resyncs cursor_expired.
- M10 (socket): `read` bounds seq to int64 and answers invalid.
- L11: the client frame limit counts UTF-8 bytes.
- L7: an open socket rechecks pod.read on each slot refresh and on
  `presence`, and closes 4403 when it is gone (slot and presence released).
- M21: the inbox remembers what each reader was told; counts that did not
  move send no frame and read no totals.
- L13: new questions are found by query, not in the first 50 lines.
- L10: the Python SDK carries `events` frames (LiveTopicEvent) and any topic
  via LiveSession.add(topic=, params=) and watch_topic().

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A new Telegram surface whose webhook is refused is deleted, not left in
  ERROR holding its name (M26).
- GET /surfaces and /surfaces/{name} carry problem: why the surface cannot
  work (webhook refused, or credentials refused); /setup is not ready and
  leads with it.
- A send answered 401 records its provider status, notes the problem on the
  bot's surface, and a contact's channel asks for a person (M27).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A refused WhatsApp group start says retrying will not help (and names a
  refused token), rather than 'try again shortly'.
- A WhatsApp message from the business's own number is not a person.
- A message in a group the bot did not create leaves a log line.
- WhatsApp /setup is ready once messages arrive through the webhook.
- The email-not-configured push reason no longer carries operator
  instructions; they are in the log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ep the ratchet

channel_answers.py and pause_questions.py grew past the 600-line ratchet:
the agent's answer/notice lines move to channel_answer_lines.py and an
approval's shown action to approval_action.py. with_channel_background
drops back under the complexity cap. A refusal notice is not carried to a
group's place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…last owner

- Text a person sends cannot hold a NUL (post, parts, title, topic, list and
  search q, notices): 422, not a 500 from Postgres. Line numbers are bounded
  by bigint (REST seq params, /read).
- Naming an agent that is not the pod's is refused (422); platform lines drop
  it rather than hit the mention FK.
- client_message_id is kept per member (member:{user}:{name}) and shown back
  to its writer as sent; an Idempotency-Key names the line when no id does,
  and both sent must agree. Reusing a key/name for something else is 422 on
  lines, groups and notices, saying which was sent.
- Read state: unread lines are the timeline's (thread replies excluded, by
  number: channels.replies vs read_state.read_replies); a mention is read
  where it was said (channel_mentions.read_at; /read {seq, thread_root_id});
  a reply reads its thread, not the channel; joining starts at last_seq;
  muted channels are not counted as new.
- A group keeps an owner and a private one somebody: LAST_OWNER /
  LAST_MEMBER (409); the last owner leaving hands ownership on.
- Threads: root carries its reply count, deleted replies leave it, and
  replies page backwards (newest, before_seq).
- Semantic error codes on channel refusals, documented responses on the
  routers; PATCH {} is not an edit; a deleted last line says deleted; list q
  matches every word and DIRECT/CONTACT by person; snippets keep words whole;
  notify_via is a known platform; members add forbids unknown fields;
  notices are rate limited per sender.

Schema: edits to the unreleased 0058 in place (client_message_id 128,
channel_mentions.read_at + partial index, channels.replies,
channel_read_state.read_replies).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
anukuljha and others added 19 commits October 20, 2026 13:59
uvicorn's ws_max_size is now WS_MAX_BYTES (app/core/server_limits.py),
passed explicitly by every start of the API -- Dockerfile (plus
UVICORN_WS_MAX_SIZE), Railway, compose, make dev, Desktop, the local stack,
the harnesses -- and a gate test holds each to the constant. A message past
it closes the channels live socket and the datastore /changes socket with
1009 as it arrives.

The ceiling stays at 16 MiB, not the 1 MiB asked for: the Agent Host link
is a socket of the same server and installed hosts send messages past 1 MiB
on it (op data, process output, inline images). At 1 MiB the real-binary
host e2e fails its 2 MiB file round trip.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… one websocket ceiling

# Conflicts:
#	lemma-backend/app/modules/channels/domain/entities.py
#	lemma-backend/app/modules/channels/infrastructure/message_models.py
#	lemma-backend/app/modules/channels/infrastructure/rows.py
#	lemma-backend/app/modules/channels/services/messages.py
#	lemma-backend/docs/modules/channels.md
#	lemma-backend/migrations/versions/2026-10-17_channels_0058.py
#	lemma-frontend/tests/channel-timeline-dom.test.ts
Owner decision: nothing a client means to send over a socket is larger
than 1 MiB. Files and other bulk bytes travel over the HTTP API.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
config_source stays a string on the wire (validated server-side, 422 for an
unknown source): a named enum becomes a strict TypeScript enum in the
generated client and breaks every caller passing the literal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…is unread

Marking a timeline (or thread) read joined channel_mentions to
channel_messages on every read -- including the read a member's own line
takes as it is posted -- so a member's first Telegram message read one more
channel_messages than its budget. Whether anything naming them is unread is
now asked of the unread-only mention index (in the read-state update's
RETURNING for the timeline), and the lines are visited only then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The API reads no websocket message past 1 MiB, and the link sent more: a
file.read answer carried up to 1 MiB of base64 (1.33 MiB), and a batch of
256 events carried generated images inline. A 1009 closes the link for every
run on it, and the host resends the same frame after each reconnect.

File chunks now cross HTTP as transfers the op names. file.write names a
chunk Lemma staged (GET /agent-host/transfers/{id}); file.read names a slot
the host fills (PUT /agent-host/transfers/{id}, x-lemma-sha256) and answers
size + sha256 instead of data. A transfer is the host's own (host secret as
bearer, like the link), used once, digest- and size-checked, and expires a
minute past its op's deadline; bytes wait in Redis. The relay does the HTTP
half outside the sandbox, so the exec-server is unchanged.

Run-event images go up first as blobs of the run (PUT
/agent-host/runs/{run_id}/blobs/{sha256}, only for a run the host holds,
kept as long as the run's event stream) and the event names them with
lemma_blob; the artifact writer reads them back. Event batches are bounded by
bytes (512 KiB) on the host, an event still too large has its longest strings
cut, and any frame past 1 MiB is refused before it is sent.

What still rides the link inline is held to 256 KiB: process.read (new
max_bytes), process.input, secret.deliver, initial_input. A host without the
new `transfers` capability is served by its link session, which inlines a
write's chunk and delivers a clamped read's answer into the slot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moves sender_values and nameless from lines.py to rows.py, keeping
lines.py inside the architecture ratchet's 600 lines.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…s regenerated

ReadState (mark_read, mark_thread_read, read_seq(s)) moves out of
members.py into read_state.py, mixed into MemberRepository, keeping both
inside the architecture ratchet. The share route gets its contract entry and
route-inventory line; scenario coverage is regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…red for itself

A run stopped half way left its request and tool calls in history with
nothing saying it was over; the next run in the conversation finished it
and posted "Done" under the new line. The history now closes a stopped
run with a notice, before anything said after Stop.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
"Cats" was refused against the option "cats". Choices now match without
regard to case and are stored as the option spells them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The refusal told the member to set the webhook manually to the address of
a surface that had just been deleted, and a webhook set by hand would lack
the secret Lemma checks. It now says Telegram refused the bot, with
Telegram's reason, and to check the token and try again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A validly signed token naming a pod that is gone failed the suppression's
pod foreign key and answered 500. It now answers the friendly 404 page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a line near the top of the timeline the menu opened upward and its
first names were hidden under the channel header. It now measures the room
between the box and the top of what it scrolls in, and opens below when the
menu would not fit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The send is still out when the assertion runs; asserting before React
flushed the pending state failed about half the runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 336 files, which is 186 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5b0c84f9-fba7-49f5-bcec-265c7617b067
📥 Commits

Reviewing files that changed from the base of the PR and between ecfd9f1 and fd362dc.

⛔ Files ignored due to path filters: 1
  • desktop/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing: 336
  • .gitleaks.toml
  • .typos.toml
  • Makefile
  • deploy/compose/docker-compose.yml
  • deploy/railway/railway.json
  • desktop/agent-host/Cargo.toml
  • desktop/agent-host/src/host_exec/process.rs
  • desktop/agent-host/src/host_exec/relay.rs
  • desktop/agent-host/src/host_exec/wire.rs
  • desktop/agent-host/src/journal/events.rs
  • desktop/agent-host/src/journal/tests/events.rs
  • desktop/agent-host/src/link/connection.rs
  • desktop/agent-host/src/link/mod.rs
  • desktop/agent-host/src/link/protocol.rs
  • desktop/agent-host/src/link/stub.rs
  • desktop/agent-host/src/link/tests.rs
  • desktop/agent-host/src/link/transfer.rs
  • desktop/agent-host/src/runtime/event_payloads.rs
  • desktop/agent-host/src/runtime/events.rs
  • desktop/agent-host/src/runtime/mod.rs
  • desktop/agent-host/src/runtime/tests/target_worker/control.rs
  • desktop/agent-host/src/runtime/tests/target_worker/events.rs
  • desktop/agent-host/src/runtime/worker.rs
  • desktop/agent-host/tests/fixtures/scripted_acp_agent.py
  • desktop/agent-host/tests/fixtures/wire_contract.json
  • desktop/agent-host/tests/seatbelt.rs
  • desktop/e2e/launch_smoke.py
  • desktop/e2e/throwaway_stack.py
  • desktop/locald/src/native_host_pack/build.rs
  • docs/architecture/agent-host-events.md
  • docs/architecture/agent-host.md
  • docs/architecture/desktop-host-execution.md
  • docs/product/coverage.md
  • lemma-backend/Dockerfile
  • lemma-backend/app/app.py
  • lemma-backend/app/core/live/api.py
  • lemma-backend/app/core/live/pod_access.py
  • lemma-backend/app/core/live/presence.py
  • lemma-backend/app/core/live/socket.py
  • lemma-backend/app/core/log/event_catalog.py
  • lemma-backend/app/core/server_limits.py
  • lemma-backend/app/core/tests/unit/test_ws_max_size_everywhere.py
  • lemma-backend/app/modules/agent/api/controllers/agent_host_transfer_controller.py
  • lemma-backend/app/modules/agent/contracts/contact_conversations.py
  • lemma-backend/app/modules/agent/contracts/folded_history.py
  • lemma-backend/app/modules/agent/contracts/visitor_stream.py
  • lemma-backend/app/modules/agent/domain/agent_host_link.py
  • lemma-backend/app/modules/agent/domain/agent_host_ops.py
  • lemma-backend/app/modules/agent/domain/agent_host_transfer.py
  • lemma-backend/app/modules/agent/domain/surface_prompts.py
  • lemma-backend/app/modules/agent/events/channel_answer_work.py
  • lemma-backend/app/modules/agent/infrastructure/agent_host/transfers.py
  • lemma-backend/app/modules/agent/infrastructure/harnesses/agent_host/artifacts.py
  • lemma-backend/app/modules/agent/infrastructure/harnesses/channel_context.py
  • lemma-backend/app/modules/agent/module.py
  • lemma-backend/app/modules/agent/prompts/messaging.md
  • lemma-backend/app/modules/agent/services/agent_host_link_ops.py
  • lemma-backend/app/modules/agent/services/agent_host_link_outbound.py
  • lemma-backend/app/modules/agent/services/agent_host_link_pusher.py
  • lemma-backend/app/modules/agent/services/agent_host_link_session.py
  • lemma-backend/app/modules/agent/services/agent_host_link_wire.py
  • lemma-backend/app/modules/agent/services/agent_host_ops.py
  • lemma-backend/app/modules/agent/services/approval_action.py
  • lemma-backend/app/modules/agent/services/channel_answer_lines.py
  • lemma-backend/app/modules/agent/services/channel_answers.py
  • lemma-backend/app/modules/agent/services/channel_background.py
  • lemma-backend/app/modules/agent/services/contact_run_failures.py
  • lemma-backend/app/modules/agent/services/pause_questions.py
  • lemma-backend/app/modules/agent/services/pause_supersession.py
  • lemma-backend/app/modules/agent/services/runtime_history.py
  • lemma-backend/app/modules/agent/tests/e2e/test_agent_host_process_e2e.py
  • lemma-backend/app/modules/agent/tests/e2e/test_agent_host_transfers_e2e.py
  • lemma-backend/app/modules/agent/tests/e2e/test_channel_answer_outcomes_e2e.py
  • lemma-backend/app/modules/agent/tests/e2e/test_channel_answers_e2e.py
  • lemma-backend/app/modules/agent/tests/unit/agent_host_link_fakes.py
  • lemma-backend/app/modules/agent/tests/unit/test_agent_host_link_outbound.py
  • lemma-backend/app/modules/agent/tests/unit/test_agent_host_transfers.py
  • lemma-backend/app/modules/agent/tests/unit/test_agent_host_wire_contract.py
  • lemma-backend/app/modules/agent/tests/unit/test_outsider_isolation.py
  • lemma-backend/app/modules/agent/tests/unit/test_pause_questions.py
  • lemma-backend/app/modules/agent/tests/unit/test_runtime_history_prefix.py
  • lemma-backend/app/modules/agent/tests/unit/test_visitor_stream.py
  • lemma-backend/app/modules/agent_surfaces/api/controllers/contact_unsubscribe_controller.py
  • lemma-backend/app/modules/agent_surfaces/api/controllers/notice_controller.py
  • lemma-backend/app/modules/agent_surfaces/api/controllers/public_web_controller.py
  • lemma-backend/app/modules/agent_surfaces/api/controllers/source_controller.py
  • lemma-backend/app/modules/agent_surfaces/api/controllers/surface_controller.py
  • lemma-backend/app/modules/agent_surfaces/contracts/provisioning.py
  • lemma-backend/app/modules/agent_surfaces/domain/surface_representation.py
  • lemma-backend/app/modules/agent_surfaces/platforms/delivery.py
  • lemma-backend/app/modules/agent_surfaces/platforms/whatsapp/parser.py
  • lemma-backend/app/modules/agent_surfaces/public/widget.js
  • lemma-backend/app/modules/agent_surfaces/services/channel_deliveries.py
  • lemma-backend/app/modules/agent_surfaces/services/contact_acknowledgement.py
  • lemma-backend/app/modules/agent_surfaces/services/contacts.py
  • lemma-backend/app/modules/agent_surfaces/services/created_groups.py
  • lemma-backend/app/modules/agent_surfaces/services/delivery_outcome.py
  • lemma-backend/app/modules/agent_surfaces/services/direct_messages.py
  • lemma-backend/app/modules/agent_surfaces/services/notification_channels.py
  • lemma-backend/app/modules/agent_surfaces/services/notification_delivery.py
  • lemma-backend/app/modules/agent_surfaces/services/opt_out_notes.py
  • lemma-backend/app/modules/agent_surfaces/services/outside_cap.py
  • lemma-backend/app/modules/agent_surfaces/services/push_sends.py
  • lemma-backend/app/modules/agent_surfaces/services/surface_problems.py
  • lemma-backend/app/modules/agent_surfaces/services/surface_service.py
  • lemma-backend/app/modules/agent_surfaces/services/surface_setup_read.py
  • lemma-backend/app/modules/agent_surfaces/services/surface_telegram_webhook.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/mock_infrastructure.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/test_contact_care_e2e.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/test_delivery_outcomes_e2e.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/test_push_budget_e2e.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/test_surface_api_e2e.py
  • lemma-backend/app/modules/agent_surfaces/tests/e2e/test_web_widgets_e2e.py
  • lemma-backend/app/modules/agent_surfaces/tests/unit/test_configuration_access.py
  • lemma-backend/app/modules/agent_surfaces/tests/unit/test_platform_new_adapters.py
  • lemma-backend/app/modules/channels/api/access.py
  • lemma-backend/app/modules/channels/api/attachments.py
  • lemma-backend/app/modules/channels/api/case_controller.py
  • lemma-backend/app/modules/channels/api/controller.py
  • lemma-backend/app/modules/channels/api/inbox_controller.py
  • lemma-backend/app/modules/channels/api/member_controller.py
  • lemma-backend/app/modules/channels/api/message_controller.py
  • lemma-backend/app/modules/channels/api/message_schemas.py
  • lemma-backend/app/modules/channels/api/schemas.py
  • lemma-backend/app/modules/channels/api/views.py
  • lemma-backend/app/modules/channels/contracts/__init__.py
  • lemma-backend/app/modules/channels/contracts/answers.py
  • lemma-backend/app/modules/channels/contracts/deliveries.py
  • lemma-backend/app/modules/channels/contracts/places.py
  • lemma-backend/app/modules/channels/domain/entities.py
  • lemma-backend/app/modules/channels/domain/idempotency.py
  • lemma-backend/app/modules/channels/domain/projection.py
  • lemma-backend/app/modules/channels/infrastructure/access.py
  • lemma-backend/app/modules/channels/infrastructure/accounts.py
  • lemma-backend/app/modules/channels/infrastructure/cases.py
  • lemma-backend/app/modules/channels/infrastructure/contact_channels.py
  • lemma-backend/app/modules/channels/infrastructure/deliveries.py
  • lemma-backend/app/modules/channels/infrastructure/fold_lines.py
  • lemma-backend/app/modules/channels/infrastructure/inbox.py
  • lemma-backend/app/modules/channels/infrastructure/line_search.py
  • lemma-backend/app/modules/channels/infrastructure/lines.py
  • lemma-backend/app/modules/channels/infrastructure/members.py
  • lemma-backend/app/modules/channels/infrastructure/message_models.py
  • lemma-backend/app/modules/channels/infrastructure/models.py
  • lemma-backend/app/modules/channels/infrastructure/read_state.py
  • lemma-backend/app/modules/channels/infrastructure/repository.py
  • lemma-backend/app/modules/channels/infrastructure/revisions.py
  • lemma-backend/app/modules/channels/infrastructure/rows.py
  • lemma-backend/app/modules/channels/services/live/events.py
  • lemma-backend/app/modules/channels/services/live/hub.py
  • lemma-backend/app/modules/channels/services/live/inbox_feed.py
  • lemma-backend/app/modules/channels/services/live/readers.py
  • lemma-backend/app/modules/channels/services/live/timeline.py
  • lemma-backend/app/modules/channels/services/live/topics.py
  • lemma-backend/app/modules/channels/services/messages.py
  • lemma-backend/app/modules/channels/services/posting.py
  • lemma-backend/app/modules/channels/services/pushes.py
  • lemma-backend/app/modules/channels/services/reading.py
  • lemma-backend/app/modules/channels/tests/e2e/channel_scale.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_api_guards_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_files_and_views_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_input_refusals_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_line_writes_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_permissions_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_read_budgets_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_read_state_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_search_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channel_search_scale_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channels_live_access_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channels_live_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_channels_v2_api_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_inbox_feed_statements_e2e.py
  • lemma-backend/app/modules/channels/tests/e2e/test_shared_answers_e2e.py
  • lemma-backend/app/modules/channels/tests/unit/test_delivery_producers.py
  • lemma-backend/app/modules/connectors/api/schemas/__init__.py
  • lemma-backend/app/modules/connectors/tests/unit/test_auth_config_schemas.py
  • lemma-backend/app/modules/contacts/api/controller.py
  • lemma-backend/app/modules/contacts/contracts/__init__.py
  • lemma-backend/app/modules/contacts/contracts/outreach.py
  • lemma-backend/app/modules/contacts/infrastructure/repository.py
  • lemma-backend/app/modules/contacts/infrastructure/suppressions.py
  • lemma-backend/app/modules/contacts/services/outreach.py
  • lemma-backend/app/modules/contacts/tests/e2e/test_contact_listing_e2e.py
  • lemma-backend/app/modules/contacts/tests/e2e/test_outreach_e2e.py
  • lemma-backend/app/modules/datastore/tests/e2e/test_live_ws_e2e.py
  • lemma-backend/app/modules/test_support/e2e/runtime.py
  • lemma-backend/app/modules/workflow/events/step_jobs.py
  • lemma-backend/app/modules/workflow/infrastructure/form_questions.py
  • lemma-backend/app/modules/workflow/tests/e2e/test_workflow_work_e2e.py
  • lemma-backend/app/modules/workspace/providers/agent_host.py
  • lemma-backend/app/modules/workspace/providers/agent_host_ops.py
  • lemma-backend/app/modules/workspace/services/host_workspace.py
  • lemma-backend/app/modules/workspace/tests/unit/test_agent_host_provider.py
  • lemma-backend/docs/contracts/channels.md
  • lemma-backend/docs/modules/agent.md
  • lemma-backend/docs/modules/agent_surfaces.md
  • lemma-backend/docs/modules/channels.md
  • lemma-backend/docs/modules/contacts.md
  • lemma-backend/docs/modules/live.md
  • lemma-backend/docs/modules/route-inventory.md
  • lemma-backend/load_tests/docker-compose.load-test.yml
  • lemma-backend/load_tests/docker-compose.workers.yml
  • lemma-backend/load_tests/live_socket_bench.py
  • lemma-backend/local_app.py
  • lemma-backend/migrations/versions/2026-10-17_channels_0058.py
  • lemma-backend/standalone_app.py
  • lemma-cli/lemma_cli/cli_core/channel_view.py
  • lemma-cli/lemma_cli/cli_core/chat.py
  • lemma-cli/lemma_cli/cli_core/commands/_grants.py
  • lemma-cli/lemma_cli/cli_core/commands/channel_lines.py
  • lemma-cli/lemma_cli/cli_core/commands/channels.py
  • lemma-cli/lemma_cli/cli_core/commands/connectors.py
  • lemma-cli/lemma_cli/cli_core/commands/conversations.py
  • lemma-cli/lemma_cli/cli_core/commands/pods.py
  • lemma-cli/lemma_cli/cli_core/errors.py
  • lemma-cli/lemma_cli/cli_core/io.py
  • lemma-cli/lemma_cli/cli_core/select.py
  • lemma-cli/lemma_cli/cli_core/state.py
  • lemma-cli/tests/e2e/conftest.py
  • lemma-cli/tests/test_channel_view.py
  • lemma-cli/tests/test_error_attribution.py
  • lemma-cli/tests/test_io_rendering.py
  • lemma-frontend/public/openapi.json
  • lemma-frontend/src/channels/api.ts
  • lemma-frontend/src/channels/channel-page.tsx
  • lemma-frontend/src/channels/channel-panel.tsx
  • lemma-frontend/src/channels/composer.tsx
  • lemma-frontend/src/channels/line.tsx
  • lemma-frontend/src/channels/mention-picker.tsx
  • lemma-frontend/src/channels/model.ts
  • lemma-frontend/src/channels/new-channel.tsx
  • lemma-frontend/src/channels/parts.tsx
  • lemma-frontend/src/channels/sample-api.ts
  • lemma-frontend/src/channels/search-model.ts
  • lemma-frontend/src/channels/search.tsx
  • lemma-frontend/src/channels/thread-panel.tsx
  • lemma-frontend/src/channels/use-channel-live.ts
  • lemma-frontend/src/channels/waiting-panel.tsx
  • lemma-frontend/src/space/space-nav.tsx
  • lemma-frontend/src/styles/channels.css
  • lemma-frontend/tests/channel-composer-dom.test.ts
  • lemma-frontend/tests/channel-live.test.ts
  • lemma-frontend/tests/channel-panels-dom.test.ts
  • lemma-frontend/tests/channel-parts.test.ts
  • lemma-frontend/tests/channel-timeline-dom.test.ts
  • lemma-frontend/tests/channels.test.ts
  • lemma-python/lemma_sdk/__init__.py
  • lemma-python/lemma_sdk/_spec_info.py
  • lemma-python/lemma_sdk/errors.py
  • lemma-python/lemma_sdk/live.py
  • lemma-python/lemma_sdk/openapi_client/api/agent_surfaces/agent_notice_send.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_case_list.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_create.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_delete.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_delivery_list.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_delivery_retry.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_get.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_inbox_get.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_list.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_me_create.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_me_get.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_add.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_join.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_leave.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_list.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_me_update.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_remove.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_member_update.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_answer.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_delete.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_file.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_list.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_post.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_share.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_thread.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_message_update.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_read.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_search.py
  • lemma-python/lemma_sdk/openapi_client/api/channels/channel_update.py
  • lemma-python/lemma_sdk/openapi_client/api/contacts/contact_list.py
  • lemma-python/lemma_sdk/openapi_client/models/__init__.py
  • lemma-python/lemma_sdk/openapi_client/models/agent_surface_response.py
  • lemma-python/lemma_sdk/openapi_client/models/channel_detail_response.py
  • lemma-python/lemma_sdk/openapi_client/models/channel_message_response.py
  • lemma-python/lemma_sdk/openapi_client/models/channel_message_response_visibility.py
  • lemma-python/lemma_sdk/openapi_client/models/channel_post_request.py
  • lemma-python/lemma_sdk/openapi_client/models/contact_identity_response.py
  • lemma-python/lemma_sdk/openapi_client/models/contact_response.py
  • lemma-python/lemma_sdk/openapi_client/models/history_message.py
  • lemma-python/lemma_sdk/openapi_client/models/inbox_response.py
  • lemma-python/lemma_sdk/openapi_client/models/inbox_totals.py
  • lemma-python/lemma_sdk/openapi_client/models/line_delivery.py
  • lemma-python/lemma_sdk/openapi_client/models/line_preview.py
  • lemma-python/lemma_sdk/openapi_client/models/members_add_request.py
  • lemma-python/lemma_sdk/openapi_client/models/my_membership_update.py
  • lemma-python/lemma_sdk/openapi_client/models/my_membership_update_notify_via_type_0_item.py
  • lemma-python/lemma_sdk/openapi_client/models/read_request.py
  • lemma-python/lemma_sdk/openapi_client/models/thread_response.py
  • lemma-python/lemma_sdk/openapi_spec.json
  • lemma-python/lemma_sdk/resources/agent_hosts.py
  • lemma-python/lemma_sdk/resources/base.py
  • lemma-python/lemma_sdk/resources/channels.py
  • lemma-python/tests/test_live.py
  • lemma-python/tests/test_sdk_channels.py
  • lemma-stack/lemma_stack/host_pack.py
  • lemma-stack/lemma_stack/stack/specs.py
  • lemma-typescript/public/lemma-client.js
  • lemma-typescript/scripts/patch_generated_imports.mjs
  • lemma-typescript/src/__tests__/binary-downloads.test.ts
  • lemma-typescript/src/__tests__/channel-share.test.ts
  • lemma-typescript/src/__tests__/channels-namespace.test.ts
  • lemma-typescript/src/client.ts
  • lemma-typescript/src/index.ts
  • lemma-typescript/src/namespaces/channels.ts
  • lemma-typescript/src/openapi_client/core/request.ts
  • lemma-typescript/src/openapi_client/index.ts
  • lemma-typescript/src/openapi_client/models/AgentSurfaceResponse.ts
  • lemma-typescript/src/openapi_client/models/ChannelDetailResponse.ts
  • lemma-typescript/src/openapi_client/models/ChannelMessageResponse.ts
  • lemma-typescript/src/openapi_client/models/ChannelPostRequest.ts
  • lemma-typescript/src/openapi_client/models/ContactIdentityResponse.ts
  • lemma-typescript/src/openapi_client/models/ContactResponse.ts
  • lemma-typescript/src/openapi_client/models/HistoryMessage.ts
  • lemma-typescript/src/openapi_client/models/InboxResponse.ts
  • lemma-typescript/src/openapi_client/models/InboxTotals.ts
  • lemma-typescript/src/openapi_client/models/LineDelivery.ts
  • lemma-typescript/src/openapi_client/models/LinePreview.ts
  • lemma-typescript/src/openapi_client/models/MyMembershipUpdate.ts
  • lemma-typescript/src/openapi_client/models/ReadRequest.ts
  • lemma-typescript/src/openapi_client/models/ThreadResponse.ts
  • lemma-typescript/src/openapi_client/services/AgentSurfacesService.ts
  • lemma-typescript/src/openapi_client/services/ChannelsService.ts
  • lemma-typescript/src/openapi_client/services/ContactsService.ts
  • lemma-typescript/src/openapi_spec.json
  • tests/scenarios/harness/compose_stack.py
  • tests/scenarios/harness/stack.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread lemma-backend/app/modules/agent/tests/unit/test_pause_questions.py Fixed
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

CodeQL

No new findings on lines this pull request changed (as of fd362dc).

Updated on every push. A finding judged safe goes in .codeql-allow.txt with its reason, or is dismissed in the Security tab. GitHub Code Quality posts its own review comments separately.

Comment thread lemma-backend/app/modules/agent/tests/unit/test_pause_questions.py Outdated
anukuljha and others added 2 commits October 20, 2026 16:44
The host-to-Lemma direction was bounded; Lemma-to-host was not. An mcp_ok
carried tool-result images inline (up to 5 MiB, ~6.7 MiB base64), commands
and control_ok carried every due command with unbounded prompts, and an old
host's op got a 1 MiB write chunk inline (1.33 MiB).

Large parts are now staged for the host to fetch: a tool result or command
payload past 512 KiB is replaced by {"lemma_staged": {id, size, sha256}} and
the host GETs it from /agent-host/transfers/{id}, checks size and digest, and
puts it back before anyone reads the frame (LinkHandle::mcp, the worker's
command intake). Staged bodies are reusable until they expire, because a
command or tool answer may be sent again. Commands are packed into frames of
at most 960 KiB; what does not fit beside a control_ok follows as pushes.

One guard in LinkWriter.send_frame refuses any outbound frame past
WS_MAX_BYTES (agent.agent_host_link.frame_too_large): an answer becomes a
FRAME_TOO_LARGE error for that request, a push or op fails to its sender, and
the link stays open.

Hosts say they fetch staged parts with staged_bodies on hello. An older host
gets tool results with images dropped (and a note to the agent), and is not
sent a command too large for a frame. Write chunks are now 512 KiB so an old
host's inline chunk fits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…deQL false positive

- test_a_member_assigned_a_contact_is_told_when_they_write drives work
  in-process, so it is marked drives_work (test_drives_work_contract).
- gitleaks generic-api-key read `secret file", ttl_seconds=60` as a key
  and value; the call now puts each argument on its own line, and the
  commit already in this branch's history is allowlisted by exact value,
  like the ttl_seconds=3600 entry.
- CodeQL py/incomplete-url-substring-sanitization flagged a test assertion
  on a bare hostname; it asserts on the whole URL instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@deepak-jha-kgp

Copy link
Copy Markdown
Contributor

Factory review: Needs a person's review

first round · reviewed at e829a2b · updated 20 Oct, 11:34 UTC · high risk

Channels QA fixes land, and the Mac's bytes leave the socket

  • Before: A channel answered a line, told every other open browser over one socket, and counted what was new by line number alone. The Agent Host's file chunks and images rode that same socket, and a mention stayed unread even after the member read the line it was in.
  • After: The link now carries only what moves -- an id, a size, a digest -- and the bytes cross two host-authenticated HTTP routes into Redis, used once; the API reads no websocket message past 1 MiB. An answer made with a member's own access can be shared with the channel, a mention is read where it was said, and whoever may change a channel can remove anybody's line.

What could break

  • If any database has already applied migration 0058 -- any environment that has run main since it merged -- then the new columns never arrive and the channels inbox queries fail on an undefined column, or, where the columns are created without a backfill, every channel's unread badge silently stops appearing for channels that have replies. (medium likelihood, high impact; guard: lemma-backend/scripts/check_migration_order.py protects only migrations that shipped in a desktop-nightly tag, and it passes; nothing covers the API deployment)
  • If a member shares an agent's answer made with their own access, everybody who may read the channel reads it from then on -- including, in a contact's channel, the contact. (low likelihood, high impact; guard: domain.projection.shareable_by (asker only) + withheld_from; test_shared_answers_e2e.py)
  • If a Mac still runs a Desktop host older than this release and a command prints more than about 750 KB between two reads, its link is closed with 1009 mid-run and the run ends without an answer. (medium likelihood, medium impact; guard: none; the case is documented in OldHostTransfers and no test covers it)

691 of 20,260 changed lines worth reading. The rest is mechanical, generated or lockfiles.

Decisions this PR makes

  1. Every launcher of the API caps a websocket message at 1 MiB — A message is assembled before the app sees it, so a check in the app comes after the memory is spent; one launcher left out serves 16 MiB again and nothing else notices (lemma-backend/app/core/server_limits.py)
  2. An Agent Host's bulk bytes move off the link to HTTP transfers and blobs — A socket is the wrong place for a file, and a host that is not updated cannot move one; the wrong host, direction or digest would hand a file to the wrong machine (lemma-backend/app/modules/agent/infrastructure/agent_host/transfers.py)
  3. A member may share an answer made with their own access with the channel — Sharing publishes something made with one person's access to everybody who may read the channel, and in a contact's channel that includes the contact (lemma-backend/app/modules/channels/domain/projection.py)
  4. A mention is read where it was said, and unread is counted from reply counters — The badge, the bell and the sidebar now all read the same counters, so a wrong counter means a member never sees that they were named or that something is new (lemma-backend/app/modules/channels/infrastructure/read_state.py)
  5. Whoever may change a channel may delete anybody's line — Removing another person's words is a moderation power; it is shown as removed by the remover, and a copy already sent outside stays as sent (lemma-backend/app/modules/channels/services/messages.py)

Findings · 2 open

  • The new columns arrive by editing a migration that is already on main — lemma-backend/migrations/versions/2026-10-17_channels_0058.py:77
  • An old Desktop host's large process.read now closes its link with 1009 — lemma-backend/app/modules/agent/services/agent_host_link_ops.py:304
Worth fixing · 2 warnings

1. The new columns arrive by editing a migration that is already on main — lemma-backend/migrations/versions/2026-10-17_channels_0058.py:77

This PR adds channels.replies (line 77), channel_read_state.read_replies (195), channel_mentions.read_at (317), channel_messages.shared_at/deleted_by_user_id (266-267), a new index (294) and the stored search vector's english half (255) by editing 2026-10-17_channels_0058.py, which main already has (main's copy has the simple vector only and none of the columns; main's newest revision 0059 depends on it). Alembic does not re-run an applied revision, so on any database that has already run 0058 -- every environment that has run main since that migration merged -- the columns never appear, and the code that reads them fails: InboxReads.unread reads channel_read_state.read_replies and channel.replies (app/modules/channels/infrastructure/inbox.py:105-118), ReadState.mark_read writes read_replies (read_state.py:36), and the mention counts filter on read_at IS NULL (inbox.py:140). Where the columns are created but no history is backfilled, (last_seq - last_read_seq) - (replies - read_replies) goes negative, so a channel with replies shows nothing new. What to check: whether any deployed or developer database has already run 0058 (its alembic_version is 0058_channels).

Suggested fix:

Add a 0060 migration after main's 0059 with the added columns, the index and the generated `search` expression, plus a backfill: `UPDATE channels c SET replies = (SELECT count(*) FROM channel_messages m WHERE m.channel_id = c.id AND m.thread_root_id IS NOT NULL)`, and for each `channel_read_state` row set `read_replies` to the replies up to its `last_read_seq`. If instead no database has run 0058, say so in the PR and leave it as it is.

2. An old Desktop host's large process.read now closes its link with 1009 — lemma-backend/app/modules/agent/services/agent_host_link_ops.py:304-318

OldHostTransfers (line 304) is what serves a host released before transfers. Its own docstring says: "What this cannot bound is process.read: such a host ignores max_bytes, and a command that prints more than about 750 KB between two reads closes its link with 1009." The new ceiling is 1 MiB for every socket (app/core/server_limits.py:27), and the old host's own bound is LINK_EVENT_BATCH_MAX_BYTES (512 KiB) for events only -- process output is not bounded. So a user on a not-yet-updated Desktop running a chatty command (a build log, cat of a large file) gets the link closed mid-run, and the run ends with no answer, where before this PR the same read was served. Nothing covers the case: no test drives an old host with output past the ceiling.

Suggested fix:

Either ship the Desktop host before the ceiling reaches users and say so in the rollout notes, or have the relay bound what it forwards for an old host (`OldHostTransfers.outbound` could cap `process.read`'s answer before it is re-encoded, refusing with `TRANSFER_INCOMPLETE` instead of letting the socket close), and add a test that drives an old host with output over 1 MiB.

One comment, edited after every round. Last updated 20 Oct, 11:34 UTC.

@github-actions

Copy link
Copy Markdown
Module Unit E2E only Combined
Overall 73.1% 82.3% 91.6%
agent 78.6% 83.3% 93.0%
agent_surfaces 72.9% 83.0% 90.8%
apps 75.7% 87.6% 92.4%
connectors 78.7% 73.6% 90.2%
datastore 69.1% 86.4% 90.9%
function 70.2% 87.7% 92.8%
icon 88.8% 77.1% 89.6%
identity 64.4% 86.3% 92.5%
pod 62.3% 89.5% 92.0%
pod_bundle 75.8% 69.5% 88.2%
schedule 63.5% 89.1% 91.8%
usage 74.4% 84.5% 91.9%
workflow 70.0% 82.9% 89.9%
workspace 79.4% 59.4% 86.8%

@anukuljha
anukuljha merged commit f3b5b89 into main Oct 20, 2026
51 checks passed
anukuljha added a commit that referenced this pull request Oct 20, 2026
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.

3 participants