pi-onlyne 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,15 @@ Outside an onlyne session the extension is inert. The client injects `ONLYNE_ROL
14
14
  (`crates/onlyne-client/src/session/dispatch.rs`). With any of the three missing, this is an
15
15
  ordinary pi session: the plugin registers nothing and opens nothing.
16
16
 
17
+ Only the session whose own id equals `ONLYNE_SESSION_ID` mounts. A pi process runs more
18
+ than one session — a `task` subagent's sidechain, `/new` — and each of them loads this
19
+ extension with the same inherited environment, so a session that is not the bound one
20
+ stays inert and every onlyne tool it calls is refused. In resident mode (`ONLYNE_ROLE` or
21
+ `.pi/onlyne.json` `"resident": true` / `.onlyne/config.toml` `placement = "external"`), the
22
+ extension also tracks top-level `session_switch`, `session_branch`, and `session_tree`
23
+ events so `/resume`, `/new`, and context rollover keep the tools bound to the active
24
+ top-level session.
25
+
17
26
  ```
18
27
  pi session (spawned by onlyne-client)
19
28
  │ env: ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
@@ -25,14 +34,17 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
25
34
  ├─ report.ready ──► the barrier the task payload waits behind
26
35
  ◀── assign{envelope, prose, text, attachments, task_id, generation}
27
36
  ├─ prose, when the welcome did not already deliver it ──► the same section
28
- ├─ text ──► pi user message (deliverAs:"followUp"), byte for byte; `body.image`
29
- │ rides as a pi image part, and `attachments` names files the client wrote
37
+ ├─ text ──► one `onlyne_assign` card (`From <sender> · <kind> · <id>`), byte for byte:
38
+ │ `triggerTurn` starts the turn an idle session owes it and `deliverAs:"followUp"` queues
39
+ │ while busy, so the delivery is one message and never also a user prompt; a pi without
40
+ │ `sendMessage` takes it as a plain user message instead. `body.image` rides as a pi image
41
+ │ part, and `attachments` names files the client wrote
30
42
  ├─ assign_ack{accepted:true}
31
43
  ├─ report.heartbeat{agent} — `running` per turn and every 10s while a task is live,
32
44
  │ `idle` only while pi waits for input; every beat re-derives it from pi
33
45
  ├─ the client owns the turn-end rule: a turn that ends without a completion
34
46
  │ earns one nudge, and the second such ending settles the delivery
