pi-onlyne2.2.0
Onlyne agent adapter for pi: the session lifecycle an onlyne role client expects from a pi host.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-onlyne —— pi 的 onlyne agent 适配器
一个 pi 扩展:一个 pi 进程承载一个 onlyne role session。它连接该工作区的 client 所服务的
socket——机器级运行目录里的 <digest>.sock(/tmp/onlyne-<uid>/,ONLYNE_RUNTIME_DIR 可覆盖),
而不是树内的任何路径——按 crates/onlyne-adapter/PROTOCOL.md 通信,带 session 走完
hello → welcome → assign → 工作 → complete → detach。全程没有 Rust 代码:协议在 Node 的
node:net 上重写,四字节大端长度前缀加 UTF-8 JSON 的编解码是手写的,运行时零 npm 依赖。
扩展在 onlyne 之外完全静默。客户端 spawn 进程时会注入 ONLYNE_ROLE、ONLYNE_SESSION_ID、
ONLYNE_TASK_ID(crates/onlyne-client/src/session/dispatch.rs);三者缺一,就是普通 pi session,
插件不注册任何工具、不打开任何 socket。
只有自身 session id 等于 ONLYNE_SESSION_ID 的那个 session 才会挂载。一个 pi 进程会跑不止一个
session——task 子代理的 sidechain、/new——它们都会带着同一份继承来的环境变量加载本扩展;
因此不是被绑定 session 的那个保持静默,它发出的每一次 onlyne 工具调用都会被拒绝。在常驻模式
(ONLYNE_ROLE、.pi/onlyne.json "resident": true 或 .onlyne/config.toml
placement = "external")下,扩展同时监听顶层 session_switch、session_branch 与
session_tree 事件,使 /resume、/new 及上下文切换后工具自动跟随当前顶层会话。
pi session(由 onlyne-client spawn)
│ 环境变量:ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
│ .pi/onlyne.json:{ "enabled": true, "watch": { "autoStart": true } }
▼
hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{role,session,task_id,pid}}
◀── welcome{role, prose, generation, server, host_capabilities}
├─ prose ──► 系统提示里的一个 `onlyne-role-prose` section,只写一次
├─ report.ready ──► 载荷等待的那道 barrier
◀── assign{envelope, prose, text, attachments, task_id, generation}
├─ prose(仅当 welcome 尚未投递过) ──► 同一个 section
├─ text ──► 一张 `onlyne_assign` 卡片(`From <sender> · <kind> · <id>`),逐字节原样:
│ `triggerTurn` 让空闲会话就此起一个 turn,繁忙时改 `deliverAs:"followUp"` 排队,
│ 于是投递只有这一条消息、不会同时再是一条 user prompt;没有 `sendMessage` 的 pi 退回
│ 普通 user message。`body.image` 作为 pi image part 同行,`attachments` 里的路径是
│ client 已写好的文件
├─ assign_ack{accepted:true}
├─ report.heartbeat{agent} —— 每个 turn 以及任务存续期间每 10 秒发 `running`,
│ 只有 pi 等待输入时才发 `idle`;每次心跳都重新向 pi 推导
├─ 轮次结束的规则归 client:没有 completion 的轮次结束换来一次 nudge,
│ 第二次这样的结束就结算这次投递
◀── nudge{task_id, text} ──► pi user message,逐字节原样(client 自己的句子)
├─ report.complete{outcome, head, details, files} —— ledger 的终态事实,也是最后一份报告
│ └─ client 的应答就是交接点:插件据此让 pi 退出,随后 detach
├─ probe ──► 一条 heartbeat
◀── recycle ──► (未终态则先 complete)→ 停插件 → pi 退出
└─ pi 退出时 detach{reason}
1. 安装
这是一个 pi package:package.json 里声明 pi.extensions: ["./src/index.ts"],pi 用 jiti
直接加载 TypeScript,不需要构建产物。
配合生成的工作区(正规路径)
onlyne server generate 会把 [server].agent_package 复制进
<ws>/.onlyne/agent/<pkg-name>/,再把这条 package 写进 .pi/settings.json,路径相对
settings 文件自身:../.onlyne/agent/<pkg-name>(crates/onlyne-server/src/generate.rs)。
pi 0.85.1 只加载这个写法。项目 packages 里的路径以 settings 文件所在目录(<ws>/.pi)
为基准解析,所以 ../ 那份落到 <ws>/.onlyne/agent/<pkg-name>;裸写的
.onlyne/agent/<pkg-name> 会解析成 <ws>/.pi/.onlyne/agent/<pkg-name>,包被列出来却不
加载。生成的工作区就是 supervisor 拉起的那份,插件随目录一起走,不装全局。
# spec.toml
[server]
agent_package = "/abs/path/to/plugins/onlyne-agent-pi" # 只在 generate 时读一次
onlyne server generate --root <server-root> --out <dir>
生成的 .pi/settings.json 形如:
{ "packages": ["../.onlyne/agent/onlyne-agent-pi"] }
pi list 会把这条列在 “Project packages” 下。要验证真的加载了,就让复制进来的 index.ts
抛错,看报错是否出现。
手工(不经过 generate)
cp -R plugins/onlyne-agent-pi <ws>/.onlyne/agent/onlyne-agent-pi
printf '{"packages":["../.onlyne/agent/onlyne-agent-pi"]}\n' > <ws>/.pi/settings.json
从 npm 装
pi install npm:pi-onlyne # 用户级:这台机器上每个 pi 进程都会加载
发布名是 npm 上的 pi-onlyne,pi install npm:pi-onlyne@<version> 钉住某一版。这条路径会覆盖
普通交互会话,那里没有 ONLYNE_ROLE,扩展保持静默(见 §1 的身份门)。role workspace 想要面
板,不必装到全局:上面那份文件级复制、或者 onlyne server generate,都把插件限定在服务这个
role 的 workspace 里。
一次性 / 测试
pi --session-id <id> -e /abs/path/to/plugins/onlyne-agent-pi -ns -nc
开关文件
<cwd>/.pi/onlyne.json(见 onlyne.json.example):
| 键 | 默认 | 作用 |
|---|---|---|
enabled |
true |
false 时该工作区禁用扩展 |
watch.autoStart |
true |
false 时注册工具但不建连接,需 /onlyne connect |
文件缺失即取各自默认值。文件格式错误时打印一行警告,并保留默认值:一个笔误不该静默关掉一个 role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为 generate 期模板提示),所以 只有本扩展消费它;键名沿用模板里既有的形状。
其余无需配置。工作区 spec.toml 的 session_command 已经按任务拉起 pi
(["pi", "--session-id", "{session}"]),client 负责注入本扩展识别的环境变量。
2. 能力表
hello 只声明真实实现的能力:
| 能力 | 声明条件 | 含义 |
|---|---|---|
register |
始终 | welcome 之后发 session_register{session_id, task_id, generation, pid, title} |
report |
始终 | report.ready / report.heartbeat / report.complete |
inject |
pi.sendUserMessage 存在 |
投递以 assign{…, text} 到达,text 原样注入为 pi user message |
recycle |
始终 | 收到 recycle 先补终态,再停插件并让 pi 退出 |
缺了某个 pi API 时会怎样,宿主怎么应对:
| 缺失项 | 探测时机 | 行为 |
|---|---|---|
registerTool(老 pi) |
session_start |
不注册任何工具;协议通路不受影响,/onlyne status 仍可用 |
sendUserMessage |
session_start |
capability 里去掉 inject,宿主改走 config_get{key:"stdin:<投递文本>"},插件用剩余的注入通道投递 |
before_agent_start 没有 sections |
每次 run 保护性检测 | role prose 进不了指令层——这次 run 没有可供写 section 的对象;stderr 打一行说明,投递文本照常到达 |
appendEntry |
session_start |
不再写 onlyne-assign / onlyne-complete 会话条目 |
ui.setStatus |
调用点保护 | 跳过 footer 状态行 |
ui.setWidget |
调用点保护 | 日常通知继续走 footer 状态行与 [pi-onlyne] stderr 行 |
ctx.shutdown |
调用点保护 | recycle 与 completion 照常结算任务;进程留给操作者自己关闭 |
活动面板
宿主报告有 UI 时(ctx.hasUI:TUI 与 RPC 模式为 true,print 与 JSON 模式为 false)且 ctx.ui.setWidget 可用,日常 onlyne 通知显示在编辑器上方,widget key 为 onlyne。小组件支持 ANSI 与 Tern Surface Protocol(TSP)双渲染面:在 ANSI 终端下使用主题令牌为标题、连接状态、时间戳和方向标记着色排版;在支持 TSP 的 Tern 终端下通过 describe() 输出原生可折叠 card 组件(role: "omp.widget.onlyne",variant: "bare")与语义化 span。入站投递(assign 与 nudge)走的就是那张带 From <sender> · <kind> · <id> 抬头的 onlyne_assign 卡片:wakeUser 用 pi.sendMessage({ customType: "onlyne_assign", display: true, details }) 发出它,registerMessageRenderer("onlyne_assign", ...) 画它,模型读到的也正是这一条——投递不会被画两遍又读两遍。ctx.isIdle() 为真时 wakeUser 传 { triggerTurn: true },把空闲或已结算的常驻会话欠这张卡片的那个 turn 起起来;繁忙时改传 { deliverAs: "followUp" } 排队。标题行显示 role、连接状态、generation、当前 task id 与阶段。其下最多六条事件,按最新在前排列:<= 入站,=> 出站,!! 警告,.. 状态,~~ 重复投递。连续相同事件折成一行并带 xN(常驻会话在客户端首次连通前的后台探测保持静默,不刷屏 reconnecting);面板最多八行,每行最多 96 个显示单元,session_shutdown 时清除。
3. 工具面
仅在 onlyne session 内注册,用 MCP 面所带的同一套 schema(docs/v2-CONTRACT.md
“3b's interface: the tools mount”):一份义务词汇,两个驱动共用。
每个结果只有一句话——send 与 handoff 说发给谁,complete 说结果。工具结果模型看得见, 插件自己的账本没有理由出现在里面。
这三个工具属于挂载它们的那个 session。一个 pi 进程会跑不止一个 session——task 子代理的
sidechain、/new——它们都会带着同一份继承来的环境变量加载本扩展;因此当调用的 session id
不等于 ONLYNE_SESSION_ID 时,调用会被拒绝,并给出同时点名两个 id 的一句话,而不是去结算父
任务。若该 pi 不暴露 session id(ctx.sessionManager 没有 getSessionId),调用照常放行:
此时唯一连上 socket 的仍然是那个由环境变量绑定的挂载。
onlyne_send{to, text, kind?, image?}
经 send 帧提交一个 envelope。kind: "note"(默认)向运行中、空闲或常驻待命的会话发送无状态交流消息,
不带 op_id,不创建任务记录,也不要求调用 onlyne_complete。
kind: "task" 是派人办事,因此带 o-<uuid> 幂等键和新生成的 causality.task。image 是
png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成 body.image。核心限 2 MiB,
只收四种 mime。结果是 sent to <role>。
onlyne_complete{outcome, summary, details?, files?}
显式结束当前任务,outcome 就是 proto 的 Outcome:done、failed、cancelled 或
blocked。summary 是展示用的一行,原样成为 ledger 的 head:空白折叠成单行,截到
200 字符;summary 为空时不带展示行,head 退回最后一段 assistant 文本。details 是完整结果,
files 是它点名的绝对路径:两者原样搭在 report.complete 帧上,也正是下游与发起方收到的东西,
上限由 client 把守,超限正文由 client 用自己的句子拒绝(契约 §3c)。这一调用同时结束所在
session 的进程。client 应答完 completion 报告(见 §4)之后,插件通过 ctx.shutdown() 让 pi
退出。pi 0.85.1 没有 tool-result terminate 处理。结果是 reported <outcome>;若 client 此前
已经结算过该任务,它会用自己的记录作答,此时结果变成 task already settled as <state> at <settled_at>: <head> — this call changed nothing——这是调用并未造成它所请求的那个结果的唯一
情形。
onlyne_handoff{to, text, image?}
把本会话手上的任务交给家族的下一跳。插件发一个 handoff 帧,帧里点名本会话当前持有的任务,
宿主据此为 to 铸一个该家族的子任务:子任务把本任务记为 parent_task,hop 加一,家族 id、
hop 预算、origin、deadline 与 labels 一并随行。结果是 handed on to <role>:子任务 id 与 hop
是 ledger 里的行,不是模型要读回来的东西。client 拒绝时以工具错误原样抛出。image 与 send
工具同一含义:png/jpeg/gif/webp 图片的绝对路径。hop 与
hop 预算留在 envelope 的 causality 里,属于协议数据:投递文本由 client 自己渲染(发送方、
正文、附件路径),本插件原样注入。onlyne_send{kind: "task"} 是触达 role 的另一条路:
那条 envelope 开一个新家族,hop 从 0 起。
4. outcome 判定规则
onlyne_complete 是通向 done 的唯一路径,也是插件唯一的完成路径。插件每个任务只发一次
completion,取以下两者的先到者:
onlyne_complete—— 模型给显式 outcome:done、failed、cancelled或blocked。同一 任务的第二次 completion 被拒(不重报)。summary非空时即 head,原样写出。recycle{outcome}—— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停 插件并退出 pi。
没有 completion 就结束的 turn 在这里不结算任何东西,以错误结束的 turn 同样不结算。结束这件事归 client
(docs/v2-CONTRACT.md 的 “3c. One turn-end rule”):它数这类结束、把自己的一句话作为 nudge
发来,并决定一次始终不上报的投递会变成什么;agent 已死的 session 由它自己的清扫负责。插件把那
句话交给 pi,并且只回答自己交出去了,同时只上报自己的阶段与模型主动要求的 completion——措辞、
计数与结算都只有一个主人,而不是两个。注入的消息还没跑过任何 turn 的任务,无论结算信号说什么都
不动:现在就报终态,等于声称干过一件没发生过的活。
以 provider 错误(stopReason: "error")结束的 turn 也不是结论:pi 会对可重试的 provider 错误
重试,并留在同一次 run 里,而 client 的 nudge 可能在 session 结算之后的一瞬重新启动它,所以在
这里发报告只会与仍在进行的工作赛跑。错误只提供下面说的 head 退路。
head 恒为单行、上限 200 字符,与 client 写入 out_head 和回执携带的内容一致。每个任务的 head
只有一个来源:显式 onlyne_complete 带 summary 时就是它。完全没有带 summary 的调用退到
session 最后说过的话——最后一段 assistant 文本,或失败 turn 留下的错误。错误永远只是这个退路:
它本身从不构成 completion,此外没有任何东西读它;而调用之后再说的话顶不掉调用交出的内容。
报出去的 completion 会结束所在 session 的进程。report.complete 以请求形式发出,client 只有
在结算 session 行、ack 掉投递、并写好 Completion envelope 之后才应答,插件就在这个应答处
让 pi 退出。socket 当时送不出去的 outcome 会被记住,并在下一次 hello 后补发,那次补发的
应答就是结束进程的交接点。被宿主拒掉的 completion 不会让进程退出,任务不会因为退出而丢失。
若 client 此前已经结算过该任务,它会对同一个请求回答 {"already_settled": {state, settled_at, head}} 而不是 null。插件把这个记录原样转交,而不是声称一个并非由本次调用造成的结果,工具结果
也照此说明(见 §3);在重连补发那条路径上没有工具结果可以承载它,记录会进入 [pi-onlyne] 日志。
无论哪种情形任务都保持已结算:以 client 的那一行为准。
completion 是插件最后一份上报;已经交掉任务的 session 不再发心跳,也不再应答 probe。终态的
agent 维度由 client 自己写:随后到达的 detach 会退役这个已无任务的 session,该路径会喂入
AgentGone 并发布这一行(retire_idle_locked,
crates/onlyne-client/src/session/dispatch/retire.rs)。正在离开的进程不给自己声明阶段。
空闲判定
只有 session 在等待用户输入时才算空闲。其余时刻一律读作 running:正在跑的 turn、已排队的
steer 或 follow-up 消息、重试、压缩,以及被后台任务扩展移出 agent 循环的工作。
插件向 pi 查询,不自行假设。ctx.isIdle() 回答是否还有运行、压缩或排队中的续跑,
ctx.hasPendingMessages() 回答输入是否已经在路上;探针缺失或抛错一律读作 running。空闲声明通常
落在 agent_settled,因为 pi 只在一次运行彻底结算之后才触发它;单轮 turn 结束是另一件事。每
10 秒的心跳每次触发都重新向 pi 推导阶段,因此重新开始的运行不会留下过期的空闲读数。
后台任务扩展改变了这个问题。bg_run 与同类工具立即返回,工作继续在子进程里跑,于是 pi 在任务
仍在进行时就等待输入。插件通过该扩展注册的工具认出它,再向它的 EventBus 服务查存活任务列表;
处于 running 的任务会把 session 按在 running 上,于是心跳陈述的是 session 真实的阶段。插件
自己没有任何报告要等它结束:轮次结束时已经不再上报任何东西。没有装这个扩展的 session 没有工具
可认、没有查询,也没有东西要等。
5. 协议说明与偏差
下面每条要么是对 PROTOCOL.md 的明确解读,要么是在实际 client 上实测到的行为。
report 序号基址。 插件自己的
report序号从 1000 起,不是 1。client 把自身的派发事件 (created、资源 attach、ready)写进同一个(generation, seq)水位,reducer 会静默丢弃 水位及以下的报告(crates/onlyne-client/src/reconcile/),所以从 1 起会丢掉最初的观测。 一个插件一个计数器: 会话持有的每个任务都共用同一条report序号发心跳 —— 因为 client 会在同一个任务的两次心跳之间,为该行自己的事件取row.seq + 1;若每个任务每轮只推进一格, 心跳正好撞在那个数上被当作 stale 丢掉。闸门是按任务行判的,所以每条任务记录还带着自己上一次 上报的seq(task.lastSeq,/onlyne status里以taskSeqs呈现),新分配的序号会被抬到它 之上:A@1001、B@1002、A@1003才是正确的形状,任何任务都不会拿到它的行已经接受过的 seq。 心跳轮次也绝不重叠:一轮还在写时收到的心跳请求会并进这一轮,只多做一遍,而不是对同一刻再快照 一次。其余版本语义与规范一致。observed是完整的Observation。report.heartbeat携带整个合法状态元组 (version、generation_live、isolate_after、terminate_after、mismatch_count、agent、delivery、resource、recovery),不是{"state": "running"}这种简写。宿主会反序列化它,先改写 client 主张的那六项,再套用is_legal。本插件主张的 是agent(turn hooks)、resource(进程还活在那块 pane 里,attach 早被宿主派发路径记过) 与host绑定这三件事。delivery、recovery、generation_live、isolate_after、terminate_after、mismatch_count它一个见证都没有:client 在归约前会用自己 的 intent 队列、reducer 历史与角色配置重写这六项,插件填什么都不会被读。任务的 outcome 与公开视图 更已经不在元组里,所以也就不再上报。ready每连接报一次。 宿主的 hand-off 路径 (crates/onlyne-client/src/session/dispatch/delivery.rs::hand_session)在把 session 交给挂载的插件时已经报过ready,所以插件再报一次在宿主侧是 no-op。插件仍然发送:先挂载、后有活正是 ready barrier 描述的情形,而且只花一帧。从不发
cluster_ref。 本插件代表本地 role 说话,从不代表 aggregate;Rust 侧出于同样的 原因把该字段写成skip_serializing_if缺省。probe用一条 heartbeat 应答,对应PROTOCOL.md里 “probedeclares fresh resource observations”。config_get只有当键以stdin:开头时按投递文本处理,这正是PROTOCOL.md为无inject插件记录的重载:该键携带的是与assign的text相同的已渲染字节,原样注入。 其他键记日志后忽略,绝不误读。frame_too_large/bad_frame:超限正文在写出任何字节之前就被拒;帧错误关闭连接并重 连。帧一旦损坏无法重新同步,这与crates/onlyne-wire/src/frame.rs的结论一致。投递按 envelope id 幂等,任务不按 id 一次性使用:去重键是 envelope id。同一条投递重复 到达只注入一次,ack 带
reason: "duplicate";正在运行的任务收到新 envelope,会作为新消息 注入同一个会话。client 每条 envelope 都发新 uuid,所以duplicate只在真正的重投上生效。pane 绑定(Orca tab)。 在 Orca pane 里,插件在每个 heartbeat 上报自己跑在哪:报告
Observation里的observed.host.orca.pane_key(crates/onlyne-client/src/host.rs),环境 报得出时还带上tab_id/leaf_id和终端的handle。这个绑定是继承来的,不是猜的:Orca pane 会把自己那四个ORCA_PANE_KEY/ORCA_TAB_ID/ORCA_LEAF_ID/ORCA_TERMINAL_HANDLE导出给它启动的命令(2026-09-11 实测,Orca 1.4.198),而 client 会把自己的环境继续传给 session 命令。所以跑在 pane 里的那个进程,是唯一能从内部说出「这是哪个 pane」的组件;pi 之后没有任何环节能恢复这个绑定。不在 pane 里时host键整个缺席:普通终端上的 pi 报的是 一条没有 host 字段的 observation,而不是一条 pane 为空的。为此不往 workspace 写任何东西。 已经没有申报文件了:绑定搭在 client 本来就逐帧镜像的 那份 observation 上。没有东西会创建它,所以不存在过期的申报,workspace 的缓存目录也不会 被碰。这既让
integrations/orca-plugin能不读任何路径就把 tab 轴收窄到真会话,也让 supervisor 在会话 结束之后仍然说得出它跑在哪:report.complete会把host带过去。
6. 配置项
| 环境变量 | 必需 | 作用 |
|---|---|---|
ONLYNE_ROLE |
是 | 挂载的 role |
ONLYNE_SESSION_ID |
是 | 挂载的 session id;当前 client 中 session_id 等于 task_id |
ONLYNE_TASK_ID |
是 | 本进程服务的任务;驱动 session_register 与首条 ready |
ONLYNE_SOCKET |
否 | client 为该工作区实际服务的 socket 路径;凡 client 拉起的会话进程都会带上。变量未设置时,插件自己去运行目录读注册文件(<digest>.json),挑出 root 就是本工作区的那个 client |
ORCA_PANE_KEY |
否 | 本进程跑在哪(<tab_id>:<leaf_id>),每个 heartbeat 以 observed.host.orca.pane_key 上报;不在 Orca pane 里时未设置,这也是该字段缺席的原因 |
ORCA_TAB_ID / ORCA_LEAF_ID |
否 | pane 的两个 id;只设了 pane key 时插件会自己解析 |
ORCA_TERMINAL_HANDLE |
否 | 终端 handle,随 pane key 一起上报为 host.orca.handle,也是 orca terminal switch 要的那个值 |
值得记住的常量:插件每 10 秒发一次心跳(heartbeat_timeout_ms 是 30 秒),hello 最多等
5 秒,单次请求超时 30 秒,重连按 1/2/4/8/16/30 秒阶梯退避。
插件自己读一个文件:<cwd>/.pi/onlyne.json(开关,§1)。
另一处读取属于机器而不是工作区:ONLYNE_SOCKET 未设置时,插件遍历机器级运行目录里的
client 注册文件,找出写下本工作区的那个(§7)。
7. 故障排查
| 现象 | 原因 | 检查 |
|---|---|---|
看不到 [pi-onlyne] session … |
三个环境变量缺一,或 enabled 为 false |
env | grep ONLYNE_;cat .pi/onlyne.json |
socket unresolved: onlyne: no client is registered for <workspace> … |
该工作区没有 onlyne-client run,所以运行目录里没有哪个注册文件的 root 是这棵树 |
起 client,或 onlyne-client status;这条消息会点出运行目录,以及它实际读到的每个注册文件 |
socket unresolved: … N clients there name runtime pi … ambiguous |
有多个已注册的 client 都在跑 pi 会话,而它们的 root 都不包含本工作区,于是没有唯一可拨的 client | 用 ONLYNE_SOCKET 显式指定 socket,或为本工作区起 client |
socket error: connect ENOENT <路径> |
消息里的路径没人 bind:注入它的那个 client 已经停了 | onlyne-client status 看它当前服务的 socket,再看 client 日志里带 socket = <路径> 的那行 |
反复 reconnecting in 4000ms |
client 已停或 socket 被替换 | onlyne --server-root … roles |
ready refused: internal: unknown session for … |
插件为 client 从未暂存的任务报了 ready(手工起 pi 时的正常现象) | 让 client 拉起 pi,而不是手工起 |
assign 一直不来 |
client 的 session_command 没能拉起 pi,或 inject 被降级 |
client 日志里的 spawn 行;/onlyne status 看能力集 |
ledger 停在 in_flight |
还没有 completion:没跑过 turn(注入的消息尚未执行),或 turn 结束时没有 completion,而 client 还没结算这次投递 | pi session 文件里的 onlyne-assign 条目和其后注入的 nudge 句子;/onlyne status 看任务与阶段 |
hello 后立刻 forbidden / 断连 |
mount role 与 client 的 role 不一致 | hello.args.mount.role 对该工作区的 role |
frame_too_large |
正文超过 8 MiB | 只会由超限的出站图片触发;上限来自核心 |
| 工具缺失 | 该 pi 版本没有 pi.registerTool |
/onlyne status;对照上面的能力表 |
后台任务还在跑,会话却显示 idle |
没装后台任务扩展,或它的 EventBus 服务没在时限内回答状态查询,插件看不到自己留下的运行中工作 | [pi-onlyne] 日志里后台探针那一行;同一个 pi session 里的 bg_status 会列出存活任务 |
| supervisor 看板一个 tab 都不列 | 没有 live session 上报过 pane:适配器版本早于这条上报,或这个 pi 不在 Orca pane 里 | onlyne --server-root … sessions --json 看 projection.observed.host.orca.pane_key;在 pane 里跑 env | grep ORCA_ |
/onlyne status 打印实时状态(connected、socket、role、sessionId、generation、
agentState、seq、taskSeqs、tasks、pendingCompletions、lastError 与计数器);
/onlyne connect / /onlyne disconnect 手工开合连接。
8. 开发与验证
cd plugins/onlyne-agent-pi
node --test src/*.test.mjs # 帧编解码、协议词汇、agent 状态机、配置、socket 路径
src/agent.live.test.mjs 只在 target/debug/onlyne-client 与 onlyne-server 存在时运行。
crates/onlyne-testkit/e2e/pi-live.sh 是端到端用例:pi 不在 PATH 或没有可用模型凭据时 SKIP
(exit 0),否则用真 client 跑一个真任务到 acked。
cd ../..
ONLYNE_BACKEND=fake BIN_DIR=target/debug bash crates/onlyne-testkit/e2e/pi-live.sh
用例先 source 公共 helper,再自己导出 ONLYNE_BACKEND=exec,于是 pi 由 client 亲自 spawn,
stdin 是一条 client 持住不关的管道。agent 自己的输出落在
<ws>/.onlyne/logs/session-<task>.log。