import type { CommsAddress, CommsChannel, CommsMessage } from "./comms-types.js"; import { type CommsThreadArtifact } from "./comms-evidence.js"; import { type InboxRenderOptions } from "./comms-inbox.js"; import { type EmailSendProfile } from "./comms-email-catch.js"; import { type DetachedTimers } from "./e2b-detached.js"; import type { E2BDesktopSandbox } from "./e2b-desktop-launch.js"; /** The default in-sandbox loopback port for the catch. Fixed (not ephemeral) so the injected base-URL * env is known before `createDesktopSandbox`. 8025 is the conventional local-mail-UI port and is * unlikely to collide with a subject app; override via config if it does. */ export declare const DEFAULT_SANDBOX_CATCH_PORT = 8025; /** * The self-contained in-sandbox capture server — a plain **python3** script (stdlib only), because the * stock E2B desktop template ships python3 but NOT node, and the co-located catcher must run in a * runtime the sandbox guarantees (the precedented choice: LocalStack is a python catcher the app points * at; you pick the runtime the environment has). It runs on the sandbox's own python3, imports nothing * from humanish. DELIBERATELY dumb: it records each POST verbatim as an NDJSON line `{t, path, body}` * and returns a plausible provider success — all normalization/profile parsing happens host-side on the * drained lines, so the typed, tested profiles stay in one place. It also serves the host-rendered inbox * surface statically at /inbox + /api/inbox (with a script-forbidding CSP). argv: * [inboxPort]. The capture listener binds 127.0.0.1 (loopback) — the app under test reaches * it in-sandbox, nothing on the internet can inject a fake send. When an [inboxPort] is given (the * shared-world route, where the persona lives in a DIFFERENT sandbox), it ALSO starts a READ-ONLY inbox * listener on 0.0.0.0: so getHost can proxy the persona's inbox reads to it; that listener * serves GET only (POST → 405). The CUA same-sandbox route omits it and stays loopback-only. */ export declare const SANDBOX_CATCH_SCRIPT = "import json\nimport os\nimport random\nimport sys\nimport threading\nimport time\nfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\nfrom urllib.parse import unquote\n\nPORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8025\nOUT_FILE = sys.argv[2] if len(sys.argv) > 2 else \"/tmp/humanish-comms/deliveries.ndjson\"\nSERVED_DIR = sys.argv[3] if len(sys.argv) > 3 else (os.path.dirname(OUT_FILE) + \"/surface\")\nINBOX_PORT = int(sys.argv[4]) if len(sys.argv) > 4 else 0\n# Optional shared token guarding GET /deliveries (the drain read). Empty = unguarded, which is the\n# in-sandbox default: the capture listener binds loopback there, so nothing external can reach it.\n# An ADOPTER-HOSTED catch is reachable over the network, so it should pass one.\nDELIVERIES_TOKEN = sys.argv[5] if len(sys.argv) > 5 else \"\"\n# Optional loopback SMTP listener. Most self-hostable apps send mail over SMTP rather than an HTTP\n# provider API, so without this the catch only works for the minority that speak HTTP.\nSMTP_PORT = int(sys.argv[6]) if len(sys.argv) > 6 else 0\ntry:\n os.makedirs(os.path.dirname(OUT_FILE), exist_ok=True)\nexcept Exception:\n pass\nMAX_BODY = 5 * 1024 * 1024\nCSP = \"default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'; frame-src 'none'; img-src * data:; style-src 'unsafe-inline'; font-src * data:\"\n\n\ndef message_id():\n return \"humanish-catch-\" + format(int(time.time() * 1000), \"x\") + format(random.randrange(16 ** 8), \"08x\")\n\n\nclass BaseHandler(BaseHTTPRequestHandler):\n def log_message(self, *args):\n return\n\n def _json(self, status, obj, extra_headers=None):\n payload = json.dumps(obj).encode(\"utf-8\")\n self.send_response(status)\n self.send_header(\"content-type\", \"application/json; charset=utf-8\")\n for key, value in (extra_headers or {}).items():\n self.send_header(key, value)\n self.end_headers()\n self.wfile.write(payload)\n\n def do_GET(self):\n path = self.path.split(\"?\")[0]\n if path == \"/health\":\n self._json(200, {\"ok\": True, \"service\": \"humanish-comms-catch\", \"capabilities\": [\"recipient-inbox-v1\", \"captured-inline-images-v1\"]})\n return\n if path == \"/\":\n # A persona that trims the /inbox path lands here. It used to get the health JSON and read\n # it as \"wrong place / broken\", so send it where it meant to go. /health keeps the machine\n # marker: both readiness probes assert on /health specifically, never on /.\n self.send_response(200)\n self.send_header(\"content-type\", \"text/html; charset=utf-8\")\n self.send_header(\"content-security-policy\", CSP)\n self.end_headers()\n self.wfile.write(b\"Mailbox

