Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a58833e
Surface git stderr in repository helper failures
hamzamerzic Jul 28, 2026
cc4ccbd
Wait for readiness before bootstrap app jobs
hamzamerzic Jul 28, 2026
93d2fea
Recover every authenticated turn after planned restarts
hamzamerzic Jul 28, 2026
c01ebea
Forward Codex web-search sources to chat
hamzamerzic Jul 28, 2026
c7661aa
Show native Codex image views in chat
hamzamerzic Jul 28, 2026
2c28ad4
Expose live Codex helper lifecycle in chat
hamzamerzic Jul 28, 2026
71370c7
Report Codex usage costs and support Claude delegation
hamzamerzic Jul 28, 2026
797bbfe
Add an accessible copy action to code blocks
hamzamerzic Jul 28, 2026
1344876
Close all other tabs in a pane
hamzamerzic Jul 28, 2026
ae5cb0f
Integrate reviewed reflection and shell refinements
hamzamerzic Jul 28, 2026
34f1e6f
feat: add portable secure background job isolation
hamzamerzic Jul 28, 2026
2c2e187
Keep Standard chat geometry stable in Builder mode
hamzamerzic Jul 28, 2026
dcad1dc
Stabilize growing Q&A answers across keyboard changes
hamzamerzic Jul 28, 2026
042f02a
Keep the chat composer clear of device safe areas
hamzamerzic Jul 28, 2026
8528676
Give workspace tab titles more room
hamzamerzic Jul 28, 2026
76b3400
Require a hold before touch-dragging workspace tabs
hamzamerzic Jul 28, 2026
93e7173
Add a slash-command menu with fuzzy search to the composer
hamzamerzic Jul 28, 2026
283e916
Recover replies before clearing wedged runs
hamzamerzic Jul 27, 2026
b01a912
Improve chat switching and image previews
hamzamerzic Jul 26, 2026
3ac8616
Reserve chat images from stored dimensions
hamzamerzic Jul 27, 2026
9264ead
Skip unchanged chat runtime cache persistence
hamzamerzic Jul 27, 2026
ae08c2e
Add app sharing from the drawer
hamzamerzic Jul 26, 2026
71f0067
Add an opt-in field performance probe, and stop touch input measuring…
hamzamerzic Jul 26, 2026
b092d60
Make screenshot verification exact and cold-start safe
hamzamerzic Jul 27, 2026
d068192
Reconcile landed contributions by provenance
hamzamerzic Jul 28, 2026
3f4b460
Keep summary-owned chat names timely and stable
hamzamerzic Jul 28, 2026
aea5708
Pin the runtime base required by secure job isolation
hamzamerzic Jul 28, 2026
8aee257
Keep effort selection visible while saving
hamzamerzic Jul 28, 2026
a8d950c
Isolate validation builds from the live shell
hamzamerzic Jul 28, 2026
89ac848
Reveal live app builds in the workspace
hamzamerzic Jul 28, 2026
9b82ea2
Preserve structured tool receipts in excerpts
hamzamerzic Jul 28, 2026
6366848
Fix notification time and history contracts
hamzamerzic Jul 28, 2026
7faa35d
Refresh resolved platform status in Settings
hamzamerzic Jul 28, 2026
0e5cf0b
Keep deferred steer queue tray dismissed
hamzamerzic Jul 28, 2026
2d5d8c6
Re-measure Q&A answers after width settles
hamzamerzic Jul 28, 2026
e43e44e
Keep the steer contract test aligned with visible queue rows
hamzamerzic Jul 28, 2026
f1bde75
Keep legacy Standard chat painted on first render
hamzamerzic Jul 28, 2026
3effe13
Target the painted chat in retained-surface browser tests
hamzamerzic Jul 28, 2026
0323922
Align browser tests with retained chat and touch-hold contracts
hamzamerzic Jul 28, 2026
6c64e8a
Test the direct custom-answer question surface
hamzamerzic Jul 28, 2026
b47e1e8
Test independent Standard and Builder motion owners
hamzamerzic Jul 28, 2026
6b18217
Scope browser contracts to the painted chat owner
hamzamerzic Jul 28, 2026
f7a56e5
Keep backend CI independent of shell build artifacts
hamzamerzic Jul 28, 2026
e47c2be
Seed the active chat in cache smoke coverage
hamzamerzic Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ apps/
node_modules/
frontend/node_modules/
frontend/dist/
frontend/.watch.lock
__pycache__/
*.pyc
.venv/
111 changes: 74 additions & 37 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,11 @@ Note: there is no `routes/ai.py` and no `POST /api/ai`. An older mini-app AI pro
Host-mediated device/browser access uses the versioned capability broker; see
[`CAPABILITIES.md`](CAPABILITIES.md) for the manifest, app API, wire protocol,
provider contract, lifecycle rules, and trust-tier escape hatches.
Server-side app jobs have a separate two-tier model: ordinary reviewed scripts
retain the Möbius process authority, while `background_agent` jobs run through
one reviewed data contract and the strongest secure executor available on the
host. See [`BACKGROUND_JOBS.md`](BACKGROUND_JOBS.md) for the contract,
Bubblewrap/Landlock selection, history, and verification strategy.

