本地运行时任务
Wework 的本地运行时任务用于展示和继续用户已经在设备上创建的 Codex 或 Claude Code 工作。它不再把这些工作导入中心库的 TaskResource 或 Subtask,也不再依赖 Backend projects 表生成侧栏列表。列表来自在线设备 executor 返回的运行时线程,并在前端展示为两类:
Project
LocalTask
Conversation
LocalTask
数据归属
- Project 是 executor 线程所属工作区的展示分组,由运行时列表里的工作区信息推导。
- Conversation 是没有项目归属的 Codex 对话线程展示分组。
- LocalTask 是 executor 本机状态,只保存在设备上。
- Project 的运行时身份是
deviceId + workspacePath派生出的 workspace key,不使用中心库projects.id。Wework 可以在组件内部生成临时 UI id,但不能把这个 id 写回 Backend 或放进 URL 作为项目身份。 - LocalTask 的稳定身份是
deviceId + localTaskId。workspacePath只作为设备工作区上下文使用,用于列表分组、创建任务和右侧工具定位目录;任务 URL、IM 通知订阅和原生 Codex 更新去重都不把路径作为身份字段。 - executor 返回的
workspaceKind用于区分 Project 与 Conversation。Codex App 风格的目录(例如~/Documents/Codex/YYYY-MM-DD/<name>)会被标记为chat并展示到“对话”,其他工作区展示到“项目”。
executor 仍为非 Codex 或导入类本地任务保留 JSON LocalTask 索引:
$WEGENT_EXECUTOR_HOME/runtime-work/index.json
原生 Codex 任务不写入这个索引。它们在列表刷新时通过 Codex SDK 和 Codex session 文件即时发现,运行中状态也从 Codex status 与 session transcript 推导。运行时句柄不依赖 SQLite,也不会同步到中心数据库。
列表刷新
任务列表由 Wework 在启动、显式刷新或设备状态变化时请求,不再由固定 interval 轮询触发。
- Wework 请求
GET /api/runtime-work。 - Backend 读取当前用户在线设备列表,并通过设备 WebSocket RPC 调用
runtime.tasks.list。 - executor 刷新本机 Codex 发现结果,并合并非 Codex/导入类 JSON LocalTask 索引。
- executor 在返回值中携带
workspaceKind、工作区路径、任务标题、更新时间和设备状态。 - Backend 做轻量聚合后返回给 Wework,不再读取或匹配 Backend
projects表。 - Wework 根据 runtime work 响应展示 Project 和 Conversation;每个 LocalTask 的打开和通知身份仍是
deviceId + localTaskId。
executor 不主动向 Backend 轮询或推送任务列表。离线设备不会贡献 LocalTask;Wework 可以显示映射目录离线,但不会从中心库缓存本地任务。
如果只有一个设备,Wework 不在项目名后显示设备 IP;如果有多个设备,本地设备不显示 IP,远端在线设备显示可用的非 loopback runtime transfer host 或客户端 IP,并配绿色在线点。远程项目和远程主机选择器的主显示文本也优先使用这个 IP/host;设备 id 只是缺少网络地址时的技术回退。
打开和继续任务
打开 LocalTask 时,Wework 调用 Backend:
POST /api/runtime-work/transcript
Backend 将 deviceId + localTaskId 转发给对应设备的 runtime.tasks.transcript。原生 Codex 任务通过 Codex session path 或 session 文件发现定位;非 Codex/导入类任务可以使用 workspacePath 作为本地索引查找提示,或者通过本机 LocalTask 索引按 localTaskId 定位。executor 读取原生运行时 transcript,并返回标准化消息。
继续 LocalTask 时,Wework 调用:
POST /api/runtime-work/send
Backend 转发 runtime.tasks.send。executor 根据本地 LocalTask 的 opaque runtime handle 继续运行时会话。Claude Code 任务会把本地 transcript 写回 JSON LocalTask 索引;原生 Codex 任务只继续 Codex SDK thread,消息和状态以 Codex 自己的 session 记录为准,不写回 executor JSON 索引。流式 Responses 事件只携带 local_task_id 和运行时信息,不携带 workspacePath。
原生 Codex 任务有一个额外约束:刷新 transcript 时只信任 Codex 本身的会话记录。fork 包或 executor JSON 索引中携带的 runtimeHandle.messages 只是导入瞬间的快照,不能作为原生 Codex transcript 的回退来源,否则 Wework 刷新后会显示旧消息或丢失用户追问。非 SDK 原生任务仍可以使用 executor JSON 索引中的本地 transcript。
工作区工具上下文
Wework 打开 LocalTask 后,右侧文件、审查和终端工具使用当前 LocalTask 的设备和目录上下文解析设备与目录:
- 优先使用
runtime.tasks.list返回的 LocalTaskworkspacePath,这样 Codex worktree 不会被当成另一个 Project。 - 如果 LocalTask 能映射到 Project,环境信息和审查仍带上 Project,但 Git 命令运行在 LocalTask 的实际目录。
- 如果 LocalTask 没有映射到 Project,只要设备在线且目录可访问,本地终端仍可打开;依赖 Project API 的 IDE 能力仍要求 Project 上下文。
- 对运行时 LocalTask 打开的终端必须使用当前 LocalTask 的
deviceId + workspacePath启动设备级 PTY,不能回退到 Project 默认绑定设备,否则跨设备 worktree 会打开到错误机器。
创建任务
创建新的运行时任务时,Wework 调用:
POST /api/runtime-work/create
Backend 根据请求中的项目映射或独立设备工作区解析目标设备和目录,构造一次临时 execution request,然后调用设备 RPC runtime.tasks.create。这个流程不会 db.add() 任何 TaskResource 或 Subtask。
运行时创建的持久化位置由具体 runtime 决定:
- Claude Code 创建 executor JSON LocalTask,并在该索引中保存 transcript 和 runtime handle。
- Codex 创建时先返回 executor 进程内的
localTaskId,让前端立即打开任务并接收 stream;后台启动原生 Codex SDK thread 后,会把真实 Codex threadId 保存在内存 runtime handle 中用于后续 send/resume。 - Codex 创建和继续时都不把任务缓存到 executor JSON 索引。当前 executor 进程内会保留一个临时内存记录,用来覆盖 Codex discovery 尚未发现新 thread 的短暂空窗;executor 重启后再以原生 Codex discovery/session 为准。
- Codex 创建时仍通过 LocalTask Responses 事件通道流式返回
response.created、文本/tool 增量和response.completed/error,这些事件使用 create 返回的localTaskId,前端不需要等待下一次列表刷新才能显示运行中的回复。 - 附件仍由 executor 的 Codex attachment pipeline 处理:Backend 只传 attachment id,executor 在目标设备上下载并转换给 Codex SDK,前端不传本地附件路径。
Project 场景使用运行时 workspace 引用:
- Wework 发送 workspace key 或显式选择的
deviceId + workspacePath,不发送中心库projectId。 - Backend 根据当前用户在线设备返回的 runtime workspace 列表校验该工作区,解析出可信的
deviceId + workspacePath。 - 如果没有选中项目,Wework 使用本地设备的空工作区上下文进入普通对话;该状态不带远程 IP,也不把
projectId=0写进 URL。 - 新建空白项目会在目标设备的
~/Documents下创建目录;如果目录名已存在,前端必须要求用户重命名,而不是把已有目录当成项目使用。
空项目也由运行时持有。Wework 创建或选择目录后调用 workspace open/register 流程,让 executor 把这个工作区纳入 runtime.tasks.list 的项目分组;即使目录下还没有 LocalTask 或 Codex 会话,也应显示为项目。这个流程不写 TaskResource、Subtask,也不写 Backend projects 表。
复制和跨设备转移
复制运行时任务时,Wework 只在当前任务所属 Project 内选择目标工作区:
- 已经绑定到该 Project 的其他 Device Workspace 可以直接作为目标。
- 没有绑定到该 Project 的在线设备,需要先走和项目创建/编辑一致的设备目录准备流程:选择设备目录,并选择该目录在 Project 下的类型是
worktree还是普通workspace。 - Backend 调用
POST /api/runtime-work/device-workspaces/prepare写入 Device Workspace 映射后,再继续执行任务复制。 - Device Workspace 的
label可以保存worktree或workspace。Backend 返回 runtime work 列表时会优先用这个标签作为workspaceKind,这样前端不会把同一 Project 下的 worktree 当成另一个 Project,也不会展示无关 Project 或未映射目录作为复制目标。 - 如果 Project 配置了
git信息,Backend 会先确认源和目标工作区的 Git remote 相同,并确认源任务的HEADcommit 在目标仓库可达。确认成功后,目标设备不会直接在 Project 主目录上导入任务,而是在目标项目工作区对应的worktrees/<transferId>/<projectDir>路径创建或复用一个 detached Git worktree,再把 fork 后的 LocalTask 绑定到这个 worktree 路径。这样复制任务不会污染目标 Project 主目录,列表刷新时也能把 worktree 下的任务归回同一个 Project。 - Git fork 复制任务上下文、Codex 会话状态、必要的会话文件,以及从公共 base commit 生成的轻量 Git patch。该 patch 覆盖源工作区中的本地 commit、未提交的 tracked 文件修改,以及未被 ignore 的 untracked 文件 overlay;不会把完整 Git 仓库目录打包上传到对象存储。如果 Git 条件不满足,才进入普通 archive 传输。
- 如果 Project 不是 Git 工作区,复制才会走 executor 的直接 archive 传输,并在直连不可用时使用对象存储兜底。
- 直接 archive 传输只尝试 Backend 从 WebSocket 连接看到的 TCP peer host 和 executor 上报的 runtime transfer host。executor 会用一次带 token 的 probe 验证对端,避免把业务上报的 NAT/代理地址直接当成可信目标。直连不可用且没有对象存储配置时,Backend 返回 503,而不是静默落入不可用的 S3 路径。
复制任务的身份仍然使用 deviceId + localTaskId。workspacePath 只用于定位目标设备目录和工作区工具上下文。
Project 与 Conversation
Wework 不再显示“未映射工作区”。executor 返回的线程必须能归入“项目”或“对话”:
workspaceKind: chat的任务展示在“对话”区。- 其他任务按工作区名称展示为“项目”。
- “对话”区即使为空也始终显示,并且支持像“项目”区一样折叠和展开。
IM 通知
运行时任务可以向 IM 会话发送通知,但通知状态以 deviceId + localTaskId 为准,不创建 DB Task,也不把 workspacePath 写进通知 key。
- 在 IM 中使用
/notify on、/通知 开开启当前用户的全局运行时任务通知目标。 - 使用
/notify off关闭全局通知,使用/notify status查看当前状态。 - 单个 IM 会话订阅某个运行时任务后,只接收该任务的更新。
- executor 发现原生 Codex 任务更新时间变化时,只在最后一条 assistant 消息进入终态且有回复内容后,通过设备 WebSocket 发送不含
workspacePath、但包含status和content的runtime.tasks.updated。Backend 会忽略运行中/流式更新,并按订阅和全局通知设置把终态回复投递到 IM。 - Wegent 发起的 runtime send 与原生 Codex watcher 使用同一个
deviceId + localTaskId去重,避免 Codex 和 Wework 对同一次任务更新重复通知。
URL
Wework 的运行时任务 URL 使用:
/runtime-tasks?deviceId=<device>&localTaskId=<local-task>
URL 不包含 workspacePath。刷新页面或复制链接时,前端先用 URL 里的 deviceId + localTaskId 打开任务,再从最新的 runtime work 列表恢复该任务的工作区上下文。
新对话和未选择项目的入口使用根路径或普通会话路径,不使用 projectId=0 这类占位参数。项目选择状态由 runtime workspace 引用和当前会话上下文恢复。
兼容性
Wegent 原生 Task/Subtask 流程仍保留给现有聊天、共享任务和历史 task URL。Wework sidebar、移动端 drawer、项目下任务展示和新任务创建路径使用 runtime work API,不再依赖 DB task list 或 Backend projects 表。