Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5997e12
fix(runtime): stabilize session recovery and presentation
stack-muggle Aug 13, 2026
a4ea0a1
fix(runtime): stabilize session continuity across profiles
stack-muggle Aug 14, 2026
520390c
fix(codex): persist exact terminal lifecycle fences
stack-muggle Aug 15, 2026
dd95a56
fix(codex): keep oversized sessions on shared daemon
stack-muggle Aug 15, 2026
8166c6b
fix(web): reconcile completed process history
stack-muggle Aug 16, 2026
f149ce1
fix(codex): settle persisted plan projections
stack-muggle Aug 18, 2026
5a4c3fb
fix(web): reconcile Codex history after wrapper restart
stack-muggle Aug 17, 2026
ac6d812
fix(codex): stabilize active history projections
stack-muggle Aug 18, 2026
cfd286d
test(web): align terminal plan expectations
stack-muggle Aug 18, 2026
294f4c9
fix(codex): harden session recovery invariants
stack-muggle Aug 19, 2026
a110ee0
fix(codex): recover missed native user prompts
stack-muggle Aug 19, 2026
5aba317
fix(web): support long pastes on private HTTP
stack-muggle Aug 19, 2026
1442391
fix(runtime): close review recovery edge cases
stack-muggle Aug 19, 2026
e3ac46a
fix(runtime): address recovery review findings
stack-muggle Aug 20, 2026
37d1439
test(web): stabilize plan geometry regression
stack-muggle Aug 20, 2026
b4f9da5
fix(web): stabilize process and paste rendering
stack-muggle Aug 20, 2026
17b4005
fix(web): hide completion actions until turn finishes
stack-muggle Aug 20, 2026
d3c013c
fix(web): scope live ownership to active turns
stack-muggle Aug 26, 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
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ local `claude` or `codex` session through a WebSocket relay. Two independent lin
transport, never the caller's Origin. Uvicorn trusts forwarded transport
metadata only from loopback Caddy. Never put tokens in URLs or protocol
message bodies; logging redacts token/password fields.
- **Protocol version gate**: current wire protocol v34 is declared by
- **Protocol version gate**: current wire protocol v35 is declared by
`PROTOCOL_VERSION` in both `protocol.py` and `web/src/protocol.ts`.
`deserialize` hard-rejects a version mismatch, and
`_Base` is `extra="forbid"`, so ANY protocol change must be deployed to all
Expand Down Expand Up @@ -113,7 +113,12 @@ local `claude` or `codex` session through a WebSocket relay. Two independent lin
hello sends lightweight resident `Snapshot`s; reconnect cursors replay only
the bounded missing live tail. Source fingerprints invalidate appended pages,
and rollback explicitly invalidates both server and browser projections. These
reads never spawn/resume an engine or create a model turn.
reads never spawn/resume an engine or create a model turn. Codex
`History.terminal_fences` is a separate bounded lifecycle projection: only a
real app-server terminal or a source-validated rollout marker may enter it;
local synthetic failures may not. The browser applies a fence only to its
exact native turn identity and never changes completion receipts or guesses
from the last open row.
- **Token-aware residency**: resuming an evicted Claude SDK session may rebuild
a cold prompt cache, so it only happens on first spawn / re-focus after
eviction; raising the cap trades RAM for fewer cold re-sends. Codex context is
Expand All @@ -132,8 +137,9 @@ local `claude` or `codex` session through a WebSocket relay. Two independent lin
- `cc_remote/log.py` — JSON logging with token redaction; use `logger("...")`.
- `cc_remote/wrapper/` — `sdk.py` / `stream.py` and `claude_*` implement Claude;
`codex_handle.py` / `codex_stream.py` / `codex_daemon.py` / `codex_external.py`
implement the official Codex app-server paths; `history_store.py` owns the
rebuildable SQLite projection; `machine.py`, `command_router.py`,
implement the official Codex app-server paths; `codex_lifecycle.py` owns the
source-bound exact-terminal ledger; `history_store.py` owns the rebuildable
SQLite projection; `machine.py`, `command_router.py`,
`session_ctx.py`, `ringbuffer.py`, `transport.py`, and `session.py` provide
the shared session pool, command dispatch, live replay, relay transport, and
persistence.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