| Tier | Boundary and capability | UX / standalone consequence |
|---|---|---|
Expand Down Expand Up @@ -503,7 +508,7 @@ automatically armed by installing Möbius.

## Chat scroll + steer contract

**Owner-authoritative contract — v1.10 (2026-07-26).** This section is the
**Owner-authoritative contract — v1.11 (2026-07-27).** This section is the
canonical source of truth for how a chat scrolls and steers. When implementation,
comments, and this contract disagree, the implementation/comments are the bug:
fix behavior to match this contract. If a real case is unspecified or the desired
Expand Down Expand Up @@ -612,6 +617,14 @@ and attaches their rule ids to new diagnostic chats. The Playwright lock-in spec
geometry, while a disclosure first settles any preceding gesture and then owns
layout caused by its own expansion/collapse. A bounded dead-man remains the final
escape hatch for any interrupted no-scroll gesture.
A marked Q&A custom-answer field is the deliberate exception to "ordinary
typing cannot scroll": changing its value can grow the field and cause the
browser to move the transcript to keep the native caret visible. From
`beforeinput` through one complete rendered frame, that mutation uses the
same reader-ownership gate as a possible scroll. If no scroll lands, layout
resumes immediately after that frame; if one does, the ordinary quiet-settle
path records the resulting hold. The controller must not restore a stale
anchor between those two outcomes.
- **R5a — Attention nudges reveal the usable tail.** Tapping an offscreen question
or paused-turn nudge is an explicit one-shot reading action: it lands at the
physical tail, including the list's composer-clearance padding, so the card's
Expand All @@ -638,6 +651,14 @@ and attaches their rule ids to new diagnostic chats. The Playwright lock-in spec
persistence. A failed answer
keeps that settled reading anchor for the retryable card rather than manufacturing
follow intent again.
While the custom-answer field is focused, a visual-viewport change may rebase
an ordinary `ANCHOR_AT` hold to the browser's current caret-visible position
instead of reapplying its stale pre-edit offset. `PIN_USER_MSG`,
`HOLD_RESERVED_TAIL`, `FOLLOW_BOTTOM`, and the transient question-submission
overlay retain their existing stronger rules. The editing lifecycle remains
active through keyboard-closing focusout until the full pane height returns,
preventing alternating browser/controller corrections without reserving any
extra conversation tail space.
The source handoff
preserves the question, its answer, and every pre/post-answer thinking, tool, and
text block in event order, without hiding, duplicating, or reordering them. Only a
Expand Down Expand Up @@ -672,6 +693,7 @@ path means routing it through the same entries rather than inventing another rul
| Chat exits/backgrounds/returns | any | `ANCHOR_AT` | Restore exact saved anchor |
| In-process question is answered | any | transient `ANCHOR_AT` over the prior mode; same active assistant row | Hold exact visible anchor through same-viewport card reflow and resumed output |
| Viewport/keyboard changes after question submission | transient question anchor | pre-submit unanswered-card mode | Apply ordinary viewport behavior; answering adds no extra movement |
| Focused Q&A custom answer grows or its keyboard viewport changes | ordinary hold | current caret-visible `ANCHOR_AT` | Browser may reveal the caret once; controller rebases instead of snapping back. Pins, reserved-tail holds, follow, and submission overlay are unchanged |
| Live assistant row settles to the durable transcript | any | same mode and row identity | None (except R3's exact spacer handoff) |
| Offscreen question or paused-turn nudge tapped | any hold | `ANCHOR_AT` at physical tail | User-requested one-shot move; clears the overlaid composer |

Expand Down Expand Up @@ -744,47 +766,58 @@ completion or a particular scroll mode.

Automatic continuation reuses one durable run transition with separate
chat-local policies and cause validation. Provider-limit exits mark their exact
`ChatRun` as `parked` until the parsed reset time. A planned restart
creates a fresh nonce, stops and finalizes each exact live run, and parks it
due-now with that nonce. The platform process then publishes an intent and
restart request; it does not terminate itself on the normal path.
`ChatRun` as `parked` until the parsed reset time. A planned restart creates a
fresh nonce and, **before provider interruption**, stamps it onto every exact
live run in one writer transaction. Provider stops then run concurrently; clean
stops finalize and become due-now parks immediately, while a slow stop or failed
terminal transcript write keeps its exact nonce-stamped `running` row for boot
recovery. The platform process then publishes an intent and restart request; it
does not terminate itself on the normal path.

The frozen root-owned entrypoint poller validates and consumes the request,
records its one-shot nonce in `/data/.restart-ledger`, and only then terminates
pid 1. At the very start of the next entrypoint invocation, the ledger binds
that accepted nonce to the new `MOBIUS_BOOT_ID`. The app only continues a
restart park when the root-owned boot acknowledgement matches the nonce on the
latest exact DB run and the restart policy is on. The supervisor
attests the boot transition; the database owns run identity. An intent merely
written before a crash/OOM, an acknowledgement skipped by a failed handshake,
or an acknowledgement left across another boot authorizes nothing. Transcript
text is presentation, never restart-cause evidence.
latest exact DB run and the restart policy is on. At startup, the same
authorization converts matching stranded `running` rows into due restart parks
after finalizing their persisted partial transcript; this happens before the
writer starts and before the initial continuation sweep. The supervisor attests
the boot transition; the database owns run identity. An intent merely written
before a crash/OOM, an acknowledgement skipped by a failed handshake, or an
acknowledgement left across another boot authorizes nothing. Transcript text is
presentation, never restart-cause evidence.

| Event | Durable result | Boot/sweep result |
|-------|----------------|-------------------|
| Provider usage/rate limit | exact run `parked` until reset | notify; continue if the usage policy is on |
| Accepted planned restart, exact park + boot nonce match | exact run `parked`, reason `restart`, nonce, due now | continue immediately if the restart policy is on |
| Accepted planned restart, stop/finalize did not settle | exact latest run remains `running` with the authenticated nonce | finalize partials, convert to due restart park, then continue in the same pre-yield pass |
| Crash/OOM before supervisor acknowledgement | unacknowledged park or generic `running` evidence | resolve/reconcile to manual resumable interruption |
| Repeated/unrelated boot before claim | acknowledgement is retired by boot-id mismatch | manual resumable interruption |
| Policy off, unanswered question, app-owned run, or app-queued work | due park resolves without an automatic send | notify/manual owner action |
| Owner sends, switches provider, deletes the chat, or a newer run wins | old park is superseded by the existing latest-run fence | no stale continuation |
| Restart task creation fails after promotion | exact promoted rows roll back; restart park becomes `interrupted` | manual recovery; one-shot cause is not retried |

Eligibility is rechecked under the per-chat transition lock immediately before
promotion, and the global idle gate permits only one automatic turn at a time.
The provider still receives a synthetic user `continue`, but the durable row is
tagged `kind="auto_continuation"` with reason `restart` or `usage_limit`; the UI,
copy behavior, title selection, time context, compaction, provider-switch
handoff, chat-note summarization, and redacted chat logs treat it as a product
marker rather than owner speech.
promotion. Provider-limit retries are staggered one at a time; an authenticated
planned restart restores the exact set that was already concurrent, so its
eligible batch may start together. The provider still receives a synthetic user
`continue`, but the durable row is tagged `kind="auto_continuation"` with reason
`restart` or `usage_limit`; the UI, copy behavior, title selection, time
context, compaction, provider-switch handoff, chat-note summarization, and
redacted chat logs treat it as a product marker rather than owner speech.

The sweep is cheap: one indexed due-row query immediately at boot, on
`chat_run_finished`, and on a 60-second fallback, plus one bounded local ledger
read when due rows exist. It does not create per-chat workers or poll at a short
interval. Paid provider-limit continuation (`auto_resume_on_limit`) initially
defaults off; planned-restart continuation (`auto_resume_on_restart`) initially
defaults on. Each chat stores both choices independently, and changing either
choice seeds future chats without rewriting existing conversations.
`chat_run_finished`, and on a 60-second fallback. Startup captures the boot
authorization once and threads that exact value through reconciliation and the
pre-yield sweep, so a second ledger read cannot make the two phases disagree;
later sweeps perform one bounded local ledger read only when due restart rows
exist. It does not create per-chat workers or poll at a short interval. Paid
provider-limit continuation (`auto_resume_on_limit`) initially defaults off;
planned-restart continuation (`auto_resume_on_restart`) initially defaults on.
Each chat stores both choices independently, and changing either choice seeds
future chats without rewriting existing conversations.

### Tool output rendering

Expand Down Expand Up @@ -923,28 +956,29 @@ tab. A pane will instead store `activeTabKey` and compare it with `tabKey(tab)`.
hidden app-iframe LRU. This is the degenerate one-pane form of the target model.

`frontend/src/components/Shell/workspacePlacement.js` is the placement seam.
Producers issue an `open-item` request with `placement: 'beside-source'` and
`activation: 'background'`; they never name a tab strip, pane id, split
direction, or breakpoint. The flat resolver inserts a built app after its
source chat. A pane resolver should interpret the same request as: use the next
pane when one exists, create one when the viewport supports it, and fall back
to an adjacent background tab on narrow screens.
Producers issue an `open-item` request with `placement: 'beside-source'`; they
never name a tab strip, pane id, split direction, or breakpoint. Generic opens
use `background` / `foreground`. A committed build uses the internal
`live-preview` activation: it enters Builder, reveals the app in a companion
pane while keeping the chat focused on wider screens, and activates the app tab
on phones.

| Input | Confirmation | Current action |
| --- | --- | --- |
| `app_created {appId, chatId}` | Refetched row matches both ids | Apply one background `beside-source` request |
| `app_created` missing/mismatched ids | No matching live row | Ignore the placement request |
| Fresh app-list row with `chat_id` | App absent from the established session baseline | Apply the same request as reconnect fallback |
| `app_updated` | Live row exists | Refresh CTA/code and warm cache; never place again |
| `app_created {appId, chatId}` | Live row exists | Refresh lifecycle/list state |
| `app_preview_ready {appId, chatId}` | Refetched row matches the app and requesting chat | Apply one `live-preview` `beside-source` request |
| `app_preview_ready` missing/mismatched ids | No matching live row | Ignore the placement request |
| Fresh app-list row with `chat_id` | App absent from the established session baseline | Apply the same live-preview request as reconnect fallback |
| `app_updated` | Live row exists | Refresh CTA/code and live-swap the open frame |
| Store install or app without `chat_id` | No source-chat relationship | Drawer arrival only |
| Replayed/duplicate placement | Target app already open | Strict same-reference no-op |

Every automatic built-preview path passes through
`applyWorkspaceRequestsToFlatTabs`. Direct drawer/user tab opens remain
explicit foreground navigation and bypass automatic placement by design.
When the flat strip is at capacity, automatic placement protects the currently
visible tab as well as the new source-chat/app pair; background work must never
make the user's on-screen tab disappear from the strip.
`resolveWorkspaceRequests`. Direct drawer/user tab opens remain explicit
foreground navigation and bypass automatic placement by design. Generic
background work preserves every already-visible surface; a live preview is
intentionally visible and may activate an existing companion tab without
moving keyboard focus away from the building chat.

### Target pane model

Expand Down Expand Up @@ -1070,4 +1104,7 @@ cover it deterministically.
## See also

- **Build / test / run commands and the dev loop:** `CONTRIBUTING.md`. (The #1 deploy gotcha — a stale `/data/platform/frontend/dist` masking a fresh image — is covered under *Frontend serving priority* above.)
- **Secure server-side app jobs:** `BACKGROUND_JOBS.md` defines the
background-agent data contract, portable executor design, historical
rationale, and topology-level verification.
- **Subsystem deep-dives are inlined above** as their own sections: *Stop-chat contract*, *AskUserQuestion interception*, *Chat persistence — single-writer actor*, *Navigation back-stack + drawer model*, *Service worker + offline*, and *Mini-app manifest (mobius.json)*. (The chat-persistence v2 design + staged-rollout notes remain internal/gitignored — the as-built contract is the section above.)
Loading