Codex 用量 / 重置额度是独立的账户级面(三个方法,全部按 SessionRef 限定路由):
runtime.codex.usage.get(codex.usage.read,只读):返回CodexUsageView{session, model, primary, secondary, captured_at, available_reset_count},两个窗口各有used_percent/window_minutes/reset_at,全部必有且可空。window_minutes是上游真实窗口 长度(控制台据此标注5h/7d,不再沿用 TUI 的硬编码1d)。captured_at与reset_at都是 Unix 秒。runtime.codex.reset_credits.get(codex.usage.read,只读):返回CodexResetCreditsView{session, model, available_count, credits},每行{id, reset_type, status, granted_at, expires_at, title, description}(granted_at/expires_at是额度自身的到期时间)。runtime.codex.reset_credits.consume(codex.reset.consume,写):expected_model必须 等于会话当时的有效模型(否则conflict),confirmed必须是字面量true(DTO 与 wire decoder 各校验一次),command_id是客户端生成的幂等键并作为上游redeem_request_id。 返回CodexConsumeResult{session, model, command_id, outcome},outcome是五个字面量之一 (reset/alreadyRedeemed/nothingToReset/noCredit/unknown);未知上游动词一律 归一为unknown,不原样透传上游文本。
两个读能力与写能力互相独立:codex.usage.read 不授权兑换,session.read 也不授权其中任何一个。
兑换是不可撤销的账户级操作(消耗该 OAuth 账户的 1 次重置额度,不只影响当前会话),因此
daemon 侧还有一层:command_id 幂等账本(同键同指纹复用结果、异键同指纹 conflict)、同额度
未决请求不再以新键重发、以及“兑换前重新读取额度列表并确认该额度为 available 且未过期”。
该面不投影任何凭据:没有 access token、account id、请求头、URL,也没有 OAuth 授权的
expires_at(那是令牌到期时间,不是额度重置时间)。上游异常与响应正文被替换为固定文案,
不进入 RPC 错误。runtime.config.get 因此新增一个只读字段 codex_usage_enabled(默认
false):只有在「daemon 已注入 provider + 会话已打开 + 该会话实际选中的 profile
auth == "openai_oauth"」三者同时成立时才为 true;客户端不得按模型名猜测。
| runtime.codex.reset_credits.consume | session, expected_model, credit_id, command_id, confirmed | CodexConsumeResult |
| runtime.codex.reset_credits.get | session, optional force | CodexResetCreditsView |
| runtime.codex.usage.get | session, optional force | CodexUsageView |
S7 Wire 协议表¶
Envelope¶
| 类型 | 字段 |
|---|---|
| request | jsonrpc: "2.0", id: string/int, method, params: object |
| success | jsonrpc, id, meta: {wire_version: "1"}, result |
| error | jsonrpc, id, meta: {wire_version: "1"}, error: {code,message,data.service_code} |
| notification | jsonrpc, meta: {wire_version: "1"}, method, params,无 id |
Request 不支持 notification。SessionRef 在所有方法中都是精确的 {project_id,thread_id};两个字段均为非空、无 NUL、最多 256 UTF-8 bytes。未知字段、未知方法、principal 字段和非 object params 都拒绝。
Methods¶
契约冻结后 wire 表共 50 个方法(48 个 service 方法 + runtime.protocol.negotiate / runtime.events.unwatch 两个连接态方法)。下表按方法名排序,与 service/contract_registry.py 的 WIRE_METHODS 一一对应。
| method | params 的主要字段 | result |
|---|---|---|
runtime.artifacts.list |
session, optional path/cursor/limit |
ArtifactPage |
runtime.artifacts.read |
ref: {session,path}, optional offset/limit/expected_revision |
ArtifactChunk |
runtime.artifacts.stat |
ref: {session,path} |
ArtifactMetadata |
runtime.apps.list |
optional limit |
ExternalAppPage |
runtime.attachments.begin |
session, size, mime, optional display_name |
BeginAttachmentResult |
runtime.attachments.append |
ref: {session,attachment_id}, expected_offset, data_base64 |
AppendAttachmentChunkResult |
runtime.attachments.finish |
ref, expected_size, expected_mime |
FinishAttachmentResult |
runtime.attachments.abort |
ref |
AbortAttachmentResult |
runtime.attachments.stat |
ref |
AttachmentMetadata |
runtime.attachments.read |
ref, optional offset/limit |
AttachmentChunk |
runtime.config.get |
session |
RuntimeConfigView |
runtime.events.read |
session, optional after/limit/scan_limit/filter/max_event_bytes |
EventPage |
runtime.events.unwatch |
subscription_id |
{removed} |
runtime.events.watch |
session, optional after/queue_size/filter/max_event_bytes |
{subscription_id,cursor} then notifications |
runtime.fs.list |
optional path/limit |
DirectoryListing |
runtime.git.diff |
session, path, optional staged |
GitDiffResult |
runtime.git.status |
session |
GitStatusResult |
runtime.project.list |
optional limit/offset |
ProjectListPage |
runtime.project.register |
workspace_path |
ProjectListItem |
runtime.project.thinking.set |
project_id, level, optional command_id |
SetProjectThinkingLevelResult |
runtime.protocol.negotiate |
versions, optional client |
NegotiateResult |
runtime.session.close |
session, optional cancel_active/command_id |
CloseSessionResult |
runtime.session.create |
project_id, optional thread_id/title/command_id |
CreateSessionResult |
runtime.session.delete |
session, optional command_id |
DeleteSessionResult |
runtime.session.get |
session |
SessionView |
runtime.session.goal |
session |
SessionGoalView or null |
runtime.session.goal.set |
session, objective, optional token_budget/command_id |
SessionGoalResult |
runtime.session.goal.edit |
session, expected_goal_id, objective, optional command_id |
SessionGoalResult |
runtime.session.goal.clear |
session, expected_goal_id, optional command_id |
SessionGoalResult |
runtime.session.goal.pause |
session, expected_goal_id, optional command_id |
SessionGoalResult |
runtime.session.goal.resume |
session, expected_goal_id, optional command_id |
SessionGoalResult |
runtime.session.history |
session, optional before_turn/limit |
SessionHistoryPage |
runtime.session.list |
project_id, optional limit/offset |
SessionListPage |
runtime.session.mcp.reload |
session, optional server/enabled/include_tools/command_id |
ReloadMcpResult |
runtime.session.open |
session, optional command_id |
OpenSessionResult |
runtime.session.rebind |
session, model, optional command_id |
RebindSessionResult |
runtime.session.reconcile |
session, optional probe_turn_ids |
SessionRecoverabilityView |
runtime.session.rename |
session, title, optional command_id |
RenameSessionResult |
runtime.session.search |
project_id, optional text/limit/offset |
SessionSearchPage |
runtime.session.thinking.set |
session, level, optional command_id |
SetThinkingLevelResult |
runtime.workspace.open_external |
session, path, optional app_id/mode/command_id |
OpenExternalResult |
runtime.workspace.revert |
session, turn_id, path, optional command_id |
RevertTurnChangeResult |
runtime.turn.approval.get |
session, expected_turn_id |
PendingApprovalView |
runtime.turn.approval.resume |
session, expected_turn_id, decisions, optional command_id |
ResumeTurnResult |
runtime.turn.cancel |
session, expected_turn_id, optional reason/command_id |
CancelTurnResult |
runtime.turn.steer |
session, expected_turn_id, text, optional command_id |
SteerTurnResult |
runtime.turn.submit |
session, text, optional command_id/config_overrides/attachment_refs |
CommandReceipt |
filter 是 {kinds: string[], turn_ids: string[]};read 还接受 limit、scan_limit、max_event_bytes,watch 不接受 read-only 分页字段。artifact data_base64 原样传输。attachments 走独立六方法:一个 chunk 解码后不超过 256 KiB(base64 上限 MAX_CHUNK_BASE64_CHARS),始终落在 1 MiB frame 内;begin / append / finish / abort 由 attachments.write 授权,stat / read 由 attachments.read 授权,均按 SessionRef 限定。runtime.turn.submit 新增可选 attachment_refs(不透明 id 数组,最多 8 个),服务端从可信 session workspace 解析;旧 in-process attachments 对象仍被 wire 拒绝非空,两来源不可混用,且 text 与 attachment_refs 至少提供其一。Web 侧上传、取消与历史缩略图均已接线。
会话管理(additive)¶
runtime.session.list(session.list,project scope,limit1..100 /offset0..100000)与runtime.session.history(session.read,limit1..100、before_turn≥ 1)是只读面;runtime.session.reconcile(session.read)接受最多 32 个去重probe_turn_ids。runtime.session.create(session.create,project scope)写一条会话元数据行:thread_id可省略,省略时由服务端分配并在结果里返回,客户端不得自行编造 id;title可选、非空、≤ 120 字符。它不是runtime.session.open:不打开 runtime。runtime.session.rename(session.rename)要求title非空且 ≤ 120 字符;会话不存在是not_found,不会创建数据库文件或 schema。runtime.session.delete(session.delete)删元数据行与 thread goal,并把该 thread 从它留下的每一个存储里清掉:LangGraph checkpoint(含tools:*子代理命名空间)、transcript projection、全文检索索引、以及该会话的回滚快照。结果里的retained_history报告实际结果:false表示四个存储都已清空,true表示有存储拒绝(purge_failures点名是哪一个),因此 UI 只在false时才可以声称对话已被擦除。行删除与清理都在同一个 lifecycle 临界区里完成;正在运行回合的会话原子地以conflict拒绝,且不取消该回合。runtime.session.search(session.search,project scope)是元数据搜索(title / summary / thread_id / model / active_model),text≤ 200 字符、limit1..100、offset0..100000;它不是 transcript 全文检索,也不会创建数据库或构建 agent。
goal 管理写(additive)¶
runtime.session.goal(读)之外的五个写方法 runtime.session.goal.set / .edit / .clear / .pause / .resume 共用一个独立 capability session.goal:session.read 只授权读,绝不授权任一写操作。edit / clear / pause / resume 都要求 expected_goal_id(乐观并发,目标已被替换时返回 conflict);set 的 objective 必填、token_budget 可选且必须为正整数,且拒绝覆盖未完成的 goal(conflict)。pause 只请求取消本会话自己的 live turn 并通过 cancellation_requested 报告;resume 只做状态转移,不会自动续跑一轮。写入落在该会话自身的 ledger 上,不用全局 get_goal_service() 单例,因此一个项目不会写到另一个项目的 goal。
项目列举与登记、宿主目录浏览(additive)¶
runtime.project.list(project.list,catalog scope,无 per-request 项目位置)只读枚举已登记项目:请求 limit 1..100、offset 0..100000,visible_project_ids 是服务端计算字段,wire decoder 显式拒绝客户端传入;服务端在 provider 内先按可见性过滤再分页。该方法不打开会话、不构建 agent、也不注册新项目。项目登记(ProjectCatalog.register_project)现在有 wire 方法:runtime.project.register。
runtime.project.register(project.register,catalog scope,无 per-request 项目位置)是写面:请求只有 workspace_path(≤ 4096 UTF-8 bytes,wire decoder 只校验长度与 NUL),daemon 解析该宿主路径、确认它是已存在目录,再 upsert 用户层项目 catalog(~/.synapse/catalog.sqlite)。登记按 workspace 路径幂等:重复登记复用同一个稳定 project_id,结果是与列举面相同的 ProjectListItem 投影。
runtime.fs.list(fs.list,catalog scope,无 per-request 项目位置)是只读且有界的宿主目录浏览:请求 path(null 表示 daemon 的 home 目录)与 limit(默认 200,1..1000),结果是 DirectoryListing(path / parent,文件系统根处 parent 为 null / entries: DirectoryEntry[] / truncated / roots,其中 roots 是平台顶层入口:Windows 为盘符、POSIX 为 /),DirectoryEntry 只有 {name, path}。它只枚举该目录的直接子目录——从不返回文件、从不递归。
两者的授权都只要求一个项目级(project-wide)能力授予(project.register / fs.list,与 project.list 一样经 visible_project_ids 计算),不依赖任何 per-request 项目位置。
runtime.session.rebind 与 runtime.session.thinking.set 都是会话级写:只替换该
thread 后续 turn 使用的 agent/settings 绑定并持久化到该会话的 model binding,绝不修改
项目默认值。thinking.set 的 level 必须落在该会话 runtime.config.get 公布的
thinking_levels 白名单内(否则 invalid_request),返回 level 为规范化后的实际等级
(关闭思考时为 off)与刷新后的 RuntimeConfigView。授权上二者各自需要独立写能力位
(session.rebind / session.thinking),session.read 不足以授权任一写操作。
runtime.project.thinking.set 是项目级写(参数是 project_id,不是 session):把
level 写入该项目的设置层(<workspace>/.synapse/settings.json 的 enable_thinking /
reasoning_effort),语义是「此后新建会话的默认推理等级」——已打开的会话不被重绑,daemon
重启后仍生效。level 必须落在与 runtime.config.get 同一来源的白名单内(否则
invalid_request),授权需要独立能力位 project.thinking,且只接受项目级授权
(thread_ids=None);session.read / session.thinking / session.rebind 都不授权它。返回
{command_id, project_id, level}(level 为规范化后的实际等级),不返回设置文件路径。
runtime.config.get 因此新增两个只读字段:project_thinking_level(项目自身默认,可能与
thinking_level 会话值不同)与 can_set_project_thinking(该 peer 是否提供项目级写端口)。
S9 增加 transport 控制方法 runtime.protocol.negotiate,严格参数为
{versions: string[], client?: {name: string, version: string}}。当前仍只有
SUPPORTED_WIRE_VERSIONS=("1",);S7 客户端不协商时,首个业务请求隐式锁定 v1,保持
向后兼容。协商不属于 service dispatch,元数据仍使用连接选定的 v1。
尚未 wire(本表之外)¶
下列后端能力存在但没有 wire 方法,本表(及整个契约)不覆盖:会话清理 prune_empty、会话导出(JSON / Markdown)、对话全文搜索、上下文压缩与上下文状态(TUI /compact、/context)、safety / 权限策略读写、工具输出压缩设置(TUI /compression)、远程身份 / 多用户 principal 管理。项目登记/更新已不再是缺口:runtime.project.register 覆盖 ProjectCatalog.register_project 的登记/更新(touch_project 没有单独方法),宿主目录浏览由 runtime.fs.list 提供。TUI 的 /theme 与 slash 命令解析/补全属 UI-only,不需要 RPC。逐项真源与状态见 progress.md 的「尚未 wire 的后端能力(真实矩阵)」。
Limits and lifecycle¶
单 text frame 默认最多 1 MiB,binary frame 拒绝(1003);超限 frame 由 WebSocket 层关闭 1009;JSON 最大 nesting 64,单连接最多 32 inflight/32 subscriptions。输出 canonical UTF-8、compact、sorted,单消息最多 8 MiB。单 connection 一个 bounded writer queue,默认 128,范围 1..4096;满时关闭 1013,固定 reason;writer 发送失败关闭 1011,使用固定 transport reason。watch response 在 stream enter 成功后等待 writer acknowledgement,实际发送后才启动 pump;replay gap 或 enter error 只返回 request error 且不残留 subscription。断开连接只 detach,不调用 close session/cancel turn。S8 daemon ownership、S9 client 重连/版本协商见 S9 compatibility matrix 与 ADR-S-017。