35
- ◀── nudge{task_id, text} ──► pi user message, byte for byte (the client's sentence)
47
+ ◀── nudge{task_id, text} ──► the same card route, byte for byte (the client's sentence)
36
48
  ├─ report.complete{outcome, head, details, files} — the ledger's terminal fact, and the last report
37
49
  │ └─ the client's answer is the handover: pi is asked to shut down, then detaches
38
50
  ├─ probe ──► one heartbeat
@@ -145,7 +157,7 @@ What happens when a pi API is missing, and what the host does then:
145
157
 
146
158
  ### Activity panel
147
159
 
148
- When the host reports a UI (`ctx.hasUI`, true in the TUI and RPC modes, false in print and JSON modes) and `ctx.ui.setWidget` is available, routine onlyne notices draw in the panel above the editor with widget key `onlyne`. The header shows role, connection state, generation, the current task id, and phase. Below it, up to six newest-first events use `<=` for inbound frames, `=>` for outbound frames, `!!` for warnings, `..` for state changes, and `~~` for duplicate deliveries. Repeated identical events fold into one line with `xN`; the panel holds at most eight lines, each capped at 96 cells, and `session_shutdown` clears it.
160
+ When the host reports a UI (`ctx.hasUI`, true in the TUI and RPC modes, false in print and JSON modes) and `ctx.ui.setWidget` is available, routine onlyne notices draw in the panel above the editor with widget key `onlyne`. The widget registers a dual-surface component: on ANSI terminals it renders theme-styled lines (`accent` role badge, status-toned connection state, dimmed timestamps, and semantic event markers), and on Tern Surface Protocol (TSP) hosts it implements `describe()` to emit a native collapsible `card` (`role: "omp.widget.onlyne"`, `variant: "bare"`) with structured spans. An inbound delivery (`assign` and `nudge`) travels as the visible `onlyne_assign` custom message card (`From <sender> · <kind> · <id>`) that `registerMessageRenderer("onlyne_assign", ...)` draws: it reaches the model as that one message, so a delivery is never both drawn twice and read twice. `wakeUser` passes `{ triggerTurn: true }` when `ctx.isIdle()` is true, which is what starts the turn an empty or settled resident session owes the card (and `{ deliverAs: "followUp" }` while busy). The header shows role, connection state, generation, the current task id, and phase. Below it, up to six newest-first events use `<=` for inbound frames, `=>` for outbound frames, `!!` for warnings, `..` for state changes, and `~~` for duplicate deliveries. Repeated identical events fold into one line with `xN` (and pre-connection resident polling stays quiet until a socket connects); the panel holds at most eight lines, each capped at 96 cells, and `session_shutdown` clears it.
149
161
 
150
162
  ## 3. Tools
151
163
 
@@ -157,10 +169,20 @@ Each result is one plain sentence — the recipient for a send or a handoff, the
157
169
  for a completion. A tool result is model-visible, and nothing about the plugin's own
158
170
  bookkeeping belongs in it.
159
171
 
172
+ The three tools belong to the session that mounted them. A pi process runs more than one
173
+ session — a `task` subagent's sidechain, `/new` — and every one of them loads this
174
+ extension with the same inherited environment, so a call from a session whose id is not
175
+ `ONLYNE_SESSION_ID` is refused with a sentence naming both ids instead of settling the
176
+ parent's task. A pi that does not expose a session id (there is no `getSessionId` in
177
+ `ctx.sessionManager`) is allowed through: the env-bound mount is still the only one that
178
+ connected.
179
+
160
180
  ### `onlyne_send{to, text, kind?, image?}`
161
181
 
162
- Sends one envelope on the `send` frame. `kind: "note"` (the default) is free text and
163
- carries no `op_id`. `kind: "task"` hands work to a role, so it carries an `o-<uuid>`
182
+ Sends one envelope on the `send` frame. `kind: "note"` (the default) sends stateless
183
+ conversational text to a working, idle, or parked resident session, carries no `op_id`,
184
+ opens no task record, and requires no `onlyne_complete`. `kind: "task"` hands tracked work
185
+ to a role, so it carries an `o-<uuid>`
164
186
  idempotency key and a fresh `causality.task`. `image` is an absolute path to a
165
187
  png/jpeg/gif/webp file: the plugin reads it, base64-encodes it and attaches it as
166
188
  `body.image`. The core caps that at 2 MiB and accepts four mime types. The result is
@@ -178,7 +200,10 @@ receive, with the client holding the ceiling and refusing an oversize body with
178
200
  sentence (§3c of the contract). The call also ends the session's process: once the client
179
201
  has acknowledged the completion report (§4), the plugin asks pi to shut down through
180
202
  `ctx.shutdown()`. pi 0.85.1 has no tool-result `terminate` handling. The result is
181
- `reported <outcome>`.
203
+ `reported <outcome>` — or, when a client that had already settled the task answers the
204
+ report with its own record, `task already settled as <state> at <settled_at>: <head> —
205
+ this call changed nothing`, which is the one case where the call did not cause the
206
+ outcome it asked for.
182
207
 
183
208
  ### `onlyne_handoff{to, text, image?}`
184
209
 
@@ -196,33 +221,36 @@ of its own at hop 0.
196
221
 
197
222
  ## 4. Outcome rules
198
223
 
199
- `onlyne_complete` is the only path to `done`. The plugin sends one completion per task,
200
- at the first of these events:
224
+ `onlyne_complete` is the only path to `done`, and it is the plugin's one completion path.
225
+ The plugin sends one completion per task, at the first of these events:
201
226
 
202
227
  1. **`onlyne_complete`** — the model gives an explicit outcome: `done`, `failed`,
203
228
  `cancelled` or `blocked`. A later completion for the same task is refused (not
204
229
  re-reported). Its non-empty `summary` is the head.
205
- 2. **An errored turn** — the turn ended with a provider error (`stopReason: "error"`).
206
- That is proof on its own, so the plugin reports `failed` at once, with the error as
207
- the head. The report itself waits until pi is waiting for input, so the beat that
208
- carries it states a phase the session really has.
209
- 3. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
230
+ 2. **`recycle{outcome}`** — the host is tearing the session down. The plugin settles an
210
231
  unsettled task with the host's outcome first, then stops and exits pi.
211
232
 
212
- A turn that ends cleanly without a completion settles nothing here. The client owns that
213
- rule (`docs/v2-CONTRACT.md`, "3c. One turn-end rule"): it counts the endings, sends its
214
- own sentence as a `nudge`, and decides what a delivery that never reports becomes. The
215
- plugin hands the sentence to pi and answers only that it did, so the wording, the count
216
- and the settlement have one owner rather than two. A task whose injected message has not
217
- run a turn is left alone whatever a settle signal says: reporting now would claim work
218
- that never happened.
233
+ A turn that ends without a completion settles nothing here, and neither does a turn that
234
+ ends with one. The endings belong to the client (`docs/v2-CONTRACT.md`, "3c. One
235
+ turn-end rule"): it counts them, sends its own sentence as a `nudge`, and decides what a
236
+ delivery that never reports becomes; its own sweeps own a session whose agent died. The
237
+ plugin hands the nudge to pi, answers only that it did, and reports only its phase and
238
+ the completion the model asked for — one owner for the wording, the count and the
239
+ settlement, rather than two. A task whose injected message has not run a turn is left
240
+ alone whatever a settle signal says: reporting now would claim work that never happened.
241
+
242
+ A turn that ended with a provider error (`stopReason: "error"`) is not a verdict either.
243
+ pi retries a retryable provider error and stays inside the same run, and the client's
244
+ nudge can restart a session a moment after it settles, so a report sent on the error
245
+ would race work that is still going. The error only supplies the head fallback described
246
+ below.
219
247
 
220
248
  `head` is a single line, capped at 200 characters; it matches what the client puts in
221
249
  `out_head` and what the receipt carries. Each task has one source for it: the `summary`
222
- of the explicit `onlyne_complete` call when that call carried one, or the error a failed
223
- turn reported. The last assistant text is the fallback for a call whose `summary` is
224
- empty — a sentence spoken after such a call cannot replace what the call handed over, and
225
- nothing else reads it.
250
+ of the explicit `onlyne_complete` call when that call carried one. A call whose `summary`
251
+ is empty falls back to what the session last said — the last assistant text, or the error
252
+ of a turn that failed. The error is only ever that fallback: it is never a completion, and
253
+ a sentence spoken after such a call cannot replace what the call handed over.
226
254
 
227
255
  A reported completion ends the session's process. `report.complete` goes out as a request,
228
256
  and the client answers it only after it has settled the session row, acked the delivery
@@ -231,6 +259,13 @@ outcome the socket could not carry is queued and flushed after the next `hello`,
231
259
  flush's answer is the handover that ends the process. A completion the host refused leaves
232
260
  the process running, so an exit never loses the task.
233
261
 
262
+ A client that had already settled this task answers the same request with
263
+ `{"already_settled": {state, settled_at, head}}` instead of `null`. The plugin hands that
264
+ record on rather than claiming an outcome it did not cause, and the tool result says so
265
+ (§3); on the reconnect flush, where no tool result exists to carry it, the record reaches
266
+ the `[pi-onlyne]` log. Either way the task stays settled: the client's row is the
267
+ authority.
268
+
234
269
  The completion is the plugin's last report, and a session that has completed one answers
235
270
  no further beat and no `probe`. The terminal agent state is the client's own write: the
236
271
  `detach` frame that follows retires the task-free session, and that path feeds
@@ -256,9 +291,9 @@ A background-task extension changes the question. `bg_run` and its siblings retu
256
291
  once and the work continues in a child process, so pi waits for input while the session's
257
292
  task is still in flight. The plugin recognises the extension by the tools it registered
258
293
  and asks its EventBus service for the live task list; a task in `running` status holds
259
- the session at `running`, and the one report the plugin owes at a turn end — the failure
260
- it witnessed itself — waits that out. On a session without the extension there is no tool
261
- to recognise, no query, and nothing to wait for.
294
+ the session at `running`, so the beat states a phase the session really has. No report of
295
+ the plugin's own waits on that: nothing is reported at a turn end any more. On a session
296
+ without the extension there is no tool to recognise, no query, and nothing to wait for.
262
297
 
263
298
  ## 5. Protocol notes and deviations
264
299
 
@@ -403,6 +438,8 @@ agent's own output lands in `<ws>/.onlyne/logs/session-<task>.log`.
403
438
 
404
439
  在一个 onlyne 会话之外,扩展不会执行任何操作。客户端会向其启动的每个进程注入 `ONLYNE_ROLE`、`ONLYNE_SESSION_ID` 和 `ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`)。任一变量缺失时,这就是一个普通的 pi 会话:插件不注册任何内容,也不打开任何内容。
405
440
 
441
+ 只有自身会话 id 等于 `ONLYNE_SESSION_ID` 的会话才会挂载。一个 pi 进程会跑不止一个会话——`task` 子代理的 sidechain、`/new`——它们都带着同一份继承来的环境变量加载本扩展,因此不是被绑定会话的那个会话保持静默,它发出的每一次 onlyne 工具调用都会被拒绝。
442
+
406
443
  ```
407
444
  pi session (spawned by onlyne-client)
408
445
  │ env: ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
@@ -522,6 +559,8 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
522
559
 
523
560
  每个结果只有一句平实的话——send 与 handoff 给出发往的角色,complete 给出结果。工具结果是模型可见的,插件自己的簿记没有理由出现在里面。
524
561
 
562
+ 这三个工具属于挂载它们的那个会话。一个 pi 进程会跑不止一个会话——`task` 子代理的 sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;因此,当调用的会话 id 不等于 `ONLYNE_SESSION_ID` 时,调用会被拒绝,并给出同时点名两个 id 的一句话,而不是去结算父任务。若该 pi 不暴露会话 id(`ctx.sessionManager` 没有 `getSessionId`),调用照常放行:此时唯一连上 socket 的仍然是那个由环境变量绑定的挂载。
563
+
525
564
  ### `onlyne_send{to, text, kind?, image?}`
526
565
 
527
566
  通过 `send` 帧发送一个信封。`kind: "note"`(默认值)是自由文本,不携带 `op_id`。`kind: "task"` 将工作移交给一个角色,因此携带 `o-<uuid>` 幂等键和新的 `causality.task`。`image` 是 `png/jpeg/gif/webp` 文件的绝对路径:插件读取该文件,进行 base64 编码,并将其作为 `body.image` 附加。核心将该文件限制为 2 MiB,并接受四种 mime 类型。
@@ -530,7 +569,7 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
530
569
 
531
570
  ### `onlyne_complete{outcome, summary, details?, files?}`
532
571
 
533
- 以明确结果结束当前任务——即 proto 的 `Outcome`:`done`、`failed`、`cancelled` 或 `blocked`。`summary` 是展示用的一行,原样成为账本的 `head`:空白折叠为一行,文本在 200 个字符处截断;`summary` 为空时不携带展示行,head 回退到最后一条助手文本。`details` 是完整结果,`files` 是它所点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,上限由客户端把守,超限正文由客户端用自己的句子拒绝(契约 §3c)。该调用也会结束会话进程:客户端确认完成报告后(见 §4),插件会通过 `ctx.shutdown()` 请求 pi 关闭。pi 0.85.1 没有工具结果的 `terminate` 处理。结果为 `reported <outcome>`。
572
+ 以明确结果结束当前任务——即 proto 的 `Outcome`:`done`、`failed`、`cancelled` 或 `blocked`。`summary` 是展示用的一行,原样成为账本的 `head`:空白折叠为一行,文本在 200 个字符处截断;`summary` 为空时不携带展示行,head 回退到最后一条助手文本。`details` 是完整结果,`files` 是它所点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,上限由客户端把守,超限正文由客户端用自己的句子拒绝(契约 §3c)。该调用也会结束会话进程:客户端确认完成报告后(见 §4),插件会通过 `ctx.shutdown()` 请求 pi 关闭。pi 0.85.1 没有工具结果的 `terminate` 处理。结果为 `reported <outcome>`;若客户端此前已经结算过该任务,它会用自己的记录作答,此时结果变成 `task already settled as <state> at <settled_at>: <head> — this call changed nothing`——这是调用并未造成它所请求的那个结果的唯一情形。
534
573
 
535
574
  ### `onlyne_handoff{to, text, image?}`
536
575
 
@@ -538,18 +577,21 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
538
577
 
539
578
  ## 4. 结果规则
540
579
 
541
- `onlyne_complete` 是通向 `done` 的唯一路径。插件为每个任务发送一次完成报告,在以下事件中第一个发生时发送:
580
+ `onlyne_complete` 是通向 `done` 的唯一路径,也是插件唯一的完成路径。插件为每个任务发送一次完成报告,在以下事件中第一个发生时发送:
542
581
 
543
582
  1. **`onlyne_complete`**——模型给出明确结果:`done`、`failed`、`cancelled` 或 `blocked`。同一任务后续的完成调用会被拒绝,不会再次报告。其非空 `summary` 就是 head。
544
- 2. **出错的轮次**——该轮次以模型提供方错误结束(`stopReason: "error"`)。这本身即可证明出错,因此插件立即报告 `failed`,并将错误作为 head。报告本身要等到 pi 正在等待输入时才发出,因此承载它的那次心跳陈述的是会话真实的阶段。
545
- 3. **`recycle{outcome}`**——主机正在拆除会话。插件先使用主机给出的结果确定尚未确定状态的任务,然后停止并退出 pi。
583
+ 2. **`recycle{outcome}`**——主机正在拆除会话。插件先使用主机给出的结果确定尚未确定状态的任务,然后停止并退出 pi。
584
+
585
+ 没有完成就结束的轮次在这里不结算任何东西,以错误结束的轮次同样不结算。结束这件事归 client(`docs/v2-CONTRACT.md` 的「3c. One turn-end rule」):它统计这类结束、把自己的一句话作为 `nudge` 发来,并决定一次始终不上报的投递会变成什么;agent 已死的会话由它自己的清扫负责。插件把那句话交给 pi,并且只回答自己交出去了,同时只上报自己的阶段与模型主动要求的完成——措辞、计数与结算都只有一个拥有者,而不是两个。注入消息尚未跑过任何轮次的任务,无论结算信号说什么都不动:现在就上报等于宣称从未发生的工作已经完成。
546
586
 
547
- 正常结束、但没有完成的轮次在这里不结算任何东西。那条规则由 client 拥有(`docs/v2-CONTRACT.md` 的「3c. One turn-end rule」):它统计这类结束、把自己的一句话作为 `nudge` 发来,并决定一次始终不上报的投递会变成什么。插件把那句话交给 pi,并且只回答自己交出去了,于是措辞、计数与结算都只有一个拥有者,而不是两个。注入消息尚未跑过任何轮次的任务,无论结算信号说什么都不动:现在就上报等于宣称从未发生的工作已经完成。
587
+ 以提供方错误(`stopReason: "error"`)结束的轮次也不是结论:pi 会对可重试的提供方错误重试,并留在同一次运行里,而 client 的 nudge 可能在会话结算之后的一瞬间重新启动它,所以在这里发出报告只会与仍在进行的工作赛跑。错误只提供下面说的 head 回退。
548
588
 
549
- `head` 是单行文本,上限为 200 个字符;它与客户端写入 `out_head` 的内容以及回执携带的内容一致。每个任务的 `head` 只有一个来源:显式 `onlyne_complete` 调用携带 `summary` 时就是它,否则是失败轮次报告的错误。完全没有携带 `summary` 的调用,回退到最后一条助手文本;此类调用之后说出的句子不能替代调用已经交付的内容,也不会被其他任何内容读取。
589
+ `head` 是单行文本,上限为 200 个字符;它与客户端写入 `out_head` 的内容以及回执携带的内容一致。每个任务的 `head` 只有一个来源:显式 `onlyne_complete` 调用携带 `summary` 时就是它。完全没有携带 `summary` 的调用回退到会话最后说过的话——最后一条助手文本,或失败轮次留下的错误。错误永远只是这个回退:它本身从不构成完成,而此类调用之后说出的句子不能替代调用已经交付的内容。
550
590
 
551
591
  已报告的完成会结束会话进程。`report.complete` 以请求形式发出;客户端仅在完成会话记录的处理、确认投递并写入 `Completion` 信封后才回复。插件在该回复到达时请求 pi 关闭。套接字无法传输的结果会进入队列,并在下一次 `hello` 之后刷新;该次刷新收到的回复即为结束进程的交接。主机拒绝的完成会让进程继续运行,因此进程退出不会导致任务丢失。
552
592
 
593
+ 若客户端此前已经结算过该任务,它会对同一个请求回答 `{"already_settled": {state, settled_at, head}}` 而不是 `null`。插件把这个记录原样转交,而不是声称一个并非由本次调用造成的结果,工具结果也照此说明(见 §3);在重连补发那条路径上,没有工具结果可以承载它,记录会进入 `[pi-onlyne]` 日志。无论哪种情形任务都保持已结算:以客户端的那一行为准。
594
+
553
595
  完成报告是插件发出的最后一份报告;已经完成任务的会话不再回应心跳,也不再回应 `probe`。终态的 agent 维度由客户端自己写入:随后到达的 `detach` 会退役这个已无任务的会话,该路径会喂入 `AgentGone` 并发布这一行(`retire_idle_locked`,`crates/onlyne-client/src/session/dispatch/retire.rs`)。正在离开的进程不为自己声明阶段。
554
596
 
555
597
  ### 空闲判定
@@ -558,7 +600,7 @@ pi API 缺失时会发生什么,以及主机随后如何处理:
558
600
 
559
601
  插件向 pi 查询,而不是自行假设。`ctx.isIdle()` 回答是否还有运行、压缩或排队中的续跑,`ctx.hasPendingMessages()` 回答输入是否已经在路上;缺失或抛错的探针一律读作 `running`。`agent_settled` 通常是空闲声明落地的时刻,因为 pi 只在一次运行彻底结算之后才触发该事件;单个轮次的结束是另一个时刻。每 10 秒的心跳会在每次触发时重新向 pi 推导阶段,因此重新开始的运行不会留下过期的空闲读数。
560
602
 
561
- 后台任务扩展改变了这个问题。`bg_run` 及其同类工具立即返回,工作继续在子进程中运行,于是 pi 在会话任务仍在进行时等待输入。插件通过该扩展注册的工具识别它,并向它的 EventBus 服务查询存活任务列表;处于 `running` 状态的任务会让会话保持 `running`,插件在轮次结束时欠下的那份报告——它自己目睹的失败——也要等它结束。没有安装该扩展的会话没有可识别的工具、没有查询,也就没有可等待的东西。
603
+ 后台任务扩展改变了这个问题。`bg_run` 及其同类工具立即返回,工作继续在子进程中运行,于是 pi 在会话任务仍在进行时等待输入。插件通过该扩展注册的工具识别它,并向它的 EventBus 服务查询存活任务列表;处于 `running` 状态的任务会让会话保持 `running`,于是心跳陈述的是会话真实的阶段。插件自己没有任何报告要等它结束:轮次结束时已经不再上报任何东西。没有安装该扩展的会话没有可识别的工具、没有查询,也就没有可等待的东西。
562
604
 
563
605
  ## 5. 协议说明与差异
564
606
 
package/README.zh.md CHANGED
@@ -10,6 +10,13 @@ socket——机器级运行目录里的 `<digest>.sock`(`/tmp/onlyne-<uid>/`
10
10
  `ONLYNE_TASK_ID`(`crates/onlyne-client/src/session/dispatch.rs`);三者缺一,就是普通 pi session,
11
11
  插件不注册任何工具、不打开任何 socket。
12
12
 
13
+ 只有自身 session id 等于 `ONLYNE_SESSION_ID` 的那个 session 才会挂载。一个 pi 进程会跑不止一个
14
+ session——`task` 子代理的 sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;
15
+ 因此不是被绑定 session 的那个保持静默,它发出的每一次 onlyne 工具调用都会被拒绝。在常驻模式
16
+ (`ONLYNE_ROLE`、`.pi/onlyne.json` `"resident": true` 或 `.onlyne/config.toml`
17
+ `placement = "external"`)下,扩展同时监听顶层 `session_switch`、`session_branch` 与
18
+ `session_tree` 事件,使 `/resume`、`/new` 及上下文切换后工具自动跟随当前顶层会话。
19
+
13
20
  ```
14
21
  pi session(由 onlyne-client spawn)
15
22
  │ 环境变量:ONLYNE_ROLE / ONLYNE_SESSION_ID / ONLYNE_TASK_ID
@@ -21,8 +28,11 @@ hello{protocol:1, plugin:"pi-onlyne", kind:"agent", capabilities:[…], mount:{r
21
28
  ├─ report.ready ──► 载荷等待的那道 barrier
22
29
  ◀── assign{envelope, prose, text, attachments, task_id, generation}
23
30
  ├─ prose(仅当 welcome 尚未投递过) ──► 同一个 section
24
- ├─ text ──► pi user message(deliverAs:"followUp"),逐字节原样;`body.image`
25
- │ 作为 pi image part 同行,`attachments` 里的路径是 client 已写好的文件
31
+ ├─ text ──► 一张 `onlyne_assign` 卡片(`From <sender> · <kind> · <id>`),逐字节原样:
32
+ │ `triggerTurn` 让空闲会话就此起一个 turn,繁忙时改 `deliverAs:"followUp"` 排队,
33
+ │ 于是投递只有这一条消息、不会同时再是一条 user prompt;没有 `sendMessage` 的 pi 退回
34
+ │ 普通 user message。`body.image` 作为 pi image part 同行,`attachments` 里的路径是
35
+ │ client 已写好的文件
26
36
  ├─ assign_ack{accepted:true}
27
37
  ├─ report.heartbeat{agent} —— 每个 turn 以及任务存续期间每 10 秒发 `running`,
28
38
  │ 只有 pi 等待输入时才发 `idle`;每次心跳都重新向 pi 推导
@@ -135,7 +145,7 @@ role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为
135
145
 
136
146
  ### 活动面板
137
147
 
138
- 宿主报告有 UI 时(`ctx.hasUI`:TUI 与 RPC 模式为 true,print 与 JSON 模式为 false)且 `ctx.ui.setWidget` 可用,日常 onlyne 通知显示在编辑器上方,widget key 为 `onlyne`。标题行显示 role、连接状态、generation、当前 task id 与阶段。其下最多六条事件,按最新在前排列:`<=` 入站,`=>` 出站,`!!` 警告,`..` 状态,`~~` 重复投递。连续相同事件折成一行并带 `xN`;面板最多八行,每行最多 96 个显示单元,`session_shutdown` 时清除。
148
+ 宿主报告有 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` 时清除。
139
149
 
140
150
  ## 3. 工具面
141
151
 
@@ -145,9 +155,16 @@ role。client 不读这个文件(计划 §11 已把旧 readiness 门降级为
145
155
  每个结果只有一句话——send 与 handoff 说发给谁,complete 说结果。工具结果模型看得见,
146
156
  插件自己的账本没有理由出现在里面。
147
157
 
158
+ 这三个工具属于挂载它们的那个 session。一个 pi 进程会跑不止一个 session——`task` 子代理的
159
+ sidechain、`/new`——它们都会带着同一份继承来的环境变量加载本扩展;因此当调用的 session id
160
+ 不等于 `ONLYNE_SESSION_ID` 时,调用会被拒绝,并给出同时点名两个 id 的一句话,而不是去结算父
161
+ 任务。若该 pi 不暴露 session id(`ctx.sessionManager` 没有 `getSessionId`),调用照常放行:
162
+ 此时唯一连上 socket 的仍然是那个由环境变量绑定的挂载。
163
+
148
164
  ### `onlyne_send{to, text, kind?, image?}`
149
165
 
150
- 经 `send` 帧提交一个 envelope。`kind: "note"`(默认)是自由文本,不带 `op_id`。
166
+ 经 `send` 帧提交一个 envelope。`kind: "note"`(默认)向运行中、空闲或常驻待命的会话发送无状态交流消息,
167
+ 不带 `op_id`,不创建任务记录,也不要求调用 `onlyne_complete`。
151
168
  `kind: "task"` 是派人办事,因此带 `o-<uuid>` 幂等键和新生成的 `causality.task`。`image` 是
152
169
  png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成 `body.image`。核心限 2 MiB,
153
170
  只收四种 mime。结果是 `sent to <role>`。
@@ -160,7 +177,10 @@ png/jpeg/gif/webp 的绝对路径:插件读出内容,base64 编码后挂成
160
177
  `files` 是它点名的绝对路径:两者原样搭在 `report.complete` 帧上,也正是下游与发起方收到的东西,
161
178
  上限由 client 把守,超限正文由 client 用自己的句子拒绝(契约 §3c)。这一调用同时结束所在
162
179
  session 的进程。client 应答完 completion 报告(见 §4)之后,插件通过 `ctx.shutdown()` 让 pi
163
- 退出。pi 0.85.1 没有 tool-result `terminate` 处理。结果是 `reported <outcome>`。
180
+ 退出。pi 0.85.1 没有 tool-result `terminate` 处理。结果是 `reported <outcome>`;若 client 此前
181
+ 已经结算过该任务,它会用自己的记录作答,此时结果变成 `task already settled as <state> at
182
+ <settled_at>: <head> — this call changed nothing`——这是调用并未造成它所请求的那个结果的唯一
183
+ 情形。
164
184
 
165
185
  ### `onlyne_handoff{to, text, image?}`
166
186
 
@@ -175,32 +195,40 @@ hop 预算留在 envelope 的 `causality` 里,属于协议数据:投递文
175
195
 
176
196
  ## 4. outcome 判定规则
177
197
 
178
- `onlyne_complete` 是通向 `done` 的唯一路径。插件每个任务只发一次 completion,取以下三者的先到者:
198
+ `onlyne_complete` 是通向 `done` 的唯一路径,也是插件唯一的完成路径。插件每个任务只发一次
199
+ completion,取以下两者的先到者:
179
200
 
180
201
  1. **`onlyne_complete`** —— 模型给显式 outcome:`done`、`failed`、`cancelled` 或 `blocked`。同一
181
202
  任务的第二次 completion 被拒(不重报)。`summary` 非空时即 head,原样写出。
182
- 2. **turn 出错** —— turn 以 provider 错误告终(`stopReason: "error"`)。这本身就是证据,插件立即
183
- 报 `failed`,错误信息当 head。上报本身要等到 pi 正在等待输入时才发出,所以承载它的那次心跳
184
- 陈述的是 session 真实的阶段。
185
- 3. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
203
+ 2. **`recycle{outcome}`** —— 宿主拆 session。插件先按宿主给的 outcome 结算未终态的任务,再停
186
204
  插件并退出 pi。
187
205
 
188
- 干净结束、却没有 completion 的 turn 在这里不结算任何东西。那条规则归 client
206
+ 没有 completion 就结束的 turn 在这里不结算任何东西,以错误结束的 turn 同样不结算。结束这件事归 client
189
207
  (`docs/v2-CONTRACT.md` 的 “3c. One turn-end rule”):它数这类结束、把自己的一句话作为 `nudge`
190
- 发来,并决定一次始终不上报的投递会变成什么。插件把那句话交给 pi,并且只回答自己交出去了,
191
- 于是措辞、计数与结算都只有一个主人,而不是两个。注入的消息还没跑过任何 turn 的任务,无论结算
192
- 信号说什么都不动:现在就报终态,等于声称干过一件没发生过的活。
208
+ 发来,并决定一次始终不上报的投递会变成什么;agent 已死的 session 由它自己的清扫负责。插件把那
209
+ 句话交给 pi,并且只回答自己交出去了,同时只上报自己的阶段与模型主动要求的 completion——措辞、
210
+ 计数与结算都只有一个主人,而不是两个。注入的消息还没跑过任何 turn 的任务,无论结算信号说什么都
211
+ 不动:现在就报终态,等于声称干过一件没发生过的活。
212
+
213
+ 以 provider 错误(`stopReason: "error"`)结束的 turn 也不是结论:pi 会对可重试的 provider 错误
214
+ 重试,并留在同一次 run 里,而 client 的 nudge 可能在 session 结算之后的一瞬重新启动它,所以在
215
+ 这里发报告只会与仍在进行的工作赛跑。错误只提供下面说的 head 退路。
193
216
 
194
217
  `head` 恒为单行、上限 200 字符,与 client 写入 `out_head` 和回执携带的内容一致。每个任务的 head
195
- 只有一个来源:显式 `onlyne_complete` 带 `summary` 时就是它,否则是出错 turn 报的错误。最后一段
196
- assistant 文本只是 `summary` 完全缺失的 `onlyne_complete` 的退路——调用之后再说的话顶不掉调用
197
- 交出的内容,此外没有任何东西读它。
218
+ 只有一个来源:显式 `onlyne_complete` 带 `summary` 时就是它。完全没有带 `summary` 的调用退到
219
+ session 最后说过的话——最后一段 assistant 文本,或失败 turn 留下的错误。错误永远只是这个退路:
220
+ 它本身从不构成 completion,此外没有任何东西读它;而调用之后再说的话顶不掉调用交出的内容。
198
221
 
199
222
  报出去的 completion 会结束所在 session 的进程。`report.complete` 以请求形式发出,client 只有
200
223
  在结算 session 行、ack 掉投递、并写好 `Completion` envelope 之后才应答,插件就在这个应答处
201
224
  让 pi 退出。socket 当时送不出去的 outcome 会被记住,并在下一次 `hello` 后补发,那次补发的
202
225
  应答就是结束进程的交接点。被宿主拒掉的 completion 不会让进程退出,任务不会因为退出而丢失。
203
226
 
227
+ 若 client 此前已经结算过该任务,它会对同一个请求回答 `{"already_settled": {state, settled_at,
228
+ head}}` 而不是 `null`。插件把这个记录原样转交,而不是声称一个并非由本次调用造成的结果,工具结果
229
+ 也照此说明(见 §3);在重连补发那条路径上没有工具结果可以承载它,记录会进入 `[pi-onlyne]` 日志。
230
+ 无论哪种情形任务都保持已结算:以 client 的那一行为准。
231
+
204
232
  completion 是插件最后一份上报;已经交掉任务的 session 不再发心跳,也不再应答 `probe`。终态的
205
233
  agent 维度由 client 自己写:随后到达的 `detach` 会退役这个已无任务的 session,该路径会喂入
206
234
  `AgentGone` 并发布这一行(`retire_idle_locked`,
@@ -218,8 +246,9 @@ steer 或 follow-up 消息、重试、压缩,以及被后台任务扩展移出
218
246
 
219
247
  后台任务扩展改变了这个问题。`bg_run` 与同类工具立即返回,工作继续在子进程里跑,于是 pi 在任务
220
248
  仍在进行时就等待输入。插件通过该扩展注册的工具认出它,再向它的 EventBus 服务查存活任务列表;
221
- 处于 `running` 的任务会把 session 按在 `running` 上,插件在轮次结束时欠下的那份报告——它自己
222
- 目睹的失败——也要等它结束。没有装这个扩展的 session 没有工具可认、没有查询,也没有东西要等。
249
+ 处于 `running` 的任务会把 session 按在 `running` 上,于是心跳陈述的是 session 真实的阶段。插件
250
+ 自己没有任何报告要等它结束:轮次结束时已经不再上报任何东西。没有装这个扩展的 session 没有工具
251
+ 可认、没有查询,也没有东西要等。
223
252
 
224
253
  ## 5. 协议说明与偏差
225
254
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onlyne",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Onlyne agent adapter for pi: the session lifecycle an onlyne role client expects from a pi host.",
5
5
  "type": "module",
6
6
  "license": "MIT",