## Unreleased

- Upgrade the coordinated Wrapper/Relay/Web gate to protocol v35. Exact Codex
app-server and source-validated rollout terminals now travel independently of
the narrative History projection, so a multi-hundred-MiB rollout cannot keep
a completed turn spinning while its content index catches up. Terminal facts
remain profile-, revision-, and source-bound; they never guess the newest open
row or create a second completion receipt.
- Upgrade the coordinated Wrapper/Relay/Web gate to protocol v34. Main-session
completion acknowledgements and exact Goal-generation dismissals now live in
bounded wrapper-owned state, so reading or hiding them in one browser updates
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## 未发布

- Wrapper、Relay 与 Web 的协同 gate 升级到 protocol v35。Codex app-server 的
精确终态与通过源文件校验的 rollout 终态现在独立于 History 正文投影下发;数百
MiB 的 rollout 即使仍在补建内容索引,也不会让已经完成的回合继续转圈。终态事实
始终绑定账号、revision 与源文件,不会猜测“最后一个未完成回合”,也不会重复生成
完成回执。
- Wrapper、Relay 与 Web 的协同 gate 升级到 protocol v34。主会话完成回执和精确
Goal generation 的隐藏回执改由 wrapper 有界持久化;任一浏览器已读或隐藏后会
同步到所有已连接浏览器,重连后仍保持一致,同时不会误隐藏后来替换的新 Goal。
Expand Down
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ local `claude` or `codex` session through a WebSocket relay. Two independent lin
`useLayoutEffect` is deliberately dependency-free — late virtualizer/image
measurements settle without a React render, and constraining it to its read
set reintroduces a full-viewport jump on touch release.
- **Protocol version gate**: current wire protocol v34 is declared by
- **Protocol version gate**: current wire protocol v35 is declared by
`PROTOCOL_VERSION` in both `protocol.py` and `web/src/protocol.ts`.
`deserialize` hard-rejects a version mismatch, and
`_Base` is `extra="forbid"`, so ANY protocol change must be deployed to all
Expand Down Expand Up @@ -125,7 +125,12 @@ local `claude` or `codex` session through a WebSocket relay. Two independent lin
hello sends lightweight resident `Snapshot`s; reconnect cursors replay only
the bounded missing live tail. Source fingerprints invalidate appended pages,
and rollback explicitly invalidates both server and browser projections. These
reads never spawn/resume an engine or create a model turn.
reads never spawn/resume an engine or create a model turn. Codex
`History.terminal_fences` is a separate bounded lifecycle projection: only a
real app-server terminal or a source-validated rollout marker may enter it;
local synthetic failures may not. The browser applies a fence only to its
exact native turn identity and never changes completion receipts or guesses
from the last open row.
- **Token-aware residency**: resuming an evicted Claude SDK session may rebuild
a cold prompt cache, so it only happens on first spawn / re-focus after
eviction; raising the cap trades RAM for fewer cold re-sends. Codex context is
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

自托管 · 双引擎 · 多会话 · 实时过程 · 响应式 Web

**当前版本:v3.0.0** · Wire protocol v34
**当前版本:v3.0.0** · Wire protocol v35