Open the inbox

\")\n return\n if path == \"/deliveries\":\n # The drain read. In-sandbox humanish reads the NDJSON file directly; an adopter-hosted\n # catch is on another machine, so the same bytes are served over HTTP. Capture bodies\n # can contain a verification link, so this is the one route worth guarding.\n if DELIVERIES_TOKEN:\n supplied = self.headers.get(\"authorization\", \"\")\n if supplied != (\"Bearer \" + DELIVERIES_TOKEN):\n self._json(401, {\"error\": \"unauthorized\"})\n return\n try:\n with open(OUT_FILE, \"rb\") as handle:\n body = handle.read()\n except Exception:\n body = b\"\"\n self.send_response(200)\n self.send_header(\"content-type\", \"application/x-ndjson; charset=utf-8\")\n self.send_header(\"cache-control\", \"no-store\")\n self.end_headers()\n self.wfile.write(body)\n return\n if path == \"/inbox\" or path.startswith(\"/inbox/\") or path == \"/api/inbox\" or path.startswith(\"/api/inbox/\"):\n rel = unquote(path)\n if \"..\" in rel or chr(0) in rel:\n self.send_response(400)\n self.end_headers()\n return\n data = None\n for candidate in (SERVED_DIR + rel, SERVED_DIR + rel + \"/index\"):\n try:\n with open(candidate, \"rb\") as handle:\n data = handle.read()\n break\n except Exception:\n data = None\n if data is None:\n import re\n if re.fullmatch(r\"/(api/)?inbox/for/[a-f0-9]{64}/?\", rel):\n if rel.startswith(\"/api/\"):\n self._json(200, [])\n return\n self.send_response(200)\n self.send_header(\"content-type\", \"text/html; charset=utf-8\")\n self.send_header(\"content-security-policy\", CSP)\n self.send_header(\"cache-control\", \"no-store\")\n self.end_headers()\n self.wfile.write(b\"Your inbox

Your inbox

No messages yet.

\")\n return\n # A JSON route answers in JSON; only the HTML route answers in HTML.\n if rel.startswith(\"/api/\"):\n self._json(404, {\"error\": \"message not found\"})\n return\n self.send_response(404)\n self.send_header(\"content-type\", \"text/html; charset=utf-8\")\n self.send_header(\"content-security-policy\", CSP)\n self.end_headers()\n scope = re.match(r\"^/inbox/for/[a-f0-9]{64}(?:/|$)\", rel)\n back = scope.group(0).rstrip(\"/\") if scope else (None if rel.startswith(\"/inbox/for\") else \"/inbox\")\n body = \"Mailbox

message not found

\"\n if back:\n body += \"

Back to the inbox

