# Ego Chat technical reference
The full technical reference. For a short, plain-English overview, see [README.md](README.md).
Ego Chat is a local durable broker for Codex-, ZCode-, or Claude Code-to-ChatGPT collaboration through Ego Lite. It is not a second chat UI. A thin MCP process connects the coding agent, the broker owns each browser handoff independently of that client connection, and a fixed Ego Browser driver operates one persistent ChatGPT conversation.
The supported surfaces are a one-shot advisory handoff, durable broker-owned Codex convergence, and a current-host fallback loop:
```text
Codex, ZCode, or Claude Code
-> stdio MCP facade
-> authenticated local Unix socket
-> durable single-user broker
-> fixed Ego Browser driver
-> one named ChatGPT conversation
-> captured review returned to the same Codex turn
or
one start request
-> durable broker
-> dedicated Codex App Server thread (A)
-> persistent ChatGPT web conversation in Ego (B)
-> A/B review cycles
-> continue through recoverable faults until explicit settlement
or
current Codex, ZCode, or Claude Code task/Goal (A)
-> schema-constrained candidate
-> one-candidate Ego Chat review call
-> persistent ChatGPT web conversation (B)
-> validated review returned to the same ZCode task
-> current host submits the next cycle while it remains alive
```
The broker persists a named conversation lease. `create_once` starts from a verified blank ChatGPT page and promotes the lease only after the first confirmed send exposes a canonical conversation URL. `existing` opens and verifies a supplied canonical URL. Later calls need only the binding key; the broker resolves the task space, tab, URL, and expected conversation head.
## Implemented capabilities
- Checkpointed JSONL workflow and binding ledger that avoids rewriting the full state on every transition, compacts after 5,000 events or 8 MiB, and reduces completed convergence cycles to bounded identity/recovery metadata before storing the next candidate.
- Content-addressed private result blobs, 30-day raw-body retention, a 256 MiB default blob quota, and retained metadata for the latest 500 ordinary terminal workflows. Running, `human_required`, and still-reconcilable failed browser workflows retain their bodies or a full-size result reservation.
- One canonical broker identity per real data-directory path, a stable socket independent of `TMPDIR`, an exclusive broker lease, and a monotonic fencing epoch. The canonical broker also reserves known v0.1 socket aliases so a late stale Codex, ZCode, or Claude Code facade cannot launch a second daemon.
- Private local state directory, files, token, runtime directory, and Unix socket.
- A private 4 MiB / 16-file driver mailbox with a 512 KiB per-input ceiling, five-minute inactive crash retention, strict lstat ownership/mode/link checks, live-child preservation, and child-owned unlink immediately after a successful read and before browser interaction.
- A per-run driver fence beside the mailbox: the driver records its stage before every browser mutation and refuses the mutation unless its run's fence still exists, and the adapter revokes that fence before it gives up on a run, so a driver still running inside Ego Lite after a timeout, abort, or shutdown can never act again.
- Authenticated IPC and an independently restartable stdio MCP facade.
- A runtime-contract digest that allows stale facades to inspect status but returns `restart_required` before they mutate durable state.
- Read-only adoption of a supplied private ChatGPT conversation URL, including a broker-owned wait for an already-running response and same-turn return to the invoking coding agent.
- Explicit Token-Saver waits that keep one durable MCP call open, suppress periodic progress notifications, minify the returned text envelope, and direct the bundled skill not to poll from extra model turns.
- One normal `ego_exchange_and_wait` MCP call that returns a long ChatGPT review into the same agent turn.
- One `ego_review_candidate_and_wait` call for a current-host-owned candidate, with exact target/candidate/cycle binding, objective settlement validation, ordinary-prose review support, and local continuation when a legacy response is structurally invalid.
- Detached `ego_start_exchange`, `await_workflow`, `workflow_status`, and `cancel_workflow` operations for recovery, plus an explicit acknowledged recovery-abandonment tool that preserves the at-most-once operation tombstone.
- A staged browser lifecycle that durably records `send_confirmed`, performs response capture read-only, and resumes that capture after a facade or broker restart without resending.
- One broker-wide FIFO browser lane across every binding and supported host. Confirmed-send response capture uses at most 15-second read-only slices, yields between slices, and revalidates the canonical conversation, prior head, prompt message ID, and unique marker before continuing; it never repeats Send.
- A two-hour default broker-owned ChatGPT generation budget, an eight-hour caller-attachment ceiling, and host MCP caps five minutes longer than that attachment while browser-workflow ownership remains durable.
- Compact Token-Saver text summaries plus digest-bound `ego_read_result` ranges for responses larger than 16 KiB.
- Persistent `ego-chat-main` conversation binding with exact canonical-URL verification.
- A durable `strongest_available` / `maximum_available` ChatGPT web policy, repaired and read back during adoption and enforced again immediately before every send.
- Stable conversation-head fingerprints over message IDs, roles, and content hashes before and after each bound send.
- One bounded rich-editor input transaction for large review packets, exact composer-digest verification immediately before each click, unique outbound markers, empty-draft checks, exact send-control checks, and no blind retry after an ambiguous delivery.
- Composer-surface detection of ChatGPT's conversation-length wall, usage limits and closed conversations: an exchange stops before any Send when ChatGPT already shows one, capture attributes one that appears after the Send to that turn, and a convergence rolls over on a wall either way. Long convergences also rotate to a successor proactively, and every binding and exchange result reports its `conversationSize`.
- Evidence-only reconciliation for a first confirmed send that exposes its canonical URL late, or for one exact tail-anchored user/assistant pair that completed after capture; reconciliation never clicks Send, stores the response by digest, and completes the original workflow for exact retry. An unbound create-once exchange interrupted before Send confirmation is instead proven absent account-wide before it is closed as delivery absent, at once when its run fence was revoked and otherwise by a newer broker generation. An exchange stopped before any Send (`PRE_SEND_PROVEN_STOP_CODES`) is closed as delivery absent after the same read-only proofs, except a convergence review that stopped at a full chat while its convergence's continuation checkpoint still names it (`continuation_checkpoint_source`): resume or cancel that convergence instead.
- Codex App Server spikes for broker-owned thread start/resume and desktop active-writer isolation.
- Broker-owned `ego_start_convergence` and `ego_converge_until_settled` workflows that alternate Codex and ChatGPT without human copy/paste.
- Immutable target and acceptance-contract digests, validated implementing-agent candidates, exact cycle identity, and objective settlement checks.
- A bounded durable task/runner core with immutable acceptance-and-evidence settlement contracts, exact-revision artifacts and evidence, completion-bound fenced verification leases, approval invalidation, and an effect ledger exercised only through fake adapters in this slice.
- Exclusive conversation leases across every cycle, an optional caller-selected cycle budget, caller-attachment deadlines, liveness guidance for repeated state, secret scanning of exact outbound review bytes, and terminal-state compare-and-set protection.
- ChatGPT feedback injected into the next Codex turn as explicitly untrusted App Server context.
- Native Rust setup, conflict-safe MCP configuration, and the same host-aware skill for Codex, ZCode, and Claude Code.
The delivery claim is deliberately limited: automatic sends are fail-closed and effectively at-most-once. Ego Chat does not claim exactly-once delivery across a browser UI and a remote service.
### Overnight reliability and continuation
The current reliability work preserves one logical task across recoverable interruptions; it does not promise an infinitely long ChatGPT conversation or unattended recovery from every failure.
- Local turns record a launch intent before `turn/start`. A lost acknowledgement is reconciled against the exact thread boundary and uniquely marked input. Missing or conflicting receipts preserve an ambiguous checkpoint rather than launching the work again.
- Read-only adoption cancellation is committed before aborting its runner, preventing a cancelled task from retaining a false active-binding reservation and blocking later upgrades.
- The pre-Send state is read as one atomic snapshot per decision (composer text, send control and hit test, generation state, policy pill, page locator) instead of separate page calls that can tear between reads, and the snapshot verified with the composed prompt is re-read immediately before the click: a changed composer digest, locator, generation state or send-enabled state stops the run as `pre_send_state_changed` with the draft cleared and is retried without a resend, never clicked. Policy-menu waits are bounded settles on the observable condition rather than fixed sleeps, and every result carries the run's own `driverStats` call counts and timings.
- A timed-out driver is fenced per run and cannot act afterwards. `ego-browser nodejs` runs the driver inside Ego Lite's service, so killing the local client does not stop the script, and its output only arrives when it ends. Before each run the adapter creates a private fence file; the driver records its stage at every mutation boundary, before the fence check that admits the mutation. Whenever the adapter stops waiting for a run (a local timeout, an abort, an output overflow, a process error, or shutdown), it revokes the fence before it kills the client. On a timeout it reads the stage record after revoking and attaches it to `ego_driver_timeout` as `runFenceRevoked`, `driverStage` and `sendClickStarted`, so the record names the last stage any of the run's mutations can have reached; a missing or malformed record leaves only `runFenceRevoked`. A fenced timeout whose record stops before the Send click is proven pre-Send and retried automatically: no later mutation can happen, and a composed prompt it left in the composer is this workflow's own exact draft, which the next attempt clears before composing. The retries are bounded, because each costs a full send deadline: the pre-Send recovery record carries a `timeoutStreak` of consecutive driver timeouts (any other retried stop starts it again), and the timeout that reaches the broker's `driverTimeoutRetryLimit` (default 3) stops the exchange as `browser_operation_interrupted_before_send_confirmation` with its fenced evidence. For a bound conversation, reconciliation can then prove delivery absent at once from that evidence; an unbound create-once exchange is proven absent account-wide instead, and because its run fence was revoked, that proof also needs no broker restart (see below). A record at or after the click, or no readable record, keeps the ambiguous `browser_operation_interrupted_before_send_confirmation` stop for read-only reconciliation. Response-capture timeouts are unchanged.
- ChatGPT keeps one unsent new-chat draft per browser profile and shows it on every create-once start page. A composer draft that is exactly this workflow's marked prompt is cleared before composing; any other draft is never cleared, and one that stays unchanged for five minutes stops the run as `unexpected_chatgpt_draft_persistent` with nothing sent, instead of being retried indefinitely. The driver reads ChatGPT's real composer, the visible rich editor, not the hidden, always-empty fallback textarea that precedes it. Only the exchange stops on a foreign draft, at selection and before touching the policy menu; every other mode reports the draft and proceeds, a restart clears its own exact prompt only after proving its marker absent from the conversation, and preflight's `unexpectedDraft` and `browserContract.safe` reflect real drafts.
- Every other retryable pre-Send stop is bounded by one clock for the whole exchange. The first pre-Send recovery records `preSendRetryStartedAt` on the workflow, and that time is carried across every retried reason and across broker restarts. Once the broker's `preSendRetryBudgetMs` (default 90 minutes, longer than a long answer someone started in a bound chat, which can keep it generating for 20 to 40 minutes) has passed, the next retryable stop ends the exchange as `human_required` with `pre_send_retry_budget_exhausted` and evidence `{ attempts, lastReason, since }`: nothing was sent, and a fresh exchange with a new marker may be started once the page is healthy. The stricter bounds keep precedence: the five-minute unchanged-draft stop and the fenced driver-timeout streak of three.
- Three invalid candidates in one cycle produce a strategy-change checkpoint for review, followed by fresh local-thread recovery. Already-captured reviews and settlement do not require an unnecessary local-client connection.
- Attributable, repeated latest-turn provider UI signals distinguish stopped thinking, conversation exhaustion, provider error, and quota; ChatGPT's `Thinking failed` banner, shown in an assistant turn with no message, is classified as a provider error. These pause the exact sent turn without committing an error label as an assistant answer. Quoted text, old/global labels, active generation, and conflicting observations do not establish that evidence. The current selectors have synthetic regression coverage, **not live ChatGPT markup qualification**; unsupported markup remains unknown.
- ChatGPT shows its conversation-length wall ("You've reached the maximum length for this conversation, but you can keep talking by starting a new chat."), its usage limits and closed conversations on the composer surface, `div#thread-bottom`, not as reply text, so the latest-turn signals above cannot see them. The driver reads only that container, never the editor, the "ChatGPT can make mistakes" disclaimer, message content, code, quotes or hidden nodes, and keeps bounded evidence: at most four 160-character banner labels, eight 80-character call-to-action labels, and whether the editor accepts input. Three signals are classified, in this order: a banner label that is exactly a wall sentence (2026, 2023 or the latest-turn wording) is `conversation_exhausted`; exactly a known usage-limit sentence beside an editor that refuses input is `quota_limited` (beside a usable editor it is advisory, because ChatGPT then answers with another model, as when a model allowance such as the weekly GPT-6 Pro limit runs out); an editor that refuses input beside a *New chat* or *Start new chat* control, with no wall sentence, is `conversation_closed`, which also covers ChatGPT's safety banner. *Continue in a new chat* beside a usable editor is advisory only (`continuationOffered`): ChatGPT offers it during context truncation, long before the wall. A page without `#thread-bottom` classifies nothing; there is no fallback to the whole page.
- Before Send, an exchange reads the composer surface right after selecting the conversation, before the model-policy menu or any composition. A state that a second read one second later confirms stops it with nothing composed or sent, as `chatgpt_conversation_exhausted_before_send`, `chatgpt_quota_limited_before_send` or `chatgpt_conversation_closed_before_send`, with the bounded evidence and a `composer_surface` provider terminal. These stops are human-only: they are never retried, and a convergence review child that stops this way is never reconciled automatically. The broker exports them as `PRE_SEND_PROVEN_STOP_CODES`. A wall that first appears between the Send click and its confirmation stays with the ambiguous-send handling and reconciliation.
- After a confirmed Send, capture reports such a state as the turn's `providerTerminal` (`source: "composer_surface"`) only when the confirmed prompt is the last user message, no response ends with the terminal marker, nothing is generating, two reads a second apart agree, and the Send's own pre-Send read had not already seen that state; a Send recorded before this version has no such read and never qualifies. The exchange then pauses as `chatgpt_conversation_exhausted`, `chatgpt_quota_limited` or `chatgpt_conversation_closed`, a closed conversation remaining reconcilable like the other provider terminals. Pending captures put the banner labels ahead of the latest turn's in `statusLabels`, which the optional terminal judgment reads.
- Inactive response capture backs off from two to thirty seconds. Thirty minutes of repeated inactive/nonterminal observations produces a retained `capture_paused` checkpoint. Active generation does not trigger this timer. A pause never authorizes a Send or proves conversation exhaustion.
- A stopped convergence review preserves its private contract, candidate and evidence. Qualified checkpoints expose a compact `continuationCheckpoint` receipt. `ego_resume_convergence` (CLI: `ego-chat resume-convergence <input-json-file>`) can consume the exact child's subsequently reconciled response without another Send. A failed first response in a create-once chat also retains its exact read-only reconciliation path. A confirmed create-once send whose task space vanishes during capture recovers exactly like a bound conversation's vanished Space (recreated by name, its conversation reopened), and a confirmed send still left stuck on that identity evidence remains reconcilable: reconciliation recreates the vanished Space by name and reopens the confirmed conversation (no wait, since the workflow is already stopped) before reading its attributable response, without another Send.
Since Codex 0.153.4, a completed turn's own `turn/completed` notification may carry only `itemsView: "summary"` (final-answer items only, omitting `commandExecution` and other workspace-activity items); older Codex releases and every `thread/read` response carry `itemsView: "full"`. `AppServerClient` re-reads the thread when a completed turn's own notification is not already `"full"`, and unions in any activity item types already streamed via `item/completed` notifications while the turn ran, so a summarized notification and a slow or failed thread/read cannot together be mistaken for an idle Codex turn that never touched the workspace.
A convergence that keeps producing the same outcome — an unchanged candidate/review status, or repeated liveness checkpoints — stops instead of looping forever. Each completed cycle is fingerprinted from its candidate status, per-criterion statuses, review decision and per-criterion review statuses (never cycle numbers or free text); consecutive cycles with the same fingerprint, or any cycle carrying a liveness checkpoint, count toward a streak that resets the moment a cycle's fingerprint changes. Once the streak reaches the limit, the run ends as `human_required` with code `convergence_stagnant`, retaining the captured candidate, cycles and reviews unchanged — no resend, no successor. The limit defaults to 3 and is raised per convergence (2-50) with the `stagnationLimit` request/tool option; the durable `stagnation: { streak, limit, signature }` on the workflow exposes the current streak.
For unattended convergence, rollover to a same-project successor is automatic by default; pass `conversationContinuation: "manual"` at start to opt out. Existing workflows are not silently upgraded — each keeps whatever value it was started with. Only conversation exhaustion triggers preparation of one isolated same-project successor, one next-generation review of the retained candidate, and permanent-URL/head promotion after a valid captured answer: either an exhaustion attributed to the sent review, or a wall seen before the review's Send (`chatgpt_conversation_exhausted_before_send`). That review was never delivered, so its checkpoint records `confirmedSend: false` and a `composer_surface` provider terminal, and the successor receives the same review. A closed conversation or a usage limit, before or after the Send, pauses the convergence for a human with no allowed action. It uses the normal strongest-model/maximum-effort Send policy and the broker's existing serialized browser lane. The successor's first review carries a bounded, redacted summary of the previous conversation's review as untrusted context, and a proven pre-Send failure on that first review is retried once before pausing. A paused convergence's public record exposes a bounded `candidateSummary` (status, summary, per-criterion status, blocker count) alongside `continuationCheckpoint`; the review packet and per-criterion evidence remain private.
A long convergence also rotates before it meets the wall. At a cycle boundary, right before a review Send, a convergence whose active chat has recorded at least `rotation.maxMessages` messages hands that cycle's review to a same-project successor through the same machinery, with a `proactive_rotation` checkpoint (no review child, no provider terminal, `confirmedSend: false`), and records why in `lastRotation` (`bindingKey`, `cycle`, `generation`, `messageCount`, `maxMessages`, `reason: "message_count_threshold"`). The `rotation` input is `{ "maxMessages": 40-1000 }` or `"off"`. Omitted, the broker records `{ "maxMessages": 120 }`, except on `ego-chat-main`, the long-lived main chat, where it records `"off"` so that chat stays one conversation; an explicit input still applies there. A convergence started before the input existed never rotates, because existing workflows are not silently upgraded. It never rotates under manual continuation, with rotation off, or once a review child exists for that cycle, since that child may already have been sent. A single exchange never rotates. Instead, every binding counts Ego Chat's own exchanges and the UTF-8 bytes of its prompts and captured responses inside the existing head-commit write, with no extra event or revision, and the public binding and every exchange result expose `conversationSize: { messageCount, egoChatExchanges, egoChatBytes, rotationRecommended }`. `rotationRecommended` is true from 120 recorded messages, or when the last capture saw ChatGPT offer to continue in a new chat, so a host can choose a new binding; it stays false on `ego-chat-main`. A binding recorded before the counters existed counts from zero.
The reservation, exact prompt identity, child operation and promotion are durable. Restart reattaches the same child rather than generating another Send. Pending-successor reservations exclude other tasks, survive recovery pauses and retention cleanup, and cannot be released by a racing duplicate resume. Cancellation revokes pending handoff authority before Send. A successor that fails before establishing a valid head stays reserved for exact recovery; it does not trigger an unbounded chain of empty chats.
In manual mode, changing chats requires **both** conversation-exhaustion evidence (attributed to the sent review, or a wall seen before its Send) and an explicit caller-selected, already-bound successor. The resume input pins the parent workflow, checkpoint digest, successor key, canonical URL and binding revision:
```json
{
"workflowId": "<paused parent UUID>",
"expectedCheckpointDigest": "<receipt SHA-256>",
"successor": {
"bindingKey": "<verified successor binding>",
"canonicalUrl": "https://chatgpt.com/g/<same-project>/c/<successor>",
"expectedBindingRevision": 1,
"acknowledgeConversationChange": true
}
}
```
Omit `successor` only to consume an already-reconciled normal response in the currently active bound chat. An exact resume replay rediscovers its existing receipt. A stale checkpoint or different selection fails closed. The original parent binding, old chat and old delivery records remain unchanged; an `activeChat` generation routes the same candidate to the successor. Generation history is private, crash-recoverable and bounded to 32 generations in v1. Cancellation, authentication, quota, a closed conversation, “Stopped thinking,” unknown inactivity, ambiguous local acceptance and one-time attachment authority cannot authorize rollover.
Continuation validation also retains explicit resource limits: 40,000 nodes, 64 nesting levels and a 64 MiB private snapshot. Serialized checkpoints allow 3.5 MiB, including worst-case JSON escaping of a valid 512 KiB review packet. These are fail-closed safety bounds, not an unlimited-history guarantee. Exact child responses remain protected from retention cleanup until the parent consumes or relinquishes its checkpoint.
Blank-successor preparation is a separate, explicit transaction. For an attributed exhaustion checkpoint, `ego_prepare_successor` (CLI: `ego-chat prepare-successor <input-json-file>`) accepts:
```json
{
"workflowId": "<paused parent UUID>",
"expectedCheckpointDigest": "<receipt SHA-256>",
"acknowledgeNewChat": true
}
```
Before touching the browser, it reserves one binding slot and a deterministic per-checkpoint Space name. It creates an empty same-project starting tab and atomically records an **unbound** `create_once` binding plus a `successorPreparation` receipt. This standalone transaction leaves the parent paused: it produces no prompt, model call, Send, canonical conversation URL, or generation advancement. A blank prepared binding cannot be passed straight to resume. An opted-in automatic runner uses the same preparation transaction internally and then performs the separately checkpointed first review.
After a lost acknowledgement, retry only these same arguments. A `dispatched` receipt can inspect the reserved Space's sole blank tab or complete navigation of its sole native New tab, never create a replacement Space or tab. A `prepared` replay returns the stored receipt if its binding is unchanged. Restart resumes a running opted-in handoff, not a manually paused one. Missing, ambiguous, nonempty, user-owned, or identity-changing Spaces fail closed. Preparation supports the observed native `chrome://newtab/` / `chrome://new-tab-page/` pair and ChatGPT's title-suffixed Project route by stable Project ID. Native blank preparation was verified on 2026-09-08 with zero Sends; that is not a live provider-exhaustion or overnight qualification. Browser-global interference from external controllers remains outside the broker's guarantee.
Automatic rollover belongs to the explicitly opted-in broker workflow. Ego Monitor follows that workflow and its pending successor; it cannot independently invoke preparation/resume, invent a replacement chat or act as a second browser controller. Its deterministic checks use no LLM tokens. See [Ego Monitor](docs/ego-monitor.md).
After `successor_recovery_required`, retain the exact checkpoint. If blank preparation lost its acknowledgement, finish `ego_prepare_successor` with the same arguments, then use `ego_resume_convergence` without `successor` to resume the already opted-in handoff. If the exact successor answer was already committed, that resume consumes it directly. If its child is stopped, first use eligible read-only reconciliation of that exact child. None of these paths replaces an unresolved successor or repeats an accepted Send.
`cancel_workflow` on a `continuation_paused` parent permanently revokes that checkpoint and prevents a late preparation result from committing. It does not delete any possibly created blank Space or undo or release the old child's delivery evidence. A valid resume consumes the per-checkpoint preparation receipt, allowing later exhaustion to reserve a distinct successor. Other stopped recovery workflows retain their existing explicit `abandon_workflow_recovery` boundary.
Before claiming nightly readiness, qualify real provider markup and run representative 8–12-hour fault-injected shadow/safe sessions covering disconnects, process death, live hangs, sleep/wake, notification failure, multiple chats, and successor-recovery crashes. Unit/integration tests are not live overnight proof, and a forced-asleep Mac cannot keep either client running.
Further work remains on recovery when the first successor itself exhausts before a valid head, menu-order-independent model capability ranking, an optional budgeted BYOK advisor, and the experimental task-spine/admission simplification. These are not part of the bounded rollover guarantee.
### Concurrent hosts and Ego Spaces
Codex.app, ZCode.app, and Claude Code share one authoritative Ego Chat broker and the same default `ego-chat-main` binding. They do not silently create separate conversations or task spaces. One binding remains an exclusive ordered conversation lease: if another host already owns it, a different fresh operation receives `conversation_busy` instead of interleaving a stale prompt. An exact retry with the same operation identity still rediscovers its existing workflow.
Independent host tasks use explicitly distinct binding keys, canonical ChatGPT conversations, and Ego task spaces. Each new binding, and each successfully recovered legacy binding, records the complete browser-observed task-space `name`/`taskId` tuple, while its numeric ID remains only a location hint. If an old binding has no stable task-space identity, Ego Chat derives a private recovery-space name from the canonical conversation URL, not from a reusable binding-key convention. That typed `name` selector constrains only the live name; Ego Lite's independently observed opaque `taskId` is retained rather than assumed equal to it. Explicit legacy strings remain compatible by resolving to one unique name-or-task-ID match, while a split match is ambiguous. Immediately after acquiring the serialized browser lane, every operation receives a strict versioned owner/deny guard assembled from all other persisted tuples, in-flight tuples, and unresolved stable selectors. A missing, malformed, duplicate, or internally conflicting guard stops before task-space discovery or mutation. Existing typed matches are selected by their observed numeric location so the legacy `useOrCreateTaskSpace(string)` API cannot reinterpret a name as an opaque task ID; creation also stops before mutation when such a decoy exists. The returned live tuple and actual browser-reported canonical URL are reserved synchronously before that lane is released and remain reserved across confirmed-Send capture, retries, durable blobs, events, and binding commits. Confirmed-Send and validated adoption-capture claims remain authoritative after `failed` or `human_required`, are rebuilt after broker restart, and are released only after exact transfer to the binding or explicit recovery abandonment. A distinct session therefore cannot collapse its chat into another session's workspace merely because both hosts use familiar binding-key names or Ego Lite later recycles a numeric ID. Ego Chat serializes all of its browser-driver children inside one broker even across those independent bindings, so Codex, ZCode, and Claude Code queue rather than racing Ego Lite's global automation channel. A long confirmed-send capture releases that lane every 15 seconds while ChatGPT keeps thinking remotely; the caller remains in one Token-Saver wait and no second Send is created.
Using another Ego Space manually does not change a durable Ego Chat binding. Do not take over, close, stop, or type into the exact bound Space or its ChatGPT conversation while a workflow is active. Automation started outside Ego Chat is not part of its broker lane, and separately configured broker data domains cannot provide a process-global Ego Lite automation mutex. Until Ego Lite provides per-task-space CDP channels, simultaneous unrelated agent automation can still interfere despite separate Spaces. Track the upstream limitation in [`citrolabs/ego-lite#213`](https://github.com/citrolabs/ego-lite/issues/213).
## Requirements
- macOS with Ego Lite installed and the expected ChatGPT account logged in.
- Node.js 24 or newer.
- Codex CLI 0.149.0 or a compatible Codex app installation for Codex setup and broker-owned Codex convergence.
- ZCode for a ZCode-owned implementation/review loop; Codex is optional on a ZCode-only installation.
- Claude Code 2.1.203 or newer for a Claude Code-owned implementation/review loop, which also covers the Claude.app Code tab; Codex is optional on a Claude-only installation.
Install the locked dependencies:
```sh
npm ci
```
## Portable Rust installation
The Rust binary is a distribution and launch wrapper around the qualified Node broker; it does not reimplement the browser protocol. This keeps one proven core while giving other Macs a stable command, embedded host skills, and automatic Codex, ZCode, or Claude Code MCP configuration.
The receiving Mac still needs Ego Lite with ChatGPT logged in, Node.js 24 or newer, npm, Rust 1.88 or newer for `cargo install`, and at least one supported coding client. From a local checkout, configure any of the clients:
```sh
cargo install --path /absolute/path/to/ego-chat --locked
ego-chat setup
ego-chat doctor
ego-chat setup-zcode
ego-chat doctor-zcode
ego-chat setup-claude
ego-chat doctor-claude
```
`ego-chat setup` performs these bounded local actions:
- materializes the embedded runtime under `~/Library/Application Support/Ego Chat/runtime/<version>`;
- runs `npm ci --omit=dev --ignore-scripts` inside that managed runtime;
- redirects only verified older managed daemon launchers to the new runtime, so a still-open older host facade cannot resurrect an obsolete broker;
- hands off an authenticated stale broker only after closing new mutation admission and proving it is idle with no prompt mailbox entry or browser child; current brokers drain atomically, while the one-time legacy path quarantines every authenticated socket before two final identity/idle/lease checks and restores those sockets on any active or ambiguous evidence;
- installs the bundled `ego-chat` skill under `~/.codex/skills/ego-chat`;
- registers the installed executable as the `ego_chat` STDIO MCP server with a timeout of at least 29,100 seconds: the eight-hour attachment plus five minutes of host transport margin.
Restart Codex.app after setup and use `/mcp` to verify `ego_chat`. Use `ego-chat setup --skip-codex-config` when configuration is managed separately. Setup refuses to replace a different skill or MCP entry unless `--force` is explicit.
`ego-chat setup-zcode` uses ZCode's native user-level surfaces:
- installs `SKILL.md` under `~/.zcode/skills/ego-chat`;
- semantically merges `mcp.servers.ego_chat` into `~/.zcode/cli/config.json` while preserving existing plugin and server entries;
- registers the absolute installed executable with `args: ["mcp"]` and a timeout of at least 29,100,000 ms: the eight-hour attachment plus five minutes of host transport margin, which `doctor-zcode` also validates;
- does not require Codex for ZCode-owned review cycles, while preserving a still-executable managed Codex path from an earlier Codex setup when Codex is temporarily absent from `PATH`.
Restart ZCode.app after setup and verify `ego_chat` under MCP Services. The paths and configuration shape follow ZCode's official [MCP Services](https://zcode.z.ai/en/docs/mcp-services) and [Skills](https://zcode.z.ai/en/docs/skill) documentation. A conflicting `ego_chat` server or skill is never replaced without explicit `--force`.
`ego-chat setup-claude` uses Claude Code's native user-level surfaces and also covers the Claude.app Code tab:
- installs `SKILL.md` under `~/.claude/skills/ego-chat`, or under `$CLAUDE_CONFIG_DIR/skills/ego-chat` when that variable relocates the Claude home directory;
- registers the absolute installed executable with `args: ["mcp"]` as the user-scope `ego_chat` stdio server through `claude mcp add-json --scope user`, with a per-server `timeout` of at least 29,100,000 ms: the eight-hour attachment plus five minutes of host transport margin. Setup never rewrites `.claude.json` itself; it reads the file to detect a conflicting entry, delegates the write to the Claude CLI, and re-reads the file to verify the result, which `doctor-claude` also validates read-only;
- requires Claude Code 2.1.203 or newer, because from that version the per-server timeout also floors the 30-minute stdio idle abort that would otherwise end a silent Token-Saver wait;
- warns, without editing anything, when `~/Library/Application Support/Claude/claude_desktop_config.json` also defines `ego_chat`, because the Claude.app Code tab would use that definition and its timeout instead of the user-scope entry;
- does not require Codex for Claude Code-owned review cycles; broker-owned Codex convergence remains available when Codex is installed.
Restart open Claude Code sessions and Claude.app after setup and verify with `claude mcp get ego_chat`, which reports the executable and the timeout. Claude Code moves a main-conversation tool call that runs past two minutes to a background task and delivers its result as a notification, which suits Ego Chat's single long wait. The Claude.app Code tab loads the same user-scope server and personal skill; the Claude.app chat surface is not configured because it cannot load the skill from disk, has no documented long-call timeout, and a `claude_desktop_config.json` entry would override the timeout-carrying definition for the Code tab. A conflicting `ego_chat` server or skill is never replaced without explicit `--force`.
Install the released crate from crates.io:
```sh
cargo install ego-chat --locked
ego-chat setup
# Or, for ZCode:
ego-chat setup-zcode
# Or, for Claude Code:
ego-chat setup-claude
```
To try the current unreleased `main` branch directly from GitHub:
```sh
cargo install --git https://github.com/xicv/ego-chat --locked
ego-chat setup
# Or, for ZCode:
ego-chat setup-zcode
# Or, for Claude Code:
ego-chat setup-claude
```
The CLI and MCP facade autostart the broker. Check it with:
```sh
node ./bin/ego-chat.mjs ping
```
Inspect the authoritative broker generation, runtime-contract digest, active workflows, and bounded-store counters without opening ChatGPT:
```sh
node ./bin/ego-chat.mjs broker-status
```
`ego-chat doctor` and `ego-chat doctor-zcode` also compare any live authoritative broker with the installed runtime. During setup, a protocol-aware stale broker atomically rejects new mutations, waits for already admitted mutation handlers, and rechecks durable workflows, active bindings, and the driver mailbox before gracefully releasing its lease. A legacy v0.2.1 broker has no drain method, so setup temporarily removes its authenticated socket names from ordinary clients, waits for in-flight requests, repeats the idle proof through the quarantined socket, and either stops it or restores every socket without signalling. This makes concurrent Codex/ZCode use fail closed instead of racing shutdown.
After upgrading, restart every open supported host, including both Codex.app and ZCode.app when both are running. A stale facade may read status and existing workflow results, but new mutations fail with `restart_required`; its verified managed daemon launcher points to the current runtime, so it cannot recreate an obsolete broker while waiting to be restarted. Ego Chat never starts a second broker against the same data directory to hide a mismatch.
## Persistent conversation
Inspect the existing default binding without touching the browser:
```sh
node ./bin/ego-chat.mjs conversation ego-chat-main
```
Run a browser-backed maintenance checkpoint of its canonical URL and current conversation head without sending:
```sh
node ./bin/ego-chat.mjs verify ego-chat-main
```
Do not use `verify` as a preflight for an exchange or review. Fresh sends perform their own canonical URL, stable-head, browser-readiness, and live model-policy checks. When an unattended loop has explicit reclaim authorization, the fresh send is also the only path allowed to reclaim the exact binding-owned task space. If you only need to confirm binding identity before a send, use `conversation` or `ego_get_conversation`; both read durable state without touching the browser.
Create-once and existing-URL bindings are accepted through `ego_bind_conversation` or the CLI's `bind <input-json-file>` command. A binding key is immutable: an existing key is never silently replaced. A ChatGPT Project can organize the conversation, but the canonical conversation URL and head fingerprint remain the authoritative identity. Separate workflows may run concurrently only when they use different canonical conversations and different Ego task-space identities. Broker reservations compare those stable identities instead of recycled numeric IDs, so an old A3K binding cannot be blocked merely because an unrelated new workflow later received the same number. The same conversation or proven task-space identity is rejected before browser work with a reservation error. A create-once exchange requires the staged Send/capture adapter: the in-lane Send result reserves its newly learned canonical URL and complete task-space tuple synchronously before the browser lane can be released or a long capture can begin. A monolithic adapter is rejected before Send while that binding is still unbound. Continuous convergence reserves its canonical conversation for the whole workflow, so no manual or second automated send can interleave with the A/B loop.
For a bound conversation, the stored numeric Ego task-space ID is a recoverable location hint, not conversation identity. Navigation first requires the complete persisted `{ name, taskId }` tuple; malformed, discordant, non-unique, or missing live evidence fails closed. Ego Chat does not create a replacement when an established tuple with an opaque task ID disappears; the recreation rule later in this paragraph covers only Spaces that Ego Chat named itself. Only a legacy binding that has never stored a tuple may migrate once into a deterministic per-conversation agent space whose name uses a domain-separated 128-bit SHA-256 prefix; that name and the independently observed opaque task ID are then persisted atomically. Once Send is confirmed, its exact canonical URL and tuple become the effective identity for every pending, terminal, restarted, or reconciled capture; if the old binding and Send both carry tuples, they must be exactly equal. The selected numeric ID and complete tuple are re-observed together immediately before every browser mutation, including selection, navigation, tab switching, policy interaction, prompt focus/insertion, Send press/release, reclaim, and every draft-cleanup retry. Every success result carrying task-space evidence also performs a final live tuple and broker-authority readback before it can be emitted or persisted. When the user explicitly authorizes an unattended loop or tells Ego Chat to take its space back, a fresh exchange can set `allowTaskSpaceReclaim: true`. The driver then claims a user-owned/inactive exact per-conversation `ego-chat-bound-*` space, or takes back the exact persisted space previously delegated to the user, rechecks broker authority after live discovery, verifies agent ownership and the stable tuple, and only then begins pre-Send inspection. This authority is limited to that exact binding space and that fresh Send. It is stripped before response capture and is never honored during reconciliation, verification, re-anchoring, after a possible delivery, or for a recycled unrelated numeric space. Without that explicit opt-in, the existing fail-closed stop remains. When an established tuple that Ego Chat itself named disappears entirely, with no live Space matching or conflicting with it, the driver recreates that identity by name after a short delay (30 seconds by default), reported meanwhile as the retryable `bound_task_space_missing` reason. It then reopens the canonical conversation in the recreated Space and continues; the binding's numeric location is re-recorded and the ledger records `binding.task_space_recovered`. A Space with an opaque `taskId` that Ego Chat did not name still cannot be recreated and still ends as `bound_task_space_identity_changed`.
An ordinary create-once exchange stopped during confirmed-Send capture can be recovered with `ego_reconcile_conversation` after cancellation. This is read-only recovery of the same workflow, never another Send: it requires an empty prior head, the exact durable provider prompt ID, the original tab and complete stable task-space identity, and an attributable two-message response with the original markers. The durable Send record binds the exact outbound prompt digest; the provider message ID anchors the rendered prompt, whose presentation need not preserve those bytes. Older recovery without a provider message ID still requires the exact rendered prompt digest. Missing or conflicting Send evidence and receipt-enabled asset exchanges are not eligible for this cancellation recovery path.
A completed create-once recovery transfers its provisional-URL reservation through the committed canonical result only when the original stable space and bound target still match. The original Send evidence is preserved. A conflicting or malformed result retains the reservation; a valid transfer remains usable after broker restart.
An unbound create-once exchange that stopped before Send confirmation (`browser_operation_interrupted_before_send_confirmation`, for instance after `ego_driver_timeout`) leaves no browser evidence to attribute. A driver the adapter gave up on keeps running inside Ego Lite's service. Its revoked per-run fence stops it from acting, but a broker process that died mid-run never revoked that fence. `ego_reconcile_conversation` (CLI: `ego-chat reconcile <binding-key> <workflow-id>`) resolves it with an account-wide absence proof. When the stop records `browserInterruption.runFenceRevoked: true`, the proof runs at once in the same broker generation: the adapter sets that flag only after it unlinked the attempt's run fence, so no leftover driver mutation can pass its fence check any more, and any Send that did land before the revocation is found by the account scan, which runs after it. Any other stop is proven only from a newer broker generation. The stop records the lease epoch that ran it as `browserInterruption.brokerEpoch`, which must be older than the current epoch; a record written before that field existed must have been last updated before the running broker started. Otherwise the call returns `reconcile_requires_new_broker_generation`. A newer generation's lease fences every leftover driver of the earlier one, and its startup removes a dead broker's run fences, so even a driver whose broker died before revoking its fence can no longer act. The binding must still be the same unbound create-once binding at the recorded revision, with no canonical URL, no messages and the exact prior head, and the workflow must carry no confirmed-Send evidence. A start other than the plain new-chat page, or a `projectUrl`, returns `create_once_absence_unsupported`, because a project conversation is outside the lists the proof reads. The driver selects the binding's Space by its stable identity. A vanished Space is recreated by name and the new-chat page reopened; this is the only unbound case allowed to recreate one. Inside the logged-in page it then reads ChatGPT's own conversation lists, active and archived, for every conversation created from ten minutes before the attempt began onwards, with no upper bound, and checks each one's user messages for the attempt's unique marker. The session token, titles and message text never leave the page. The scan fails closed as `create_once_absence_unproven`, with a reason, on a missing session, any non-200 response, the 40-page cap, an unparseable or zone-less time, an out-of-order or shifting listing, or a conversation without a readable user message. A found marker, or a recorded tab that has become a conversation, returns `create_once_delivery_found` with the conversation IDs. Both leave the workflow unchanged. A completed proof clears the composer only when it holds this workflow's exact prompt and leaves any other draft untouched. It then cancels the workflow as `delivery_absent` with `result.absenceProof`, and re-records a recreated Space or reopened tab as the binding's location, still unbound with its head, message count and canonical URL unchanged, so one successor can be sent on the same binding. One gap remains: a conversation the user has deleted since the attempt is no longer listed, so a prompt delivered into it cannot be seen.
A `human_required` exchange stopped before any Send is reconciled the same way, without waiting for anything. Its `humanRequired.code` is one of `PRE_SEND_PROVEN_STOP_CODES` (exported by `src/broker.mjs`): `unexpected_chatgpt_draft_persistent`, `pre_send_retry_budget_exhausted`, `model_effort_unavailable_before_send`, `pro_model_unavailable_before_send`, `chatgpt_conversation_exhausted_before_send`, `chatgpt_conversation_closed_before_send` and `chatgpt_quota_limited_before_send`. Each is raised before any composition or after only proven pre-click failures, and the workflow must still be browser-owned with no confirmed-Send evidence. On a bound binding the read-only bound reconcile runs with `allowDeliveryAbsent`: the conversation and the binding must still be at the recorded prior head and the marker absent. A marked turn found there is refused as `pre_send_stop_marker_found`, never captured. On an unbound create-once binding the account-wide proof above runs with every check except the broker-generation rule, because the stop came from a driver run that completed and any earlier timed-out attempt was retried only once its run fence was revoked. Either way the workflow ends as PeoplePlanner expects, `status: "cancelled"`, `phase: "delivery_absent"`, `result.deliveryState: "absent"`, so its dispatch can be retired and one successor sent. Any other stop code is still `workflow_not_reconcilable`.
### Accept an intentional external continuation
If a person or another client appends to a bound ChatGPT conversation, the next Ego Chat send still stops with `conversation_head_changed`; it never silently skips the ownership check. Before reporting that stop, the browser now reads the tail twice. A transient hydration mismatch that returns to the durable head is accepted, while an unstable or persistently changed tail remains fail-closed.
**A page that drew the answer blank.** A ChatGPT page whose own scripts failed to load draws every answer as an empty message while user messages still show, and it never recovers by itself. This was seen live on 2026-09-23, right after an Ego Lite restart. When the bound answer reads blank where Ego Chat recorded text, a Send or `ego_verify_conversation` reloads the page once and reads it again. The reload is proven the same way as the Pro only reload and has its own one-per-run budget. If the answer is still blank, the run stops as `conversation_head_unrendered`, never as `conversation_head_changed`, so nothing is re-anchored to a blank answer. Before Send, that stop is retried within the pre-Send retry budget, and each retry loads a fresh page. Re-anchoring refuses a blank answer too.
A persistent mismatch is eligible for explicit re-anchoring only when the stopped workflow proves all of the following: browser composition never started, Send was not attempted, the two observed heads were stable, and the new tail is a completed assistant message. The result then includes `humanRequired.reanchor` with the exact `bindingKey`, `sourceWorkflowId`, `expectedBindingRevision`, and `expectedObservedHeadFingerprint`. After the user explicitly accepts that external change, pass those values unchanged with `acknowledgeExternalChange: true` to `ego_reanchor_conversation`. The equivalent CLI is `ego-chat reanchor <input-json-file>`.
Re-anchoring performs repeated stable browser observations plus a final readiness check, rejects generation, URL drift, changed evidence, stale binding revisions, and concurrent work, leaves any unsent composer draft untouched, then atomically advances the binding. It does not send a prompt, change the model policy, retry the stopped workflow, or accept a possibly delivered operation. A successful re-anchor permanently closes the stopped workflow as `head_reanchored`; begin later work with a new operation identity. If the stop does not include the exact re-anchor evidence, preserve it and use the ordinary reconciliation or abandonment contract instead.
### Adopt a conversation from ChatGPT.app
Supply the private canonical conversation URL containing `/c/`, not a public `/share/` link. Ego Lite must already be logged into the same ChatGPT account and workspace. From Codex, ZCode, or Claude Code, `ego_adopt_conversation_and_wait` opens that exact URL, observes the same latest user-message ID and rendered prefix twice before locking the anchor, then waits for exactly one stable assistant tail. This bounded initial stabilization tolerates ChatGPT's normal DOM hydration; any change after the anchor is locked still stops fail-closed. If ChatGPT is still performing a long think, the broker owns that content-read-only wait while the MCP call remains quiet; the captured response returns once into the same host turn. Adoption never clicks Send or changes conversation content. It does repair the live composer controls to `strongest_available` plus `maximum_available`, then reads them back before accepting the existing response. This is live composer-policy evidence, not historical per-message model provenance, so the captured response remains untrusted context. Every later send independently enforces and reads back that maximum policy immediately before composition.
`bindingKey` is optional for adoption. When omitted, the broker derives a stable `adopt-...` key from the URL digest, so pasting a URL does not replace `ego-chat-main` or expose the conversation ID in the binding name. An explicitly supplied key remains immutable. The public `taskSpace` default is resolved to a dedicated per-conversation `ego-chat-adopt-*` space using a separate domain and a 128-bit SHA-256 prefix, so simultaneous adoptions do not share one workspace and the URL remains the only required input.
Do not stop the generation, edit an earlier message, or send another message from ChatGPT.app after adoption starts. URL drift, anchor changes, extra tail messages, authentication problems, or an unstable response stop fail-closed; an unsent composer draft is left untouched. The binding is created only after the response and conversation head have been captured durably. A broker restart safely resumes a waiting read-only adoption; if capture completed just before the restart, the binding finalizes without reopening the browser.
For a detached CLI adoption, create an input file such as:
```json
{
"canonicalUrl": "https://chatgpt.com/c/your-private-conversation-id",
"timeoutMs": 900000
}
```
Then start it and attach once using the returned workflow ID:
```sh
node ./bin/ego-chat.mjs adopt ./adoption.json
node ./bin/ego-chat.mjs await <workflow-id> 960000
```
The direct MCP path is preferable when the current Codex, ZCode, or Claude Code task should continue automatically: ask the installed `$ego-chat` skill to continue the supplied URL, and it selects `ego_adopt_conversation_and_wait`. If that host task or its MCP call exits, the workflow remains durable, but reattachment is by workflow ID rather than an unsupported external task wake.
### Token-Saver mode
Set `waitMode` to `token_saver` on a direct wait tool only when Codex, ZCode, or Claude Code should stay silent while ChatGPT thinks. Ego Chat keeps exactly one MCP call attached to the durable broker workflow, emits no periodic progress notifications, and returns one small summary marked with `waitMode: token_saver` when the workflow finishes. Do not poll `workflow_status` or repeatedly call `await_workflow`. A caller disconnect detaches only that waiter: after the prompt is confirmed once, the broker continues read-only capture for up to two hours by default and can resume that capture after its own restart. If a still-connected host receives a wait error, Ego Chat includes the durable workflow ID so it can reattach once; a fully exited host task still has no external wake guarantee. Conversation adoption defaults to Token-Saver. Durable convergence keeps its default `progress` mode: a deterministic local supervisor reports phase changes, recovery counters, exact ChatGPT delivery state, and terminal parent/child state, with a bounded one-minute unchanged-state heartbeat. Initial and interval status reads are both abortable and drained before the wait result returns. Notification writes use one owned, coalescing queue: status reads continue during stdio backpressure, pending observations collapse to the latest state, and the accepted write completes before the terminal result is returned, so stale running text cannot follow completion. Those status reads do not invoke another model or start another browser workflow.
Token-Saver reduces idle outer-agent turns and transport chatter. It does not weaken the strongest-model policy, shorten ChatGPT's reasoning, reduce the implementation turns genuinely needed for convergence, or provide an external wake after the host task has exited.
Local state defaults to `~/Library/Application Support/Ego Chat`. The directory is mode `0700`; ledgers, checkpoints, blobs, and the broker token are private. Large responses return an `ego-chat-result:<sha256>` reference; use `ego_read_result` with the originating workflow ID and expected digest when the full body is needed. Raw bodies expire after 30 days or earlier under the 256 MiB blob limit, while bounded excerpts and identity metadata remain.
Storage admission is fail-closed and happens before policy mutation, composition, or Send. Running, `human_required`, and still-reconcilable failed browser workflows keep their captured bodies protected; a bodyless workflow reserves the full 256 KiB per-result maximum across compaction and restart until exact reconciliation stores its response, delivery absence is proven, or the user explicitly abandons that recovery. The durable state checkpoint is limited to 64 MiB, the active event ledger to 8 MiB, unresolved recovery workflows to 256, bindings to 256, and exact lifetime operation identities to 10,000. The two checkpoint/state copies, event ledger, and blob tree therefore have a bounded persistent-data envelope of about 392 MiB plus small control files.
Prompt-bearing fixed-driver handoff files live in a separate private mailbox capped at 4 MiB, 16 files, and 512 KiB per input, giving the complete bounded local envelope about 396 MiB plus small control files. A browser child is durably registered before its PID-derived input file is created. The child opens that exact regular file with no-follow semantics, validates owner/mode/link count and size, reads it, then unlinks the pathname before the first browser interaction. Startup and every admission lstat only recognized owned inputs, preserve an exact file for a still-live registered process group, and remove inactive crash leftovers after five minutes; legacy UUID-only inputs follow the same short retention rule. A symlink, foreign owner, non-`0600` mode, hard link, oversize input, ambiguous duplicate PID, unknown entry, or exhausted byte/file quota fails closed before a browser process starts. `broker-status` reports mailbox limits, current files/bytes, reservations, and retention without exposing prompt contents or paths.
Per-run driver fences and stage records live beside the mailbox, in `/tmp/egc-driver-<uid>-fences`, never inside it: brokers up to 0.2.37 reject any mailbox entry that is not a driver input, so a fence directory inside the mailbox would stop an older broker sharing it. The fence directory is created `0700` on the first run and must stay an owner-only directory; a symlink, foreign owner, or other mode fails closed before a browser process starts. Fence and stage files never count against the mailbox limits. A finished run removes its own fence and stage record, and startup removes only the fence and stage files of broker processes that no longer exist, leaving this broker's and any other live broker's runs untouched.
At an identity, recovery, or mailbox-capacity limit, existing exact retries and evidence remain readable, while a genuinely new operation stops before browser work instead of deleting proof or reopening an old identity. Treat the state directory and driver mailbox as sensitive.
`abandon_workflow_recovery` is a deliberate last-resort capacity release, not a retry mechanism. It requires `acknowledgePotentialDelivery: true`, is valid only for one stopped adoption, browser, or convergence recovery, and never removes any durable operation identity or permits it to run again. Use it only after the user explicitly chooses to stop recovering that exact workflow and understands that a remote ChatGPT turn or Codex turn may still exist; inspect the bound conversation and task evidence before authorizing later work that could duplicate it.
The equivalent direct CLI form is `ego-chat abandon <workflow-id> --acknowledge-potential-delivery`. Omitting the exact acknowledgement flag stops with usage error and does not mutate the workflow.
## Real-world workflows
In these workflows, **the local coding agent** means Codex.app, ZCode.app, or Claude Code, and **the web reviewer** means the private ChatGPT web conversation opened through Ego Lite. Ego Chat installs into Codex, ZCode, and Claude Code; it does not install an MCP server into ChatGPT.app. If a discussion starts in ChatGPT.app, copy its private canonical `/c/` conversation URL and adopt it from Codex, ZCode, or Claude Code while Ego Lite is logged into the same ChatGPT account and workspace.
Ego Chat transports prompts, responses, and bounded review packets. It deliberately does not transport ZIP archives, clone repositories, upload files, create merge requests, or grant commit, merge, deployment, or release authority. For a code handoff, prefer an accessible repository plus an exact commit SHA or merge-request URL over a ZIP because the local agent can verify identity and drift independently.
### Coverage at a glance
| Need | Coverage | Boundary |
| --- | --- | --- |
| Start from Codex, ZCode, or Claude Code and ask ChatGPT to research or brainstorm | Supported | Use durable convergence when the request says to continue until settled; use a Token-Saver exchange for one turn. |
| Reuse the same private ChatGPT conversation for later turns | Supported | The durable binding verifies the canonical URL and conversation head before every send. |
| Continue after a deliberate manual or other-client ChatGPT turn | Supported before Ego Chat sends | A proven stable assistant-tail advance is re-anchored automatically. A possibly accepted Ego Chat Send remains in read-only reconciliation so it is never duplicated. |
| Start from ChatGPT web or ChatGPT.app and continue locally from a URL | Supported | Adoption accepts a private canonical `/c/` URL, never a public `/share/` URL. |
| Wait while an already-running ChatGPT response performs a long think | Supported | Adoption waits read-only. For Ego Chat sends, `send_confirmed` is durable and capture continues independently of the caller's original wait. |
| Import the entire earlier transcript into the local task | Not provided | Adoption returns the latest stable assistant tail; the web conversation itself retains the earlier history. Use a self-contained final handoff packet. |
| Iterate implementation and review without human copy and paste | Supported | Codex defaults to broker-owned durable convergence for explicit until-settled requests. A current Codex, ZCode, or Claude Code task can submit one candidate per cycle as a fallback. Neither path has an implicit cycle ceiling. |
| Use Codex, ZCode, and Claude Code at the same time | Supported with separate bindings | All hosts share one broker. Distinct conversations queue through one browser lane; the same binding remains exclusive and is never auto-forked. |
| Always use ChatGPT's strongest current model and maximum thinking effort | Supported | Every adoption and send repairs and verifies the live provider-defined maximum without pinning a model label. GPT-6 Astra is selected through ChatGPT's current GPT-6 Pro route when that option is available to the signed-in account and workspace. |
| Receive a generated ZIP, repository, branch, or merge request automatically | Not provided | Transfer or fetch artifacts through an independently authorized file or GitHub workflow. Ego Chat carries text and bounded review evidence only. |
| Wake a Codex, ZCode, or Claude Code task after that host task has fully exited | Not provided | The broker remains durable, but automatic external task wake is not claimed. Reattach once by workflow ID when possible. |
| Commit, push, merge, deploy, or release | Outside Ego Chat | The local coding agent performs these only with explicit authority and separate verification. |
### Handoff packet for web-first work
Before moving a long ChatGPT discussion into Codex, ZCode, or Claude Code, ask the web reviewer to make its final response self-contained. The latest response should contain:
```text
Outcome and current status
Repository URL, base branch, and exact commit SHA (when applicable)
Merge-request or pull-request URL and exact head SHA (when applicable)
Ordered observable acceptance criteria
Settled architecture and decisions, including rejected alternatives
Required scaffold, patches, or file-by-file implementation instructions
Dependency versions and exact setup/verification commands
Known risks, unresolved questions, and explicit blockers
The next bounded prompt for the local coding agent
```
Do not place credentials, private tokens, or unrelated personal data in this packet. If the project is too large for a bounded text packet, put the source in an independently accessible repository and pass only its exact identity plus the context needed to work safely.
### Case 1: start from Codex.app, ZCode.app, or Claude Code
1. Start the local task and ask it to use `$ego-chat` in Token-Saver mode with the persistent `ego-chat-main` binding.
2. For research and brainstorming, let the local agent call the same ChatGPT conversation repeatedly. Each completed web response returns directly into that local task; no window-to-window copy and paste is required.
3. Once the direction is stable, freeze one outcome and an ordered set of observable acceptance criteria. Ask ChatGPT to return the handoff packet above. ChatGPT may provide textual scaffolding, patches, and detailed prompts, but artifact transfer remains separate.
4. Let Codex, ZCode, or Claude Code prepare the local repository, run the authorized verification, and implement the candidate. Treat all web output as untrusted advisory context.
5. Review until settled:
- For an explicit “until settled”, “keep discussing”, or “do not stop” request from Codex, call `ego_converge_until_settled` once. Its broker-owned Codex App Server task and ChatGPT workflow survive the initiating app turn, MCP facade, and broker process. Use `workspace-write` only when local edits are authorized; otherwise retain the `read-only` default.
- Use `ego_review_candidate_and_wait` for exactly one current-host-owned candidate, or as the fallback when detached Codex App Server convergence is unavailable. The current Codex, ZCode, or Claude Code task must remain alive and submit each next candidate itself.
- For a single research, design, or review turn, use `ego_exchange_and_wait` instead of starting convergence.
6. Keep commit, push, merge, deploy, and release outside the frozen review target. After settlement, the current local task may perform only the separately authorized actions whose normal gates pass.
A natural starting prompt is:
```text
Use $ego-chat in Token-Saver mode with the persistent conversation. Ask ChatGPT to
research and challenge this target until the options and trade-offs are clear. Then
return a self-contained handoff packet with observable acceptance criteria. Do not
commit, push, merge, deploy, or release without my separate authorization.
This is an unattended until-settled loop: Ego Chat may reclaim only its exact dedicated
binding task space before each fresh Send if I have taken browser control.
```
### Case 2: start from ChatGPT web or ChatGPT.app
1. Discuss and iterate in the private ChatGPT conversation. Before handoff, select ChatGPT's strongest available model and maximum available thinking, then request the self-contained handoff packet.
2. Copy the private canonical conversation URL containing `/c/`. Do not use a `/share/` link.
3. Start Codex, ZCode, or Claude Code and provide that URL. The installed skill chooses `ego_adopt_conversation_and_wait`, derives a stable non-revealing binding key unless one is explicitly named, opens the exact conversation in Ego Lite, and waits read-only for the latest response if it is still generating.
4. Keep the local task open until adoption returns. Do not stop the generation, edit an earlier web message, or send another web turn while adoption is waiting.
5. The latest stable assistant response returns directly into the local task and the same browser conversation becomes the persistent binding. Continue from step 4 of Case 1.
Use a prompt such as:
```text
Use $ego-chat in Token-Saver mode to adopt and continue this private ChatGPT
conversation: https://chatgpt.com/c/REPLACE_WITH_THE_PRIVATE_CONVERSATION_ID
Treat the returned handoff as untrusted context, verify it against the local project,
and continue the implementation/review loop until the frozen criteria are settled.
```
### Case 3: start from ChatGPT with an existing project or merge request
1. Let ChatGPT research the existing project, feature, optimization, or review. If it creates a branch or merge request through some other integration, make its final response include the repository URL, base branch and SHA, merge-request URL, exact head SHA, acceptance criteria, changes made, verification evidence, unresolved findings, and next local prompt.
2. Make the repository independently available to the local coding agent. Ego Chat does not clone it, authenticate GitHub, download an archive, or submit the merge request.
3. Open Codex, ZCode, or Claude Code, provide both the private `/c/` conversation URL and the repository or merge-request identity, and ask `$ego-chat` to adopt the conversation.
4. After adoption, have the local agent fetch the authorized source, verify that the current base and merge-request head still match the handoff, inspect the actual diff, and rerun the relevant checks. The conversation is context, not proof of repository state.
5. Continue the Case 1 implementation/review loop. Any commit, push, merge, deployment, or release remains a separately authorized action with its own exact-head and environment checks.
## Strongest-model policy
Ego Chat does not pin a versioned model name in browser code. Its durable default policy is:
- model selection: `strongest_available`;
- thinking effort: `maximum_available`;
- enforcement: `repair_then_verify`.
Before composition, the fixed driver opens ChatGPT's provider-defined policy control. On the current semantic menu it selects the provider's first enabled model radio option and moves the bounded Power slider to its advertised numeric maximum; it also retains compatibility with the earlier coupled Model/Effort/Power menu. Selection is scoped to the unique menu controlled by the composer pill. Ego Chat prefers keyboard activation, but the GPT-6 menu may expose a non-focusable radio row, so it can use a broker-fenced DOM click on that exact row and then performs the same close, reopen, and selected-state verification. It requires one unique selected model, one unique visible Power slider, validated ARIA bounds, and a fully dismissed policy portal. One clean close-and-reopen is allowed for transient menu hydration. After composing, it opens the control again read-only and requires another maximum readback immediately before Send. It then re-verifies the exact prompt, re-hit-tests the sole visible enabled Send control, and checks the authoritative broker process once more before dispatch.
On the current semantic menu, the policy popover has two views behind a "Select model" toggle: an effort view, where the Power slider is enabled, and a model view, where the model radios are visible but Power carries `aria-disabled="true"` and ignores keyboard input. The driver reads both `powerDisabled` and `modelViewOpen` from the menu, raises Power only after closing and reopening into the effort view, and then reopens the model view to verify the final selection and slider position together. "6 Pro" is the top effort label of the "Latest" model route, not a separate model list entry, so selecting "Latest" and maximizing Power is what reaches it. Each captured response also records the answering model as reported by ChatGPT itself, `responseModelSlug` (for example `gpt-6-pro`), read from the assistant message's `data-message-model-slug` attribute — independent evidence of which model actually answered, alongside the pre-send policy labels.
OpenAI names the underlying model **GPT-6 Astra** and exposes it in ChatGPT Chat as **GPT-6 Pro**. On the 2026-09-05 live Web UI, an isolated source-worktree adoption observed the provider-first model route as `Latest`, the closed composer pill and effort as `6 Pro`, and Power at `5/5`, durably recorded at policy revision `1`. This did not update the locally installed Ego Chat runtime. The route and pill labels are observations, not permanent configuration values; rollout is gradual, and plan, account availability, workspace policy, and usage limits determine whether GPT-6 Pro is available. See OpenAI's [GPT-6 Astra model page](https://developers.openai.com/api/docs/models/gpt-6-astra) and [ChatGPT model availability guide](https://help.openai.com/en/articles/20001354-gpt-56-and-gpt-6-pro-in-chatgpt).
If ChatGPT later places a stronger model first or renames the maximum effort, the next successful verification records the new labels automatically and marks `selectionChanged`. No source edit or hardcoded model-name replacement is needed. A temporarily hydrating or unreadable policy control is retried with bounded backoff; its durable recovery record includes a bounded `uiReason` such as `policy_model_choice_activation` so a structural rollout regression is diagnosable without persisting page text. Ego Chat clears an exact unsent draft when necessary and keeps the workflow alive. It never silently downgrades. Only a conclusive unsupported policy, signed-out session, or human verification challenge crosses the human boundary.
The pre-send check above proves the composer's strongest route and maximum effort, but not which model actually answers — a provider capacity or availability change can quietly drop the responding model while every send still succeeds. Ego Chat ranks known `responseModelSlug` values (`ANSWERING_MODEL_RANKS` in `src/constants.mjs`, highest first: `gpt-6-pro`, `gpt-6`, `gpt-5-6-pro`, `gpt-5-6-thinking`, `gpt-5-6`, `gpt-5-5-pro`, `gpt-5-5-thinking`, `gpt-5-5`, with prefix matching for a versioned or dated slug and an unknown slug ranked lowest) and compares each captured answering model against the last known-good one for that binding. A detected drop always dispatches a local `model_downgrade` alert and records a durable `lastDowngrade` entry on the model policy (see `getStatus().alerts.lastAlert` and `ego_get_model_policy`); a single exchange only ever alerts. Pass `answeringModelPolicy: "pause"` when starting a convergence to additionally end the affected cycle as `human_required` with code `answering_model_downgraded` the moment a review's answering model ranks below that convergence's first observed review model — the downgraded review is retained without a resend. The default `"alert"` policy lets the convergence continue past the alert.
The pre-Send policy check also reads the top effort itself. ChatGPT's Power item carries an accessible description of its current effort, "<name>, <level> of <max>." (on 2026-09-23: "Pro, 5 of 5.", next to a keyboard hint), which the driver records as `effortName`, `effortLevel`, `effortMax` and `effortState` on the model-policy observation. `effortState` is `top` when the strongest route is selected, the level equals its maximum and the name is one of `EXPECTED_TOP_EFFORT_LABELS` (`Pro`, compared case-insensitively), `pro_fallback` for Pro at the maximum on the [Pro only](#pro-only-opt-in) policy's fallback route, `below_top` otherwise, and `unknown` when the description does not parse. **On this account the slider does not name the model version**, so the check proves only "Latest at the top effort named Pro"; which model actually answered is still proven by `responseModelSlug` alone. A lower top effort, for example "Extra High, 4 of 4." as when Pro was withdrawn mid-run in the 2026-09-11 qualification below, dispatches a `model_downgrade` alert (code `model_effort_below_top`) with the observed label once per episode: the top effort is account-wide, so while a Pro allowance stays used up, later sends and paused reviews on any binding raise no further effort alert until the top effort has returned. A review child of a convergence started with `answeringModelPolicy: "pause"` instead stops before Send as `model_effort_unavailable_before_send`: before composing, or after clearing its own draft at the pre-Send re-verification, with nothing sent, and the convergence pauses. A single exchange only alerts. An unreadable description is recorded as `unknown`, alerts once (code `model_effort_unknown`) until a readable one returns, and never blocks. The fields are advisory: they never fail the maximum-policy proof, and like `responseModelSlug` they stay out of the observation identity, so records written before them do not look changed. A lower effort sent under the default policy alerts when its observation is recorded at capture, so one whose capture never completes does not alert.
Successful adoption validates and durably records the observation with its source workflow identity and policy revision. `ego_get_model_policy` returns that verified state after normal store replay as well. Read the last durable observation without opening the browser:
Interpret `lastObserved.modelLabel` as the selected menu route and `pillLabel` as the closed composer label. In the separate-model menu, the compatibility field `effortLabel` repeats `pillLabel`; use `powerLevel/powerMax` to identify the thinking setting. For example, report an observed `Latest` → `6 Pro`, Power `5/5`, with its `verifiedAt` timestamp. These are observed UI labels, not a fixed API model identifier. Token-Saver replies retain the composer label and verification timestamp so the same distinction is available in compact text. The stored observation does not replace the next Send's live checks.
```sh
node ./bin/ego-chat.mjs model-policy
```
Repair to the provider's current maximum and verify the exact bound chat without sending a prompt:
```sh
node ./bin/ego-chat.mjs ensure-model-policy ego-chat-main
```
The equivalent MCP tools are `ego_get_model_policy` and `ego_ensure_model_policy`. Every normal handoff and every ChatGPT review cycle performs the repair-and-readback before composition plus a fresh read-only maximum check immediately before Send.
### Pro only (opt-in)
By default an exchange sends at whatever the strongest route's maximum is and alerts when that is not Pro. `model-policy.json` in the data directory (`~/Library/Application Support/Ego Chat` by default) turns on a stricter policy:
```json
{ "requirePro": true }
```
Then every exchange Send, from a single exchange, a convergence's review or a successor's first review, goes out only at a Pro model: GPT-6 Pro first, then GPT-5.6 Sol Pro, and otherwise it stops before composing and sends nothing. The schema is strict: `requirePro` is required and must be a boolean, and the only other keys allowed are the [Pro usage meter](#pro-usage-meter)'s optional `allowances` and `usageWarningPercent`. A missing file leaves the policy off; an unreadable or invalid one leaves it off too and reports `reason: "config_invalid"`. The broker reads the file once at start (`src/model-policy-config.mjs`), reports it as `broker.status.modelPolicyConfig` (`{ requirePro, reason, allowances, usageWarningPercent }`), and stamps `requirePro` onto each exchange it admits while the policy is on, so restart the broker after changing the file. With it off, nothing in this section happens except the usage meter.
The live facts behind it (Pro plan, 2026-09-23): GPT-6 Pro allows 200 messages a week, GPT-5.6 Sol Pro a separate 170 a day, and both together at most 200 a day. At GPT-6 Pro's weekly limit ChatGPT switches `Latest` to GPT-5.6 Thinking at Medium ("Medium, 2 of 4.", beside a disabled `Pro` option in the model list), while selecting `GPT-5.6 Sol` still reaches Pro. Under `requirePro` an exchange:
1. **Reloads its page once** before the Send baseline: after selection's readiness and foreign-draft check, so a reload never discards a draft Ego Chat did not write, and after the composer-surface and generation checks, before the head is read. A long-open tab can run an older ChatGPT bundle whose menu still shows a maximum that is gone (live: `Latest`, pill `Medium`, Power 5/5 at the limit) or hides one that has come back. The reload is a fenced mutation stage, `refreshing_policy_page`: the driver sets a page-local marker, navigates the same tab with `gotoAndWait` to the binding's canonical URL, or to a create-once binding's original new-chat URL, and proves a new document, since the marker is gone and `performance.timeOrigin` advanced. It then checks the same tab, the URL, the task-space identity and page readiness again, and the composer-surface, generation, head, marker and draft checks all run on the new page. There is at most one reload per run. A reload it cannot prove stops the run as `policy_page_refresh_failed` with a fixed `uiReason` such as `policy_refresh_unproven` or `policy_refresh_navigation`, which the broker retries under the exchange's pre-Send retry budget.
2. **Climbs the Pro ladder** after repairing the strongest route as usual. Rung 1 accepts that route only when the Power description at its maximum reads Pro ("Pro, N of N.", compared case-insensitively), the slider sits at its numeric maximum and the two agree: route `gpt6_pro`. Otherwise rung 2 selects the enabled option whose first line is exactly one of `PRO_FALLBACK_ROUTE_LABELS` (`GPT-5.6 Sol`; it must match exactly one option, and no other option is ever selected), raises its Power to the maximum and reads it back the same way: route `sol_pro`, effort state `pro_fallback`. Otherwise the exchange stops before composing as `pro_model_unavailable_before_send`, with the rungs it saw in `evidence.proLadder` and `evidence.reason` `pro_unavailable`. An unreadable or unparsable Power description stops the same way with reason `effort_unreadable`, never as an advisory `unknown`. The older coupled menu offers no second route and accepts only a maximum named Pro. The chosen route is recorded on the model-policy observation as `proRoute` (`route`, `modelLabel`, `effortName`, `effortLevel`, `effortMax`).
3. **Pins the pre-Send readback** to that route. The read-only re-verification immediately before the click compares the selected option's label, not its position (options can reorder), and a Pro maximum on it, instead of the strongest position. A change clears the composed draft and stops as `pre_send_state_changed`, whose `changed` lists `model_route`, `power` or `effort`; nothing is sent, and the retried attempt climbs the ladder again.
`pro_model_unavailable_before_send` is human-only: it is never retried, and as one of `PRE_SEND_PROVEN_STOP_CODES` reconcile can close it as delivery absent. Wait for the reset time its message names (below) or ChatGPT shows, then start a new exchange with a fresh turn marker. A Send at GPT-5.6 Sol Pro raises one `model_pro_fallback` alert (kind `model_downgrade`) per episode, deduplicated like the effort alerts across sends, bindings and broker restarts. The per-binding `answering_model_downgraded` notification stays silent for that announced fallback, a `pro_fallback` Send answered by `gpt-5-6-pro`, while `lastDowngrade` is still recorded; any other downgrade still notifies. Independently of `requirePro`, a convergence started with `answeringModelPolicy: "pause"` accepts both Pro answering models, `PRO_ACCEPTED_ANSWERING_SLUGS` (`gpt-6-pro` and `gpt-5-6-pro`, matched by prefix like the rank table): it pauses only on a drop from its first review's model to anything else.
The ladder is qualified against a DOM fixture built from those live menus, not yet in a live run. Still to confirm live: that `gotoAndWait` to the tab's own URL loads a new document (if it does not, every Pro only Send stops as `policy_page_refresh_failed` until the retry budget ends), the name GPT-5.6 Sol's top level shows at the limit (expected "Pro, 5 of 5."), and that the Power description is still present in the model view with GPT-5.6 Sol selected.
#### ChatGPT's reset times
The reload in step 1 also records when ChatGPT says each Pro allowance resets, without any request of Ego Chat's own. On a reload ChatGPT's page itself sends `POST /backend-api/conversation/init`. Just before the reload's navigation the driver drains the runtime's buffered CDP events with `drainEvents()` and discards them, so an older response can never be read. After every reload check has passed it drains again, takes the last `Network.responseReceived` for exactly `https://chatgpt.com/backend-api/conversation/init`, and reads that response's body with `Network.getResponseBody`, the one method `readOnlyCdp` admits besides the UI drift check's two. From the body it keeps only `limits`, every time as canonical UTC:
- `observedAt`, when the body was read;
- `modelLimits`, at most 8 `{ modelSlug, resetsAfter }`, each slug at most 80 characters of `[a-z0-9._-]`;
- `banner`, `{ name, description, resetsAfter }` or null, the name at most 80 characters and the description at most 200 with control characters removed;
- `blockedFeatures`, at most 8 `{ name, resetsAfter }`, each name at most 40 characters.
Every other body field, `limits_progress` among them, is dropped. Anything missing, oversized (a body over 256 KiB, or more than 8 entries), malformed or out of bounds, slower than its bound (1 second for the drain before the reload, 4 seconds for the read after it), or failing in any other way gives `limits: null` with a fixed `limitsReason`: `events_unavailable`, `init_response_missing`, `init_response_failed`, `init_body_unavailable`, `init_body_oversized`, `init_body_malformed`, `init_limits_missing`, `init_limits_invalid` or `init_capture_timeout`. The read never stops a Send and never delays it past those bounds.
`limits` and `limitsReason` ride on the Send's model-policy observation, advisory like the effort fields: a malformed value reads as null and never fails the proof, and they stay out of the observation identity. They are also in the `pro_model_unavailable_before_send` stop's driver evidence, and its message then names each reported reset in ISO UTC, for example `gpt-6-pro at 2026-09-26T04:54:12.921Z`. The durable `humanRequired.evidence` keeps only its fixed fields, so the durable message carries the times. The broker keeps an observation's non-null `limits` as the model-policy record's `proLimits`, in the same commit that records the observation; an observation without them keeps the last known `proLimits`, and a stop records no observation. `broker.status.proLimits` and `ego-chat model-policy` (`ego_get_model_policy`) show them with each entry marked `expired` once its `resetsAfter` has passed, computed when read and never stored. A `model_pro_fallback` alert names when GPT-6 Pro returns, from the current or last known `gpt-6-pro` model limit with a future reset, in local time, for example `... because GPT-6 Pro is unavailable until Sat 26 Sep, 14:54`.
The read is qualified against a synthetic body shaped like the one read live at GPT-6 Pro's weekly limit, through the same runtime globals. Still to confirm in a real Pro only Send: that the drained events hold the reload's `conversation/init` response within the 4-second bound, that draining leaves the runtime's other helpers working, and what the body holds away from a limit.
#### Pro usage meter
`broker.status.proUsage` counts Ego Chat's own answered Sends, with or without `requirePro`. It counts every retained `ego_exchange` whose result names its answering model (`modelPolicy.responseModelSlug`) and that has a Send time (`private.send.sentAt`, else `reconciliation.sentAt`), per model family (matched by prefix like the rank table), over rolling 24 hours (`window24h`) and 7 days (`window7d`). From those it derives `gpt6ProWeek` (GPT-6 Pro in 7 days), `solProDay` (GPT-5.6 Sol Pro in 24 hours) and `proCombinedDay` (both in 24 hours), with the `allowances` they are measured against and `oldestCountedAt`. Every count is a lower bound (`lowerBound: true`): ChatGPT use outside Ego Chat, Sends whose answering model was not read, and workflows the broker no longer retains are not counted.
The meter is advisory and never blocks a Send. The broker recomputes it when an exchange's completion is committed; restart recovery re-drives that commit, so a broker stop between the answer and its completion does not lose the warning. When a counter crosses its warning threshold, `ceil(allowance × usageWarningPercent / 100)`, from below to at or above, the broker dispatches one alert of kind `usage` (labelled `Pro usage`), code `pro_usage_high`, naming the counter, the count, the allowance and that manual ChatGPT use is not counted. A counter that falls back below its threshold as its window moves on alerts again at its next crossing, and a broker start never alerts.
The allowances default to OpenAI's published Pro allowances above, and `model-policy.json` may override any of them and the warning percent:
```json
{
"requirePro": true,
"allowances": { "gpt6ProPerWeek": 200, "solProPerDay": 170, "proCombinedPerDay": 200 },
"usageWarningPercent": 80
}
```
Each allowance is optional and must be a positive integer; a missing one keeps its default. `usageWarningPercent` is optional, a number from 50 to 100, default 80. `requirePro` stays required, and any invalid value makes the whole file `config_invalid`, which keeps `requirePro` off and the default allowances.
### ChatGPT UI drift check
Ego Chat recognizes some ChatGPT states by English UI text, so a ChatGPT release that renames one of those texts would silently hide that state. The read-only UI drift check tests for that:
```sh
node ./bin/ego-chat.mjs ui-check ego-chat-main
```
It runs in the broker's browser lane as the `ui_drift_check` driver mode (broker method `ui.check`). It opens the bound conversation page, where the lazily loaded chunks are present, and reads every script of the page's main frame through `readOnlyCdp`. That wrapper is the driver's only CDP outside a fenced mutation runner, and it admits only `Page.getResourceTree`, `Page.getResourceContent` and, for the Pro only reload's [reset times](#chatgpts-reset-times), `Network.getResponseBody`; the mutation audit test requires every call to name one of them literally. For each text in `src/ui-drift.mjs` it reports whether the English text and its known message id occur in any script: `Thinking failed` (`task.thinking.failed`), `Continue in a new chat` (`chatgpt.context_truncation.continuation_handoff.button.label`), `This conversation has ended.` (`PromptTextarea.conversationEndedPlaceholder`), `Message is too long` (`kDrqDJ`), `New chat`, `Start new chat`, and the call-to-action values `continue_in_new_chat` and `start_summarized_new_chat`. The result is `ok`; `drifted` when a text Ego Chat matches on is missing from every script; or `inconclusive`. It is inconclusive whenever the control text `Stopped thinking` is not found, and whenever a text is missing while any script could not be read: an unreadable resource tree, a failed read, or the 40-second read budget (10 seconds per call, 60 seconds for the whole run) or the script caps running out. A renamed id alone is not drift. The result keeps only booleans, counts, catalog keys and fixed reason codes, never script text. The last result is kept in `broker-status` as `uiDrift` (in memory, not durable), and `drifted` dispatches a local `ui_drift` alert. The result never gates an exchange: nothing on the Send path reads it. While the check runs, at most 60 seconds, it holds its binding the way `verify` does, so an exchange started on that binding meanwhile is refused as busy. Scripts of child frames are not read.
## Codex, ZCode, and Claude Code MCP configuration
The Cargo wrapper configures this automatically. For development directly from this checkout, add the facade to the Codex configuration using the absolute path:
```toml
[mcp_servers.ego_chat]
command = "node"
args = ["/absolute/path/to/ego-chat/bin/ego-chat-mcp.mjs"]
required = true
tool_timeout_sec = 29100
```
For development directly from this checkout, ZCode's equivalent native user configuration is:
```json
{
"mcp": {
"servers": {
"ego_chat": {
"command": "node",
"args": ["/absolute/path/to/ego-chat/bin/ego-chat-mcp.mjs"],
"timeoutMs": 29100000
}
}
}
}
```
For development directly from this checkout, register the facade with Claude Code's own CLI in user scope:
```sh
claude mcp add-json --scope user ego_chat '{"type":"stdio","command":"node","args":["/absolute/path/to/ego-chat/bin/ego-chat-mcp.mjs"],"timeout":29100000}'
```
All three values are eight hours plus five minutes of host transport margin. The one-minute supervised heartbeat keeps a healthy attached call observable; these configured host caps only bound that attachment and remain longer than the broker's eight-hour attachment window. Per-review and per-Codex-turn deadlines remain recovery triggers inside the durable workflow rather than limits on the overall workflow.
## Codex, ZCode, and Claude Code skill
The distributable host-aware skill lives at [`skills/ego-chat`](https://github.com/xicv/ego-chat/tree/main/skills/ego-chat). `ego-chat setup` installs the Codex copy, `ego-chat setup-zcode` the ZCode copy, and `ego-chat setup-claude` the Claude Code copy. After restarting the client, invoke it explicitly with `$ego-chat` or ask naturally:
> Use Ego Chat to review this implementation with ChatGPT until the acceptance criteria are settled.
The skill chooses between private conversation adoption, a one-shot review, durable broker-owned Codex convergence, and a current-task-owned Codex, ZCode, or Claude Code fallback loop. An explicit until-settled request selects durable convergence by default. It preserves `ego-chat-main`, defaults convergence to read-only, and reconciles ambiguous delivery inside the original durable workflow instead of asking the user to relay or resend anything.
For the normal path, the current agent calls `ego_exchange_and_wait` with:
- `bindingKey: "ego-chat-main"`;
- one unique `EGO_CHAT_...` turn marker present exactly once in the prompt;
- a distinct expected terminal marker that ChatGPT is instructed to emit exactly;
- a bounded timeout.
The tool remains pending and returns the terminal workflow and captured response. Its default `progress` mode emits keepalive notifications; `waitMode: token_saver` stays silent. The browser send and response capture are separate: once `send_confirmed` is durable, capture can be restarted safely because it never composes or clicks. If the facade remains connected long enough to return a wait error, that error includes the workflow ID for one `await_workflow` reattachment; a detached start tool is the reliable choice when caller exit is expected.
## Current-host-owned convergence
When the current Codex, ZCode, or Claude Code task must remain side A, keep that task (or ZCode [Goal](https://zcode.z.ai/en/docs/goal)) as the implementation owner. Freeze the stable outcome and ordered acceptance criteria, then call `ego_review_candidate_and_wait` after each candidate. This is a fallback, not the durable default for an unattended multi-cycle Codex request: an exited current-host task cannot submit its next candidate. Put mutable candidate identity such as an exact commit SHA in the candidate summary and review packet, not in the frozen target, so a corrective cycle does not silently change the contract. Finalize the packet before giving each exact candidate call one stable, non-secret `operationId`, then retain that ID until the result is recovered. Reissuing byte-identical arguments with that same ID rediscovers the original workflow after a lost tool result; reusing it with changed input fails closed. Ego Chat derives stable unique markers from that identity, redacts high-confidence secret signatures, verifies the exact composer contents, and repairs then verifies the strongest-model policy before every Send.
A candidate review packet admits up to 524,288 UTF-8 bytes, while the complete generated browser prompt is limited to 196,608 UTF-8 bytes. This exceeds the old 28,000-character and 65,536-byte admission ceilings without allowing an oversized browser transaction. When the assembled prompt exceeds its smaller transport budget, Ego Chat deterministically compacts its largest fields, includes digests and a transport note, and sends the bounded review automatically. A compacted review cannot settle the target by itself: it continues the loop and asks the next Codex cycle for a smaller evidence packet or exact accessible revision references. The driver inserts the assembled prompt into ChatGPT's rich editor in one bounded DOM input transaction, then verifies the exact canonical digest before and immediately before Send. For an independently accessible repository or pull request, prefer a compact evidence manifest: canonical URL, exact base and head revisions, changed-file inventory, critical security or correctness excerpts, deterministic tests and hosted checks, and unresolved risks. Use self-contained relevant hunks when the reviewer cannot access the source. Ego Chat does not publish source or split one candidate into independently settleable fragments.
If ChatGPT completed the exact marked user/assistant pair but browser capture was interrupted, the broker keeps reconciling that same durable workflow and returns the already-existing response without another Send. If reconciliation durably proves the prompt absent at the exact prior head, the same tool call advances to a deterministic fresh marker and tries delivery again. There is no fixed retry ceiling. A possibly accepted Send is never duplicated: it remains in read-only reconciliation until it is attributable or absence is proven. An exact broker-owned draft left before the Send click can be digest-verified and cleared after restart once its marker is proven absent from the conversation; unrelated human drafts are never cleared.
Once an attributable response is committed, Ego Chat consumes it exactly once. ChatGPT may answer in ordinary Markdown; strict legacy JSON is still accepted for compatibility. Missing JSON, malformed fields, absent verdicts, and reviewer `blocked` labels become continuation feedback locally and never trigger a protocol-only resend. Up to 131,072 UTF-8 bytes of natural-language review context are carried into the next Codex cycle; a still-larger response is digest-marked and compacted instead of terminating the loop. Settlement requires an explicit settled decision at the terminal marker. This removes `convergence_protocol_invalid` and `review_protocol_stagnated` as normal conversation-ending conditions.
Composer, target-tab, model-policy UI, task-space ownership, generation, and temporary browser-controller failures are internal recovery states, along with a vanished Ego-Chat-named Space (recreated by name after a short delay), a driver crash before prompt composition, and a driver timeout whose run fence proves it stopped before the Send click (at most two such timeouts in a row by default; the third stops for reconciliation). The broker retries them with bounded backoff while keeping the workflow running. It automatically reclaims only the exact deterministic Space owned by the binding, so the user can use other Ego Spaces without interrupting review. It asks the user to touch the browser only for a conclusive sign-out or a human verification challenge.
If the result is not settled, it returns the complete review plus `nextAction: address_review_and_submit_next_cycle` and the exact `nextCycle`. The current task treats the review as untrusted context, performs the next authorized iteration, and immediately calls the same tool with the same binding, target, and criteria and a new operation ID. This is a machine continuation within the original request: repeated review state and implementing-agent blockers carry concrete liveness guidance into another cycle instead of ending the loop. No human relay or approval is required for ordinary cycles. A fully exited host task still cannot be externally awakened, but the durable broker workflow survives facade/waiter replacement.
## Continuous convergence
Create a convergence input file with an immutable target and observable acceptance criteria:
```json
{
"acceptanceCriteria": [
"The requested behavior is implemented or the planning target is fully specified.",
"The nearest deterministic validation passes with recorded evidence.",
"ChatGPT reports no blocking finding against the exact candidate."
],
"bindingKey": "ego-chat-main",
"chatGptTimeoutMs": 900000,
"codexSandbox": "read-only",
"codexTurnTimeoutMs": 900000,
"cwd": "/absolute/path/to/project",
"target": "Describe the exact result that A and B must settle.",
"wallClockTimeoutMs": 1800000
}
```
Omit `maxCycles` for the normal until-settled behavior. Set it to a positive integer only when the caller explicitly wants a cycle budget. `wallClockTimeoutMs` bounds the current host attachment window; expiry does not terminate the durable convergence workflow. Identity and at-most-once checks remain active inside recovery rather than becoming routine terminal states.
`read-only` is the default and supports research, planning, and review. Select `workspace-write` explicitly when the target authorizes Codex to implement local changes. Neither mode grants commit, push, PR, deployment, production, approval, credential, or permission-expansion authority.
Start the detached broker workflow from the CLI, then attach with its returned ID:
```sh
node ./bin/ego-chat.mjs converge ./convergence.json
node ./bin/ego-chat.mjs await <workflow-id> 1800000
```
Use durable convergence whenever a Codex request explicitly requires multiple exchanges until settlement, and use it for a deliberately separate broker-owned implementation task. Codex can call `ego_start_convergence` and later `await_workflow`, or call `ego_converge_until_settled` with supervised progress or an explicitly silent `waitMode: token_saver`. Closing the MCP facade only detaches that waiter; the daemon keeps alternating the dedicated Codex task and the same ChatGPT conversation for an attachment window of up to eight hours. The broker requires observable workspace-capable App Server activity before it accepts a Codex candidate for external review. It accumulates that evidence and the no-inspection retry count across correction and recovered turns in the same cycle, so a final structured-envelope turn cannot lose tools, edits, or validation already observed earlier in that cycle and a broker restart cannot reset its liveness threshold. Every continuation atomically retires the consumed source turn and records the exact pending correction, inspection, or recovery action before another turn starts; restart therefore cannot double-count or recover an already-consumed result. A completed recovered turn is stored as an exact private pending-result receipt in the same transition that resets the consecutive streak and merges its activity. Restart consumes that receipt before constructing, connecting, or resuming an App Server client. A valid candidate reaches ChatGPT without the old thread and may settle without App Server setup; if candidate correction, workspace inspection, or later review requires more Codex work, a durable rotation marker starts a fresh thread before the next turn and remains authoritative across restart. Candidate capture or continuation clears the receipt atomically. If three turns in one cycle still have no workspace activity, the broker atomically captures a bounded blocked liveness candidate and its counters for ChatGPT recovery guidance, then carries that feedback into the next Codex cycle instead of retrying side A invisibly forever. If App Server exits during setup or any accepted turn, the broker determines accepted-turn recovery from durable state before connecting, so connect, resume, and result-inspection failures all count toward the same sequence while initial setup failures remain separate. Every durably completed accepted turn resets the consecutive streak before any correction or inspection continuation, without resetting the cumulative recovery metric. The eighth non-completed result is itself captured as the checkpoint's first durable result transition. After ChatGPT responds, the broker durably rotates away from the possibly stuck thread and starts the next cycle on a new generation. There is no fixed overall recovery or convergence ceiling. A Codex `blocked` result is sent to ChatGPT as review evidence rather than terminalizing convergence.
Each cycle is bound as follows:
1. Codex returns a schema-constrained candidate and evidence for every criterion; unresolved blockers remain explicit review evidence.
2. The broker redacts high-confidence secret signatures, creates unique turn and terminal markers, selects the strongest available model with maximum thinking, and sends through the reserved canonical conversation.
3. ChatGPT returns ordinary review prose and a simple settled-or-continue verdict; strict legacy envelopes remain accepted.
4. Settlement is accepted only for an explicit settled decision with no implementing-agent blocker. Otherwise the complete review enters the same Codex task as untrusted context for the next cycle.
The broker has no implicit cycle ceiling. Repeated candidate/review state adds a liveness instruction and continues. Browser/model-policy/transient UI failures retry; App Server exits reconnect; stable pre-Send assistant-head advances re-anchor; potentially accepted Sends stay in read-only reconciliation; proven absences get a uniquely marked new attempt. A running ChatGPT child is reattached through renewable wait windows, restart reconciliation can itself survive another broker restart, and completed cycle bodies are compacted before the next candidate is persisted. Only conclusive authentication/CAPTCHA, explicit caller budgets, unrecoverable durable corruption, or authority genuinely required outside review need to stop useful progress. App Server diagnostics retain only bounded identity, exit, signal, status, and digest fields; raw stderr is not stored in workflow state.
The original live two-cycle ChatGPT qualification remains the browser baseline. The current deterministic suite additionally covers unbounded protocol-free continuation, bounded historical cycle storage, repeated-state progress, App Server setup and accepted-turn reconnection, renewable child-review waits, exact task-space reclaim, transient model-policy recovery, repeated broker restart during reconciliation, ambiguous-delivery reconciliation, oversized-packet admission and compaction, and exact-owned unsent-draft cleanup. See [CONTINUITY.md](https://github.com/xicv/ego-chat/blob/main/CONTINUITY.md) for the contract and evidence boundary.
### Durable task and runner spine
The daemon now constructs a separate durable task spine beside the existing browser workflow store. The local `createDurableTaskSpine` entry point can also be used without the daemon or ChatGPT. Its pure reducer owns logical conversations, tasks, activities, exact base/head pull-request artifacts, capability-checked runner registrations and fenced leases, revision-bound approvals, adapter-bound effect reservations, bounded JSON inputs, and ordered cursor-addressed conversation events. The existing Ego Browser and Codex App Server workflow paths do not call the new task transitions; the broker exposes the spine through a narrow compatibility boundary for later slices.
This first slice has fake remote, runner, and effect adapters only. It cannot write to GitHub, merge, deploy, access production, retrieve credentials, or grant real-world authority. See [`docs/durable-task-runner-spine.md`](docs/durable-task-runner-spine.md) for the invariants, effect reconciliation protocol, local API example, and explicit limitations.
### Ego Monitor
The package and portable crate also expose the first Ego Monitor slice: a deterministic, per-user macOS supervisor for one already-existing durable Ego Chat workflow. Run `ego-chat setup` once to materialize the embedded runtime before using the installed `ego-monitor` binary. It can observe broker, workflow, power, and storage state; start the broker only after its canonical lease and IPC endpoint both prove it dead; reattach the configured workflow ID; and request the broker's observation-only exact-workflow reconciliation check. That dedicated broker capability only validates durable binding/workflow recovery evidence and returns bounded phase/status metadata: it cannot call the Ego Browser adapter, spawn a browser child, mutate broker state, retry Send, or create a workflow. The monitor has no browser driver, prompt, conversation selection, model selection, LLM, or network surface. In safe mode a pre-Send stall notifies once it has lasted 30 minutes, a stagnant or looping semantic classification notifies once per incident, and any accepted human-required notification repeats (doubling from 60 minutes up to a 4-hour cap) while the same incident stays open; shadow mode never notifies.
Start in observation-only mode first:
```sh
ego-monitor start --workflow <durable-workflow-uuid> --mode shadow --power-policy allow-sleep --json
ego-monitor status --json
ego-monitor doctor --json
ego-monitor incidents --limit 50 --json
ego-monitor stop --json
```
Safe recovery additionally requires the existing binding key. `keep-awake-on-ac` is an explicit alternative power policy; it requests only an idle-sleep assertion while the workflow is active and the Mac reports AC power. It cannot prevent lid-close, explicit, low-battery, thermal, or other forced sleep.
```sh
ego-monitor start --workflow <durable-workflow-uuid> --binding-key <existing-binding> --mode safe --power-policy keep-awake-on-ac --json
```
`start` and `stop` manage only `gui/<uid>/com.xicv.ego-chat.ego-monitor` through a user LaunchAgent, after first retiring the per-user service and plist left under the monitor's previous name (see the upgrade section of `docs/ego-monitor.md`). They never use root or a system domain. Do not run `start` in automated tests; the focused suite injects a fake launchctl runner. See [`docs/ego-monitor.md`](docs/ego-monitor.md) for the state/action contract, stable JSON/exit semantics, power caveats, security boundary, and runbook.
`doctor.dependenciesHealthy` is separate from `readiness`: a stopped monitor can have healthy
dependencies but `readiness.ready: false`. Status/doctor expose whether the configured service and
lease are active, a real observation is fresh, and safe recovery is enabled. Persistent unavailable
broker IPC reaches an alert-only boundary after five minutes without restarting an ambiguously
live owner. All notification paths persist command acceptance separately from incident/recovery
state and retry failed submissions with bounded backoff, including across restart and incident
changes. Command acceptance does not prove human receipt. Semantic alerts, independent observer
freshness escalation, context capsules, real-provider successor qualification and an 8–12-hour
soak remain future work; see the [monitor behavior and limits](docs/ego-monitor.md).
## Judgments (opt-in, advisory)
Ego Chat can ask TypeSafe's Jev model for a typed, calibrated second opinion on
two things its regexes read badly: whether a ChatGPT review keeps naming the
same blockers, and what an unrecognized provider status banner actually says.
It is **off by default**, and nothing runs unless the data directory enables it
**and** a key is present.
A judgment may only **alert, stop or pause**. It can never start a Send, resend
a turn, settle a convergence, promote a successor or change a binding, and it
never runs on the Send path — only between convergence cycles once a review has
been consumed, and during pending-capture observations. Every existing
heuristic stays the fallback: provider disabled, timeout, provider error,
invalid answer or low confidence all behave exactly as they do today, and a
judgment failure never changes a cycle's outcome. The deterministic stagnation
stop (`convergence_stagnant`, `stagnationLimit`, no model involved) remains the
primary defence against a looping run.
### Exactly what leaves this machine
Judgments are the only Ego Chat feature that sends anything to a third party
other than ChatGPT. When enabled, one HTTPS request per judgment goes to
`https://api.typesafe.ai/v1/systemone` carrying only:
- **status labels** — up to four provider status strings of at most 160
characters each, exactly the ones `readProviderStatusLabels` already captured
from `[role="status"]`, `[role="alert"]` and visible buttons;
- **acceptance criteria** — the `{ id, text }` criteria of the convergence
contract, which the caller wrote;
- **review text** — ChatGPT's own review prose after local secret redaction,
stripped of Ego Chat's own per-cycle terminal marker and bounded to 12,000
characters, plus the previous cycle's review under the same bound.
Never sent: the Codex prompt, the assembled ChatGPT review prompt, candidate
file bodies or review packets, blobs, conversation URLs, binding keys, workflow
identifiers, your API key in any logged form, or anything else. TypeSafe states
that it does not train on customer data; zero data retention is an enterprise
option only, so treat everything above as leaving the Mac. Nothing is sent
while judgments are disabled.
The durable record keeps **digests, never the state text**: each workflow gains
a public `judgments: { counts, recent }` ledger of at most eight entries
(`{ at, point, cycle?, model, latencyMs, usage, questionDigest, stateDigest,
answers, effect, reason }`), written by a `judgment.recorded` transition, plus
`judgmentStagnation: { lastSameBlockers, limit, streak }`. The one exception is
`private.judgmentContext.previousReviewText`, the bounded redacted review text
carried forward so the next cycle needs no blob re-read.
### Enabling it
Write `judgments.json` in the data directory (`~/Library/Application Support/Ego
Chat` by default). The key may come from the `TYPESAFE_API_KEY` environment
variable instead, which takes precedence; if you put it in the file, the file
must not be readable by group or other (`chmod 600`), or Ego Chat treats the
whole configuration as disabled with `reason: "config_insecure"`.
```json
{
"enabled": true,
"apiKey": "ts-...",
"model": "jev-1.13.0",
"timeoutMs": 5000,
"maxRetries": 1,
"stagnationLimit": 3,
"stagnationPolicy": "alert",
"humanQuestionPolicy": "alert",
"outOfScopePolicy": "alert",
"terminalPolicy": "alert",
"sameBlockersThreshold": 0.5,
"noulThreshold": 0.8,
"terminalConfidenceThreshold": 0.9
}
```
| Field | Default | Meaning |
| --- | --- | --- |
| `enabled` | `false` | Master switch. Everything below is inert while this is false. |
| `apiKey` | none | Optional; `TYPESAFE_API_KEY` wins. At least 8 characters. Requires mode 600. |
| `model` | `jev-1.13.0` | Pinned, not the `jev-latest` alias. |
| `timeoutMs` | `5000` | Per attempt, 1000–30000. |
| `maxRetries` | `1` | 0–2 retries after the first attempt. |
| `stagnationLimit` | `3` | Consecutive same-blockers reviews before the stagnation effect. 1–20. |
| `stagnationPolicy` | `alert` | `alert` notifies once at the limit and then every `stagnationLimit` further cycles; `stop` ends the run as `human_required` `convergence_stagnant`. |
| `humanQuestionPolicy` | `alert` | `alert` notifies; `pause` ends the run as `human_required` `review_asks_human_question`. |
| `outOfScopePolicy` | `alert` | `alert` notifies; `off` records the judgment without notifying. |
| `terminalPolicy` | `alert` | `alert` notifies `judgment_terminal_status` for a judged exhaustion or quota banner; `off` records it only. There is no pause. |
| `sameBlockersThreshold` | `0.5` | Noul at or above which a review counts as repeating the previous blockers. |
| `noulThreshold` | `0.8` | Noul at or above which the human-question and out-of-scope conditions hold. |
| `terminalConfidenceThreshold` | `0.9` | Choice confidence at or above which a terminal classification is actionable. Exhaustion additionally needs its own new-chat Noul at 0.5 or above. |
Alerts arrive through the same local sink as workflow attention alerts, with
`kind: "judgment"` and codes `judgment_stagnation`,
`judgment_review_asks_human`, `judgment_review_out_of_scope` and
`judgment_terminal_status`. `broker.status.judgments` reports the key-free
configuration, the evaluated/unavailable/invalid counters, input tokens and the
estimated cost at $0.042 per million input tokens — about two cents for a
thirty-cycle night. A judged stagnation streak that has reached its limit also
appears as the Ego Monitor's `judgment_same_blockers_<streak>` reason on a
`stagnant` classification it had already reached on its own.
Phase 1 covers review consumption and unrecognized status labels only. Effort
and model-menu semantics, answering-model ranking, Codex candidate claims and
host-facing judgment tools remain future work; see
[`docs/superpowers/plans/2026-09-21-judgment-phase1.md`](docs/superpowers/plans/2026-09-21-judgment-phase1.md)
and the design note in `docs/superpowers/research/`.
## Validation
Run the focused suite. When editing, pass only the modified or newly created paths to ESLint; do not run a formatter across the project:
```sh
npm test
npm run test:ego-monitor
npm run test:long
npx eslint <modified-files...>
npm run audit
```
Validate the Rust distribution and skill separately:
```sh
cargo test
cargo fmt --check
cargo clippy --all-targets -- -D warnings
python3 /path/to/skill-creator/scripts/quick_validate.py ./skills/ego-chat
cargo package --list
```
The installed-version integration gates are:
```sh
npm run gate0:codex-mcp
npm run gate0:app-server
```
`npm run gate0:ego` can send live ChatGPT turns. It requires `EGO_CHAT_GATE0_CONFIRM_SEND=1`. Once the binding already contains messages, it also requires `EGO_CHAT_GATE0_ALLOW_REPEAT=1`, preventing accidental repeat runs.
**Exhaustion qualification.** Confirming live ChatGPT terminal-banner detection needs a throwaway conversation on a test binding: opt in to convergence with `conversationContinuation: "same_project_on_exhaustion"` and let it run until the provider's own length-limit banner appears, then confirm the ledger recorded `chatgpt_conversation_exhausted` and that a successor was prepared. The procedure was run once; the record below is the current qualification state.
#### Exhaustion qualification run (2026-09-11, runtime 0.2.29)
Binding `claude-code-live-check` (plain chat, `gpt-6-pro` route at 6 Pro). Filled with 55 sequential 150 KB exchanges (8.25 MB, roughly 2 million tokens of filler; each reply "OK" plus the terminal marker), then a `maxCycles: 3` convergence with `same_project_on_exhaustion` on the same chat, Ego Monitor attached in safe mode through `supervision.monitorCommand`.
- **No length banner appeared.** Every exchange and all three review Sends succeeded; reply latency stayed flat (about 50 s on `gpt-6-pro`, 27 s after the switch below), the page showed no `role=status`/`role=alert` text and an enabled composer, and the DOM rendered only the last six turns. On this account and route the web client truncates history rather than refusing, so `chatgpt_conversation_exhausted` and the automatic successor could not be exercised live; both remain harness-qualified only.
- **The answering model changed silently at exchange 45**: `responseModelSlug` went from `gpt-6-pro` to `gpt-5-6-thinking` while the pre-send policy check kept passing (the effort slider lost its Pro level and "Extra High" became its maximum). The pre-send policy proof does not cover the answering model; the slug is the only evidence, and it is now recorded on every exchange.
- **Alerting and monitor qualified live**: the convergence ended `human_required` (`convergence_cycle_limit_reached`); the broker's local alert sink dispatched one macOS notification (`channels: [{ macos: accepted }]`, receipt in `alerts.jsonl`, `broker.status.alerts.dispatched: 1`), and the safe-mode Ego Monitor classified `human_required_other`, ran `notify_user` successfully, and scheduled its first repeat an hour later.
- **A store bug surfaced under this volume**: after exchange 39 a result blob was quarantined by a compaction that ran between `putBlob` and the event that references it, and the next exchange start failed with `corrupt_result_blob_inventory`; the blob was restored from the record's inline text and the race is fixed by pinning freshly written blobs until their event is persisted.
Composer-surface detection (browser contract 30) is harness-qualified only as well. The usage-limit sentences and call-to-action labels come from ChatGPT's client bundle and the wall sentence from its server, but no exhausted chat has been observed live, so these remain unconfirmed: the banner's DOM, whether its text is one element as the label extraction assumes, the exact wording of the usage-limit variants (matched by their distinguishing phrase), whether the calls to action are buttons or links, and what the editor shows at the wall. A diagnostic snapshot of `#thread-bottom` taken on a real wall is the cheapest way to settle them.
See [GATE0.md](https://github.com/xicv/ego-chat/blob/main/GATE0.md) for the original component qualification, [CONTINUITY.md](https://github.com/xicv/ego-chat/blob/main/CONTINUITY.md) for the convergence contract and evidence, and [RESEARCH.md](https://github.com/xicv/ego-chat/blob/main/RESEARCH.md) for the research and architectural decision record.
## Wait expiry and early delivery tracking
`await_workflow` bounds one attachment, not the lifetime of the workflow. If its window expires while the workflow is running, MCP returns a **non-error** result with `waitStatus: "pending"`, `nextAction: "await_workflow"`, and `continuation: { tool, arguments }`. Call that exact continuation on the same workflow; do not submit another request or reconcile a still-running workflow. If completion raced with expiry, the final snapshot returns the terminal result instead. Snapshot/transport failures remain errors with the durable workflow ID. Older initial `*_and_wait` calls may still return `wait_timeout` with `details.workflowId`; recover through `await_workflow`.
Token-Saver still performs no periodic status reads or progress notifications. A wait expiry adds one bounded final status read, with no model call and no new browser work. This does not wake a host task that has already exited; use broker-owned convergence for an until-settled job.
After Send, `workflow.delivery` exposes confirmation time and a permanent `canonicalUrl` as soon as the exact prompt, target, and task-space evidence prove it—even while ChatGPT is generating. Until then the public URL is null and `locatorState` is `pending`. Temporary `/c/WEB:...` URLs are recoverable confirmed-Send evidence, but cannot be adopted, advertised as permanent URLs, or committed as a final conversation head. Promotion to a permanent URL is pinned durably once; a different permanent conversation is rejected. The binding stays unbound and its head unchanged until the final attributable response is captured. Consumers should save the workflow ID immediately, then the verified delivery permalink when available.
`capturePending.observedAt` records the pending-state transition. `captureObservation.observedAt` records the latest successful browser observation, durably refreshed at most once per minute while the reason is unchanged. An observation-only update does not advance `updatedAt`, the semantic checkpoint, or its useful-progress deadline. Status describes generation as **last observed**, not guaranteed current progress. None of these signals proves an implementation or MR exists.
## Release verification
v0.2.40 fixes how Ego Chat treats a ChatGPT page that drew the bound answer blank. A page whose own scripts failed to load draws every answer as an empty message and never recovers by itself. This was seen live right after an Ego Lite restart, when `ego_verify_conversation` reported `conversation_head_changed` on an unchanged chat. A Send or a verify now reloads such a page once and reads the head again. The reload is proven like the Pro only reload but has its own one-per-run budget. An answer still blank stops as the retryable `conversation_head_unrendered`, never as a changed head, so nothing re-anchors to a blank answer, and re-anchoring refuses one. Verify's driver budget is now 120 seconds, and the CLI and MCP requests wait 125 seconds. It is not yet qualified as interruption-free overnight operation. The browser contract changed (revision 34). Upgrade through an idle, child-drained broker handoff, then restart connected hosts together. Never stop an active confirmed-Send workflow just to upgrade:
```sh
cargo install --registry crates-io --version 0.2.40 --locked --force ego-chat
ego-chat setup
# For ZCode users:
ego-chat setup-zcode
ego-chat doctor
ego-chat doctor-zcode
```
Run setup and doctor only for the hosts you use. Inspect any conflicting skill or MCP entry before explicitly using setup's `--force`; unrelated customization must be preserved. Fully quit and reopen connected hosts after setup. A stale facade may still read status, but new mutations must wait for a matching runtime rather than retrying under `restart_required`.
The crate carries the MIT license and canonical repository metadata needed for publication. Before every release, inspect the exact archive with `cargo package --list` and run `cargo publish --dry-run --locked`. A crates.io version is permanent and cannot be overwritten, so publishing always requires explicit authorization for that exact version.
## Not yet supported
- Direct MCP hosting inside ChatGPT.app; a ChatGPT.app conversation can only be continued by adopting its private `/c/` URL from Codex, ZCode, or Claude Code.
- Full historical transcript import into the local coding task during URL adoption; only the latest stable assistant tail is returned, while the browser conversation retains its full history.
- ChatGPT-first initiation through a private plugin and Secure MCP Tunnel.
- Automatic GitHub push, repository upload, or attachment transfer.
- Automatic attachment/context-capsule construction beyond the bounded, secret-scanned implementing-agent review packet.
- Externally waking a Codex desktop task after its MCP adoption waiter has exited; adoption continues the same task while `ego_adopt_conversation_and_wait` remains open, while convergence owns a dedicated App Server thread.
- Externally waking or resuming a ZCode task after ZCode exits; ZCode-owned loops remain continuous while their current task or Goal is active.
- Externally waking a Claude Code session after its turn ends; Claude Code-owned loops remain continuous while the tool call, or the background task Claude Code moves it to, is still pending.
- The Claude.app chat surface; only Claude Code and the Claude.app Code tab are configured, because the chat surface cannot load the skill from disk and has no documented long-call timeout.
- A Claude-owned implementing agent for detached convergence; broker-owned convergence still uses the Codex App Server.
- True simultaneous CDP automation between Ego Chat and unrelated external `ego-browser` clients. Ego Chat serializes its own children, but cannot serialize another process outside its authoritative broker while upstream per-Space CDP isolation remains unresolved.
- Blind replay of a possibly accepted browser Send. Ego Chat instead keeps the original workflow in read-only reconciliation and retries delivery only after exact absence proof.
- Automatic bypass of CAPTCHA or a conclusive sign-out. These are the intended browser-level human boundaries. Unknown or hydrating model/UI state retries without downgrading.
Those are later phases. Consequential repository and remote operations remain outside the browser reviewer's authority.