[English](README_en.md) ·
[5 分钟上手](#本地快速开始一台机器5-分钟) ·
Expand Down Expand Up @@ -466,15 +466,15 @@ npm --prefix web run build # 产出 web/dist/

> 现在网页**不再把 token 烤进 JS**:登录改为向中继 POST 口令换取短期会话 token。所以构建不需要任何 `VITE_*` 变量。

> **升级到协议 v34**:线协议会严格拒绝版本不一致。请在同一次维护窗口部署
> **升级到协议 v35**:线协议会严格拒绝版本不一致。请在同一次维护窗口部署
> `cc_remote/` 和新的 `web/dist/`,然后依次重启 relay、wrapper;不要新旧版本滚动混跑。
> 升级期间已有 WebSocket 会短暂重连,relay 重启也会要求浏览器重新登录。已打开的
> 旧版页面必须做一次**硬刷新**(重新加载新的带 hash 静态资源),仅重新登录不够。
> 手工发布时先停本机 wrapper,再停服更新 relay + web,最后启动 v34 relay 和
> v34 wrapper;这样旧 wrapper 不会占住同一 `machine_id` 的连接槽。v34 会迁移本机
> Work SQLite;手工发布还必须在启动 v34 前用
> `deploy/work_registry_snapshot.py snapshot` 保存两个注册表。回滚时先停 v34、恢复该
> 快照,再切回旧代码;不要在 wrapper 运行时只复制主 `.sqlite3` 文件而漏掉 WAL。
> 手工发布时先停本机 wrapper,再停服更新 relay + web,最后启动 v35 relay 和
> v35 wrapper;这样旧 wrapper 不会占住同一 `machine_id` 的连接槽。若从 v34 以前的
> 版本跨级升级,仍须执行 v34 引入的 Work SQLite 迁移保护:启动新 wrapper 前用
> `deploy/work_registry_snapshot.py snapshot` 保存两个注册表。回滚时先停新版本、恢复
> 该快照,再切回旧代码;不要在 wrapper 运行时只复制主 `.sqlite3` 文件而漏掉 WAL。

### 3)上传 staging,由原子 release 安装器发布

Expand Down Expand Up @@ -528,7 +528,7 @@ sudo bash ~/cc-remote-upload/deploy/setup-vps.sh \
脚本会:装 `python3-venv` + Caddy、建 `ccremote` 系统用户、创建不可变 release
和 release-local venv、合并 Caddy 配置、原子切换 `current`,再重启 relay。若新
relay 重启或健康检查失败,`current`、Caddyfile、systemd unit 会作为一个事务全部
恢复,并验证旧 release 的 `/healthz`。成功后再启动 v34 wrapper。
恢复,并验证旧 release 的 `/healthz`。成功后再启动 v35 wrapper。

验证:

Expand Down
19 changes: 10 additions & 9 deletions README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Self-hosted · Dual-engine · Multi-session · Live process · Responsive Web

**Current release: v3.0.0** · Wire protocol v34
**Current release: v3.0.0** · Wire protocol v35

[中文](README.md) ·
[5-minute quick start](#quick-start-local-one-machine-5-min) ·
Expand Down Expand Up @@ -548,19 +548,20 @@ npm --prefix web run build # produces web/dist/

> The web client no longer bakes any token into the JS: login POSTs the password to the relay for a short-lived session token. So the build needs no `VITE_*` variables.

> **Upgrading to protocol v34:** the wire gate rejects mixed versions. Deploy
> **Upgrading to protocol v35:** the wire gate rejects mixed versions. Deploy
> `cc_remote/` and the new `web/dist/` in one maintenance window, then restart the
> relay and wrapper; do not run a rolling mixture. Existing sockets reconnect
> briefly, and a relay restart intentionally requires browsers to log in again.
> Any already-open older page also needs one **hard refresh** to load the new hashed
> assets; logging in again inside the old JavaScript bundle isn't sufficient.
> For a manual release, stop the local wrapper first, stop and update relay + web,
> then start the v34 relay and v34 wrapper so the old wrapper cannot occupy the
> slot for the same `machine_id`. v34 migrates provider-local Work SQLite data;
> a manual release must also run `deploy/work_registry_snapshot.py snapshot`
> before v34 starts. To roll back, stop v34, restore that snapshot, then switch
> to the old code. Do not copy only the main `.sqlite3` file while the wrapper is
> live because committed pages may still be in WAL.
> then start the v35 relay and v35 wrapper so the old wrapper cannot occupy the
> slot for the same `machine_id`. When upgrading from a pre-v34 release, retain
> the Work SQLite migration protection introduced by v34: a manual release must
> run `deploy/work_registry_snapshot.py snapshot` before the new wrapper starts.
> To roll back, stop the new version, restore that snapshot, then switch to the
> old code. Do not copy only the main `.sqlite3` file while the wrapper is live
> because committed pages may still be in WAL.

### 3) Upload staging, then publish it as an atomic release

Expand Down Expand Up @@ -617,7 +618,7 @@ The script installs `python3-venv` + Caddy, creates the `ccremote` service user,
builds an immutable release and its venv, merges Caddy configuration, atomically
switches `current`, and restarts the relay. If restart/readiness fails, `current`,
the Caddyfile, and the systemd unit roll back as one transaction and the previous
release's `/healthz` is verified. Start the v34 wrapper after success.
release's `/healthz` is verified. Start the v35 wrapper after success.

Verify:

Expand Down
32 changes: 30 additions & 2 deletions cc_remote/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from pydantic import (
AfterValidator, BaseModel, ConfigDict, Field, StringConstraints, ValidationError,
model_validator,
PrivateAttr, model_validator,
)

from cc_remote.attachments import (
Expand All @@ -28,13 +28,14 @@
MAX_SINGLE_ATTACHMENT_BYTES,
)

PROTOCOL_VERSION = 34
PROTOCOL_VERSION = 35

# Codex Desktop renders a 53-week daily token-activity calendar. Keep the wire
# payload to that same bounded window so an account response can never turn a
# one-shot status frame into an unbounded relay/browser allocation.
MAX_STATUS_USAGE_BUCKETS = 53 * 7
MAX_SAFE_WIRE_INTEGER = 9_007_199_254_740_991
MAX_SAFE_WIRE_TIMESTAMP_SECONDS = MAX_SAFE_WIRE_INTEGER // 1000

State = Literal["idle", "running", "interrupting", "draining"]
Engine = Literal["claude", "codex"]
Expand Down Expand Up @@ -843,6 +844,11 @@ class TurnEnd(_Base):
# not the assistant UUID above or the browser's optimistic message id.
checkpoint_id: Optional[WireId] = None
notification_context: Optional[TurnNotificationContext] = None
# Wrapper-internal provenance. This is deliberately a Pydantic private
# attribute so it never crosses the wire or changes protocol validation.
# Only a real Codex app-server ``turn/completed`` may set it; locally
# synthesized TurnEnd frames must not become durable lifecycle facts.
_codex_authoritative_terminal: bool = PrivateAttr(default=False)


class Error(_Base):
Expand Down Expand Up @@ -1972,6 +1978,23 @@ class ConversationTurn(BaseModel):
detailLoaded: bool = False


class CodexTerminalFence(BaseModel):
"""Source-bound Codex lifecycle fact independent of History content.

App-server's terminal notification is authoritative, but a large rollout's
materialized History page can briefly lag behind it. A newest-page History
carries these small exact-turn fences so a reconnect can close the already
painted row without rescanning or guessing from the last open turn.
"""
model_config = ConfigDict(extra="forbid")
turn_id: WireId
status: Literal["completed", "interrupted", "failed"]
duration_ms: Optional[int] = Field(
default=None, ge=0, le=MAX_SAFE_WIRE_INTEGER)
completed_at: Optional[float] = Field(
default=None, ge=0, le=MAX_SAFE_WIRE_TIMESTAMP_SECONDS)


class History(_Base):
"""wrapper -> client: one summary or compatibility event page.

Expand Down Expand Up @@ -2032,6 +2055,11 @@ class History(_Base):
# real user interrupt or crash must stay terminal on a cold browser.
compaction_continuation_turn_ids: list[WireId] = Field(
default_factory=list, max_length=4)
# Exact native terminal facts are a separate lifecycle projection. They
# may close a stale/incomplete narrative page, but never identify a target
# by array position and never replace live TurnEnd notifications.
terminal_fences: list[CodexTerminalFence] = Field(
default_factory=list, max_length=16)
# Authoritative replacement after a destructive history mutation such as
# Codex rollback. Ordinary loads merge with a live tail; reset loads must
# discard turns that the engine has just removed.
Expand Down
Loading
Loading