\"\n self.wfile.write(body.encode(\"utf-8\"))\n return\n is_api = rel.startswith(\"/api/\")\n self.send_response(200)\n self.send_header(\"cache-control\", \"no-store\")\n self.send_header(\"referrer-policy\", \"no-referrer\")\n self.send_header(\"x-content-type-options\", \"nosniff\")\n self.send_header(\"content-type\", \"application/json; charset=utf-8\" if is_api else \"text/html; charset=utf-8\")\n if not is_api:\n self.send_header(\"content-security-policy\", CSP)\n self.end_headers()\n self.wfile.write(data)\n return\n self._json(404, {\"error\": \"not found\"})\n\n\nclass CaptureHandler(BaseHandler):\n def do_POST(self):\n path = self.path.split(\"?\")[0]\n try:\n length = int(self.headers.get(\"content-length\") or 0)\n except Exception:\n length = 0\n if length > MAX_BODY:\n self.send_response(413)\n self.end_headers()\n return\n body = self.rfile.read(length).decode(\"utf-8\", \"replace\") if length > 0 else \"\"\n try:\n with open(OUT_FILE, \"a\", encoding=\"utf-8\") as handle:\n print(json.dumps({\"t\": int(time.time() * 1000), \"path\": path, \"body\": body}), file=handle)\n except Exception:\n pass\n mid = message_id()\n if path == \"/v3/mail/send\":\n self.send_response(202)\n self.send_header(\"x-message-id\", mid)\n self.end_headers()\n elif path.endswith(\"/batch\"):\n self._json(200, {\"data\": [{\"id\": mid}]})\n else:\n self._json(200, {\"id\": mid})\n\n\nclass ReadOnlyHandler(BaseHandler):\n def do_GET(self):\n if self.path.split(\"?\")[0] == \"/deliveries\":\n self._json(404, {\"error\": \"not found\"})\n return\n super().do_GET()\n\n def do_POST(self):\n self.send_response(405)\n self.end_headers()\n\n\n# Optional read-only inbox listener on 0.0.0.0 (getHost-reachable from a DIFFERENT sandbox on the\n# shared-world route). Serves GET /inbox + /api/inbox + /health only; POST capture stays on the\n# 127.0.0.1 listener so nothing on the internet can inject a fake captured send. Started only when a\n# distinct inbox port is provided (the CUA same-sandbox route omits it and stays loopback-only).\nif INBOX_PORT and INBOX_PORT != PORT:\n threading.Thread(target=lambda: ThreadingHTTPServer((\"0.0.0.0\", INBOX_PORT), ReadOnlyHandler).serve_forever(), daemon=True).start()\n\n\n# Minimal SMTP capture listener. Most self-hostable apps send mail through SMTP, not an HTTP provider\n# API, so an HTTP-only catch could not study them at all. Plain sockets and the stdlib email parser:\n# python 3.12 removed smtpd, and a co-located catcher must not need a dependency.\n#\n# It normalizes each message into the SAME NDJSON line an HTTP send produces, on the /emails path, so\n# every host-side profile, the inbox surface, and the drain work unchanged \u2014 SMTP is a transport\n# here, not a second pipeline.\n# Built from chr() rather than a backslash escape: this script lives inside a TS template\n# literal, where JS would consume the escape before python ever saw it.\nCRLF = chr(13) + chr(10)\n\n\ndef smtp_reply(conn, text):\n conn.sendall((text + CRLF).encode(\"utf-8\"))\n\n\ndef smtp_session(conn):\n import email\n from email import policy\n\n reader = conn.makefile(\"rb\")\n smtp_reply(conn, \"220 humanish-comms-catch\")\n sender = \"\"\n rcpts = []\n while True:\n line = reader.readline()\n if not line:\n break\n command = line.decode(\"utf-8\", \"replace\").strip()\n upper = command.upper()\n if upper.startswith(\"EHLO\") or upper.startswith(\"HELO\"):\n # AUTH is advertised and then accepted unconditionally: the app under test holds\n # whatever credentials its config carries, and this listener is loopback-only.\n smtp_reply(conn, \"250-humanish-comms-catch\")\n smtp_reply(conn, \"250 AUTH PLAIN LOGIN\")\n elif upper.startswith(\"AUTH\"):\n smtp_reply(conn, \"235 2.7.0 accepted\")\n elif upper.startswith(\"MAIL FROM\"):\n sender = command[command.find(\":\") + 1 :].strip().strip(\"<>\")\n smtp_reply(conn, \"250 2.1.0 ok\")\n elif upper.startswith(\"RCPT TO\"):\n rcpts.append(command[command.find(\":\") + 1 :].strip().strip(\"<>\"))\n smtp_reply(conn, \"250 2.1.5 ok\")\n elif upper == \"DATA\":\n smtp_reply(conn, \"354 end with .\")\n raw = b\"\"\n while True:\n chunk = reader.readline()\n if not chunk or chunk.strip() == b\".\":\n break\n # Undo dot-stuffing (RFC 5321): a leading '.' on a body line is doubled on the wire.\n if chunk.startswith(b\"..\"):\n chunk = chunk[1:]\n raw += chunk\n if len(raw) > MAX_BODY:\n break\n try:\n parsed = email.message_from_bytes(raw, policy=policy.default)\n subject = str(parsed.get(\"subject\") or \"\")\n html_part = parsed.get_body(preferencelist=(\"html\", \"plain\"))\n body = html_part.get_content() if html_part is not None else \"\"\n import base64\n inline_images = []\n image_bytes = 0\n for part in parsed.walk():\n cid = str(part.get(\"Content-ID\") or \"\").strip().strip(\"<>\")\n content_type = part.get_content_type()\n if not cid or len(cid) > 256 or content_type not in (\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\"):\n continue\n payload = part.get_payload(decode=True) or b\"\"\n if not payload or len(payload) > 1024 * 1024:\n continue\n image_bytes += len(payload)\n if len(inline_images) >= 12 or image_bytes > 2 * 1024 * 1024:\n break\n inline_images.append({\"contentId\": cid, \"contentType\": content_type, \"base64\": base64.b64encode(payload).decode(\"ascii\")})\n except Exception:\n subject = \"\"\n body = raw.decode(\"utf-8\", \"replace\")\n inline_images = []\n record = {\n \"t\": int(time.time() * 1000),\n \"path\": \"/emails\",\n \"body\": json.dumps({\"from\": sender, \"to\": rcpts, \"subject\": subject, \"html\": body, \"inlineImages\": inline_images})\n }\n # Same append convention as the HTTP capture path: one line, opened in append mode.\n with open(OUT_FILE, \"a\", encoding=\"utf-8\") as handle:\n print(json.dumps(record), file=handle)\n sender = \"\"\n rcpts = []\n smtp_reply(conn, \"250 2.0.0 queued\")\n elif upper == \"RSET\":\n sender = \"\"\n rcpts = []\n smtp_reply(conn, \"250 2.0.0 ok\")\n elif upper == \"QUIT\":\n smtp_reply(conn, \"221 2.0.0 bye\")\n break\n elif upper == \"NOOP\":\n smtp_reply(conn, \"250 2.0.0 ok\")\n else:\n smtp_reply(conn, \"250 2.0.0 ok\")\n try:\n conn.close()\n except Exception:\n pass\n\n\ndef smtp_serve(port):\n import socket\n\n server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n server.bind((\"127.0.0.1\", port))\n server.listen(16)\n while True:\n conn, _ = server.accept()\n threading.Thread(target=smtp_session, args=(conn,), daemon=True).start()\n\n\nif SMTP_PORT:\n threading.Thread(target=lambda: smtp_serve(SMTP_PORT), daemon=True).start()\n\nThreadingHTTPServer((\"127.0.0.1\", PORT), CaptureHandler).serve_forever()\n"; export interface DeployCommsCatchOptions { /** Fixed loopback port the catch listens on (default 8025). Must be free inside the sandbox. */ port?: number; /** Optional SECOND fixed port for a READ-ONLY inbox listener bound to 0.0.0.0, so a persona in a * DIFFERENT sandbox can reach the inbox surface via getHost (the shared-world route). Omit on the * CUA same-sandbox route (loopback is enough). Must differ from `port` and be free in the sandbox. */ inboxPort?: number; /** In-sandbox working dir for the script + NDJSON (default /tmp/humanish-comms). */ dir?: string; /** Detached-process name ([a-z0-9-]); default "comms-catch". */ name?: string; /** Optional loopback SMTP port. Most self-hostable apps send mail over SMTP rather than a * provider's HTTP API, and an HTTP-only catch cannot study those at all. Captured messages are * normalized onto the same NDJSON the HTTP path writes, so nothing downstream changes. */ smtpPort?: number; /** Readiness-probe budget (ms) for the catch's /health (default 15000). */ readyTimeoutMs?: number; requestTimeoutMs?: number; timers?: DetachedTimers; } export interface DeployedCommsCatch { port: number; /** Inject THIS as the app's email-API base URL (e.g. RESEND_API_URL) — the sandbox's own loopback. */ baseUrl: string; deliveriesPath: string; /** In-sandbox dir the HOST renders the persona-facing inbox-surface files into (via writeInboxSurface); * the catch serves them at /inbox and /api/inbox. */ surfaceDir: string; /** The 0.0.0.0 read-only inbox port, when one was requested — getHost-expose THIS to give a * different-sandbox persona a reachable inbox URL. Absent on the loopback-only (CUA) route. */ inboxPort?: number; /** The loopback SMTP port, when one was requested. Point the app's SMTP host/port env at * 127.0.0.1 and THIS. */ smtpPort?: number; /** Whether the catch's /health returned OUR service marker within the readiness budget. Callers MUST * treat `ready === false` as fatal (do not inject baseUrl into a dead catch — the app's sends would * silently fail with nothing captured). */ ready: boolean; } /** A raw send the in-sandbox catch captured (host-side parsing happens in routeCapturedSends). */ export interface RawCapturedSend { path: string; body: string; t: number; } /** * Write + launch the in-sandbox catch (detached), then probe it ready. Call AFTER the subject sandbox * is created and BEFORE the subject app's serve.start, so the base URL resolves at the app's boot. */ export declare function deployCommsCatch(desktop: E2BDesktopSandbox, options?: DeployCommsCatchOptions): Promise; /** * Drain new captured sends from the in-sandbox NDJSON since `cursor` (a line count). Returns the fresh * sends and the new cursor. Cheap `cat` over commands.run; NDJSON is small for a run. */ export declare function drainCommsCatch(desktop: E2BDesktopSandbox, deployed: Pick, cursor?: number, requestTimeoutMs?: number): Promise<{ sends: RawCapturedSend[]; cursor: number; }>; /** * Parse an append-only deliveries NDJSON blob into raw sends. Split out of drainCommsCatch (#380) so * the SAME parsing serves a sandbox we own (read over the E2B command channel) and a catch running on * a plane we do not own (read from the local filesystem by `humanish comms catch`). * * A file that does not end in a newline may have a PARTIAL last line — a reader racing an append of a * large body. Dropping it is never lossy: the script only ever emits valid JSON lines, so an incomplete * line re-reads complete on the next pass. */ export declare function parseDeliveriesNdjson(text: string): RawCapturedSend[]; /** * The distinct `to` addresses the captured mail was actually sent to, parsed with the SAME profiles * that route it. A lab run knows its recipients from the declared roster; a standalone catch does not, * so it discovers them from the mail itself — otherwise an operator who forgot to name an address gets * a technically-healthy catch rendering an empty inbox forever, which is the false-green class #380 is * about. */ export declare function capturedRecipientAddresses(sends: readonly RawCapturedSend[], profiles?: EmailSendProfile[]): string[]; /** * Route raw sends into a FRESH FakeInbox and return the deduped, delivery-ordered messages. Split out * of refreshInboxSurface (#380) so the rendering pipeline is shared by every transport; the freshness * is what makes a full rebuild idempotent (a send is never routed twice, so no duplicate emails). */ export declare function inboxMessagesFrom(sends: readonly RawCapturedSend[], recipients: readonly InboxSurfaceRecipient[]): Promise; /** * Parse drained raw sends with the host profiles and route them into the CommsChannel (the host-side * FakeInbox). Returns the number of inbox deliveries made. Same profiles as the host catch, so the * in-sandbox and in-process routes normalize identically. */ export declare function routeCapturedSends(sends: RawCapturedSend[], channel: CommsChannel, profiles?: EmailSendProfile[]): Promise; /** Outcome of a whole-run comms collect. `artifact` is present only when captured mail matched a * provisioned inbox; `captured > 0 && artifact === undefined` means the app sent mail to an address * no declared recipient covers (captured but unevidenced) — the caller should surface that, not drop * it silently. */ export interface CommsThreadCollection { artifact?: CommsThreadArtifact; /** Raw sends the in-sandbox catch captured this run (all POSTed paths). */ captured: number; /** Distinct messages that matched a provisioned recipient inbox (drives whether an artifact exists). */ matched: number; } /** * End of a run's comms funnel: drain everything the in-sandbox catch captured, route it into the * host `channel`, poll the provisioned `inboxes`, and build the digest-only thread artifact. The * `artifact` is omitted when nothing was captured OR nothing matched a provisioned inbox (an empty * file would be a false claim of a delivered thread) — but `captured`/`matched` are always reported so * the caller can warn on captured-but-unevidenced mail rather than lose it silently. Composes the * tested drain/route/build pieces so the CUA and shared-world routes collect evidence identically. The * full NDJSON is drained from cursor 0 (a whole-run collect), so it is idempotent to call once at * teardown. */ export declare function collectCommsThread(args: { desktop: E2BDesktopSandbox; deployed: Pick; channel: CommsChannel; /** The inboxes provisioned for this run (declared recipients). Only mail to these is evidenced. */ inboxes: CommsAddress[]; profiles?: EmailSendProfile[]; requestTimeoutMs?: number; }): Promise; /** An adopter-hosted catch: humanish never provisioned it, so it is addressed over HTTP (#328). */ export interface ExternalCommsCatch { /** Base URL of the catch the ADOPTER runs (its POST capture endpoint and GET /deliveries). */ catchBaseUrl: string; /** Base URL the persona opens to read mail. Defaults to catchBaseUrl (same server serves /inbox). */ inboxBaseUrl?: string; /** Bearer token for the drain read, when the adopter guarded it. Value is used, never persisted. */ authToken?: string; } /** The URL a persona is told to open to read its mail on an adopter-hosted plane. */ export declare function externalInboxUrl(external: ExternalCommsCatch): string; /** * Probe an adopter-hosted catch the way the in-sandbox one is probed: assert OUR service marker in * /health, not merely any 2xx — an adopter's reverse proxy or a captive portal will happily return * 200 for anything, and a comms lab whose catch is not actually there collects nothing while * looking fine. Fail-closed callers treat `false` as a hard stop before spending on a run. */ export declare function externalCatchHealthy(external: ExternalCommsCatch, options?: { timeoutMs?: number; fetchFn?: typeof fetch; }): Promise; /** * Drain an adopter-hosted catch over HTTP. Same NDJSON contract and same partial-line discipline as * the in-sandbox `cat` drain: a body that does not end in a newline may have a torn final append, so * that line is dropped rather than parsed into a half-message. */ export declare function drainExternalCommsCatch(external: ExternalCommsCatch, cursor?: number, options?: { timeoutMs?: number; fetchFn?: typeof fetch; }): Promise<{ sends: RawCapturedSend[]; cursor: number; }>; /** * The adopter-hosted analogue of collectCommsThread: drain over HTTP, route into the host inbox bus, * and build the SAME digest-only humanish.comms-thread.v1 artifact. Evidence shape does not depend * on who hosted the catch — only the transport does. */ export declare function collectExternalCommsThread(args: { external: ExternalCommsCatch; channel: CommsChannel; inboxes: CommsAddress[]; profiles?: EmailSendProfile[]; timeoutMs?: number; fetchFn?: typeof fetch; }): Promise; /** * Render the persona-facing inbox surface (host-side, typed — see comms-inbox.ts) and write the files * into the sandbox's served dir, so the catch serves a LIVE inbox the persona opens and clicks. Creates * the nested route dirs first; overwrites idempotently, so call it whenever the message set changes * (e.g. after a mid-run drain). Returns the number of files written. Raw content is written INTO the * sandbox only (runtime-only, served to the in-sandbox browser); nothing here persists to the bundle. */ export declare function writeInboxSurface(desktop: E2BDesktopSandbox, surfaceDir: string, messages: CommsMessage[], options?: InboxRenderOptions & { requestTimeoutMs?: number; }): Promise; /** A declared inbox recipient the surface renders for (lane + the literal address the app sends to). */ export interface InboxSurfaceRecipient { lane: string; address: string; } /** * One mid-run inbox-surface refresh cycle: FULL rebuild from the append-only NDJSON (drain from cursor 0) * into a FRESH FakeInbox each call, provisioning the declared `recipients`, then (re)render the surface * so the persona sees new mail while the session is live. Returns the total captured-send `count` + whether * it rendered. * * The full rebuild is deliberate — it is IDEMPOTENT and RETRY-SAFE: a transient writeInboxSurface failure * PROPAGATES (the caller retries next tick without advancing its `sinceCount`), and because each rebuild * starts from a clean channel, a send is never routed twice, so the persona never sees duplicate emails. * Pass `sinceCount` (the last SUCCESSFULLY-rendered send count) to skip the (N-file) render when nothing * new has arrived. This is independent of the teardown collectCommsThread drain (its own fresh channel, * also cursor 0) — no evidence is lost or altered. The NDJSON is small for a run, so re-reading it is cheap. */ export declare function refreshInboxSurface(args: { desktop: E2BDesktopSandbox; deployed: Pick; recipients: InboxSurfaceRecipient[]; sinceCount?: number; originMap?: InboxRenderOptions["originMap"]; requestTimeoutMs?: number; }): Promise<{ count: number; rendered: boolean; }>;