@te-river/opencode-team-mode 1.7.7 → 1.7.8

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.
Files changed (39) hide show
  1. package/README.md +1010 -1010
  2. package/README.zh-CN.md +907 -907
  3. package/dist/host/v2-browser-gate.js +2 -2
  4. package/dist/host/v2-offload.js +6 -6
  5. package/dist/host/v2-retry.d.ts +9 -1
  6. package/dist/host/v2-retry.d.ts.map +1 -1
  7. package/dist/host/v2-retry.js +10 -2
  8. package/dist/host/v2-retry.js.map +1 -1
  9. package/dist/prompts/specialists.js +286 -286
  10. package/dist/task-offload.js +1 -1
  11. package/dist/tm/config-layers.d.ts +5 -2
  12. package/dist/tm/config-layers.d.ts.map +1 -1
  13. package/dist/tm/config-layers.js +9 -6
  14. package/dist/tm/config-layers.js.map +1 -1
  15. package/dist/tm/config.d.ts.map +1 -1
  16. package/dist/tm/config.js +7 -1
  17. package/dist/tm/config.js.map +1 -1
  18. package/dist/tm/ledger.js +5 -5
  19. package/dist/tm/memory.js +9 -9
  20. package/dist/tm/search.d.ts +3 -0
  21. package/dist/tm/search.d.ts.map +1 -1
  22. package/dist/tm/search.js +67 -15
  23. package/dist/tm/search.js.map +1 -1
  24. package/dist/tm/tools.js +3 -3
  25. package/dist/tm/webfetch.d.ts +94 -5
  26. package/dist/tm/webfetch.d.ts.map +1 -1
  27. package/dist/tm/webfetch.js +285 -64
  28. package/dist/tm/webfetch.js.map +1 -1
  29. package/docs/installation.md +505 -505
  30. package/docs/research/agent-data-exchange.md +117 -117
  31. package/docs/research/browser-pane.md +94 -94
  32. package/docs/research/host-http-api.md +132 -132
  33. package/docs/research/host-subagent-injection.md +187 -187
  34. package/docs/research/plugin-loader-contract.md +261 -261
  35. package/docs/research/v2-builtin-tools.md +447 -447
  36. package/docs/v2-port-notes.md +121 -78
  37. package/index.js +7 -7
  38. package/package.json +64 -64
  39. package/scripts/lib/config-surgery.cjs +239 -239
@@ -1,187 +1,187 @@
1
- # OpenCode 2.x — how a background sub-agent is dispatched, acknowledged, and answered
2
-
3
- **Scope:** one exported desktop session (`e-f.json`, provided by the user on 2026-09-26),
4
- read as evidence. Nothing in the host was modified and nothing here was inferred from the
5
- v1 behaviour: every claim below is either quoted from that export or measured against it.
6
- The session's workspace path is deliberately not reproduced.
7
-
8
- **Why this page exists.** On v2 a plugin cannot create an agent and cannot create a
9
- session it is allowed to read, so delegation goes through the host's own `subagent` tool.
10
- `tm_join` — the collect side — therefore needed to know what that tool gives a plugin at
11
- each of three moments: when the call is made, when the host acknowledges it, and when the
12
- child's answer comes back. Getting the second one wrong is what made the feature look
13
- impossible: the ack is small and the answer is not a tool result at all.
14
-
15
- ## 1. The dispatch, as the plugin sees it
16
-
17
- `[L]`+`[B]` `execute.before` carries the model's own arguments under **`input`** — the host's
18
- trigger is `{tool, sessionID, agent, messageID, id, input}`, where `sessionID`/`agent` are the
19
- CALLER's and `input` is the argument object:
20
-
21
- ```
22
- { tool: "subagent",
23
- sessionID: "ses_…", // the lead's own session
24
- agent: "team",
25
- input: { agent: "architect", background: true,
26
- description: "注入形状取证", prompt: "…" } }
27
- ```
28
-
29
- `description` is a task name, `prompt` is the work. Only `description` is worth keeping
30
- and only for the lead's own table: `prompt` is free text and never enters the trajectory
31
- (the R6 rule binds a diagnostic as much as a guard).
32
-
33
- ## 2. The ack — this is the seam that makes claiming a child possible
34
-
35
- `[L]` The synchronous result of a background dispatch is small (98 tokens measured on an
36
- earlier host build) and carries the child's identity twice:
37
-
38
- ```
39
- content[0].text = "The subagent is working in the background (sessionID: ses_f2417df8…).
40
- You will be notified automatically when it finishes.
41
- DO NOT sleep, poll for progress, ask the subagent for status, or
42
- duplicate this subagent's work; …"
43
- metadata = { sessionID: "ses_f2417df8…", status: "running", truncated: false }
44
- ```
45
-
46
- Two consequences that changed the design:
47
-
48
- 1. **`execute.after` on tool `subagent` yields the child session id.** No `session.get`,
49
- no `session.children`, no guessing: the host hands it over. Our `ctx.session.get`
50
- bridge never resolved a shape on a live host, so a tree walk was not an option, and
51
- this made the question moot.
52
- 2. **`status:"running"` is the host telling us the work is open.** A synchronous child is
53
- NOT distinguished by a missing id — read out of 2.0.18, the `subagent` result always
54
- carries `metadata:{sessionID,status}`, and the completed case wraps the body itself:
55
-
56
- ```js
57
- content: G.status==="completed"
58
- ? `<subagent sessionID="${G.sessionID}" state="completed">.${G.output}.</subagent>`
59
- : G.output,
60
- metadata:{ sessionID: G.sessionID, status: G.status }
61
- ```
62
-
63
- So what makes a synchronous child un-collectable is `status:"completed"` (the reply is
64
- already in hand), not an absent id. Counting it as `settledAtOnce` rather than
65
- `noChildId` is what keeps the two cases from being conflated in `tm_stats`.
66
- 3. **The `execute.before` field is `input`, not `args`.** Also from the host's own trigger:
67
-
68
- ```js
69
- e.trigger("tool","execute.before",{tool,sessionID,agent,messageID,id,input:m})
70
- ```
71
-
72
- Reading `event.args` was the first live round's failure: the stash stayed empty, every
73
- ack arrived unpaired, and each child was registered under a generic label instead of the
74
- role and task it was dispatched for — `tm_stats` said "登记 1 个(1 个没配到派发行)" and
75
- the unit test that now pins the paired label would have been red. A fake ctx that mirrors
76
- the guessed shape passes a bug like this; the host's code does not.
77
-
78
- The id is read from `metadata` first and from the ack sentence second, because a field
79
- shape is exactly what a host upgrade changes.
80
-
81
- ## 3. The answer is a message, not a tool result
82
-
83
- `[L]` When the child finishes, the parent receives a synthetic message. From the export,
84
- the message's own fields:
85
-
86
- ```
87
- type: "synthetic"
88
- description: "注入形状取证"
89
- metadata: { source: "subagent", childID: "ses_f2417df8…", agent: "architect", state: "completed" }
90
- text: <subagent sessionID="ses_f2417df8…" state="completed" description="注入形状取证">
91
- PROBE-OK
92
- </subagent>
93
- ```
94
-
95
- Note the key names differ between the two moments: the ack says `sessionID`, the
96
- injection says `childID` (and adds `source`, `agent`, `state`). A matcher that assumes one
97
- spelling for both is the bug class this page exists to record — `parseHostEnvelope` in
98
- `src/task-offload.ts` accepts both the v1 `<task id=` and the v2 `<subagent sessionID=`
99
- wrapper for exactly this reason, and rewrites only the body, because the session id inside
100
- the envelope is the pointer the lead uses to fetch the reply back.
101
-
102
- What the plugin does NOT get on v2: a hook that hands us this message BEFORE the host
103
- persists it. v1 was given `chat.message` at exactly that moment; the v2 plugin surface has no
104
- equivalent. What we DO get is the message one step later, inside the parent's assembled
105
- history at `session.hook("context")` — measured, because `applyV2CompletionWatch` settles the
106
- child from it (§7). The distinction is the whole difference between the two claims we can
107
- make: we can OBSERVE that the host wrote a completion (so `tm_join` may say 已完成 and name
108
- where it read that), and we cannot REWRITE the body the model is about to receive. `tm_join`
109
- therefore reports the child's state and provenance and says where the text is, rather than
110
- printing a reply it never read.
111
-
112
- ## 4. Ordering, measured, and the one inference it licenses
113
-
114
- `[L]` From the export's timestamps (same session, milliseconds):
115
-
116
- | moment | ts |
117
- |---|---|
118
- | child injected into the parent | …824730 |
119
- | parent's next assistant message streamed | …824763 → …829026 |
120
- | parent session `idle` | …829038 |
121
-
122
- So the injection precedes the parent's idle by construction — the parent cannot go idle
123
- with an undelivered notification. That single ordering fact is what licenses the fallback
124
- in `src/tm/dispatch.ts`: a host-dispatched child still marked running when ITS PARENT goes
125
- idle is settled as `settleSource: "parent-idle"` and printed as 推定已结算, with the reason
126
- in the same line. The child's own `session.idle` on the event feed is the measured path for a
127
- child THIS process dispatched (`settleSource: "event"`) — for a host `subagent` child it does
128
- not arrive at all, which §7 records; the two are never printed the same way, because one is an
129
- observation and the other is an inference from a host ordering we happened to see once.
130
-
131
- ## 6. Live verification, 2026-09-26 (host 2.0.18, `lxns-uni/zai-org/GLM-5.3#max`)
132
-
133
- Run against a sandbox config dir (`OPENCODE_CONFIG_DIR`, plugin loaded from
134
- `vendor/team-mode`) with the user's real profile supplying credentials, so nothing was
135
- written to the live global config. Same three-step prompt twice, before and after the
136
- `input` fix:
137
-
138
- | | acks seen | registered | unpaired | audit line's `agent` | the row `tm_join` printed |
139
- |---|---|---|---|---|---|
140
- | before | 1 | 1 | **1** | `subagent` (generic) | child id + (宿主 subagent 派发), role lost |
141
- | after | 1 | 1 | **0** | `architect` | `ses_f237aec68ffe… · architect · "认领复验2" · 运行中 22s ·(宿主 subagent 派发)` |
142
-
143
- So the claiming and the pairing are both observed on a live host, and the settle path was
144
- observed in its honest state: the child was still `运行中` when the snapshot ran (its own
145
- `session.idle` had not arrived and the parent had not gone idle yet), which is the row
146
- saying what it knows rather than guessing.
147
-
148
- **One observability caveat, measured in the same runs:** a single CLI invocation boots more
149
- than one server process, each writing its own `v2-surface` row with ITS OWN counters. The
150
- model pasted `确认 0 次 → 登记 0 个` from the sibling process while the process that
151
- registered the child reported `1 → 1`. `tm_stats` therefore prints the run id beside those
152
- counters — a bare 0 in a multi-process host is otherwise readable as "nothing was
153
- dispatched", which is exactly the silence this feature removes.
154
-
155
-
156
- ## 7. What is settled, and what is still open
157
-
158
- `[L]` **The host does NOT forward a child session's `session.idle` to a plugin subscriber.**
159
- Measured on 2.0.18 (round 5, 2026-09-26): a background child finished, its
160
- `<subagent … state="completed">` injection reached the parent with the full report, and the
161
- plugin feed produced neither an `idle` nor an `idle_presumed` line for that child — `tm_join`
162
- kept showing it 运行中. So neither of the two paths this module originally offered works for a
163
- host child mid-turn: the event never arrives, and the parent-idle presumption cannot fire
164
- while the parent is still working. That is what `applyV2CompletionWatch` exists to fix: the
165
- envelope the host itself writes, read out of the parent's assembled message history.
166
-
167
- `[L]` **The seam that carries it is `session.hook("context")`, and settle-by-injection is
168
- live-verified.** Getting there took two wrong answers, and both are counted so a wrong seam is
169
- visible rather than silent:
170
-
171
- | round | what was watched | what the host did |
172
- |---|---|---|
173
- | 6 | `session.prompt` + `session.model.request` | `prompt` fired **0** times; `model.request` fired **6** and carried no message list at all (the history is assembled separately from that payload) |
174
- | 8 | `session.context` (+ `prompt` kept, counted, still 0) | fired 6, `completion_settled=1`, audit line `{"event":"idle_injection","child":"ses_f23386a7…","agent":"researcher","ms":53102}` |
175
-
176
- The line is the third source of truth in `src/tm/dispatch.ts` (`settleSource: "injection"`,
177
- printed as 已完成 with its provenance), and the `ms` is the child's own elapsed time, not our
178
- wait. `completion_skipped` grows on every later round (4, then 5) because the scan is skipped
179
- while nothing is open — which is the point of counting the skip: "did not look" and "looked and
180
- saw nothing" stay distinguishable, and a hook that exists but is the WRONG one is exactly the
181
- failure round 6 nearly hid.
182
-
183
- `[U]` The full `status` vocabulary: 2.0.18's own code applies the `<subagent …>` wrapper only
184
- when `G.status === "completed"`, so anything else is treated as open by
185
- `hostChildIsOpen()` — whether `error` reaches a plugin-visible ack has not been seen.
186
- `[U]` Whether a child of a child is ever dispatched: the config gives nesting depth one, and
187
- the T3 rule says only the lead dispatches, so this page describes one level.
1
+ # OpenCode 2.x — how a background sub-agent is dispatched, acknowledged, and answered
2
+
3
+ **Scope:** one exported desktop session (`e-f.json`, provided by the user on 2026-09-26),
4
+ read as evidence. Nothing in the host was modified and nothing here was inferred from the
5
+ v1 behaviour: every claim below is either quoted from that export or measured against it.
6
+ The session's workspace path is deliberately not reproduced.
7
+
8
+ **Why this page exists.** On v2 a plugin cannot create an agent and cannot create a
9
+ session it is allowed to read, so delegation goes through the host's own `subagent` tool.
10
+ `tm_join` — the collect side — therefore needed to know what that tool gives a plugin at
11
+ each of three moments: when the call is made, when the host acknowledges it, and when the
12
+ child's answer comes back. Getting the second one wrong is what made the feature look
13
+ impossible: the ack is small and the answer is not a tool result at all.
14
+
15
+ ## 1. The dispatch, as the plugin sees it
16
+
17
+ `[L]`+`[B]` `execute.before` carries the model's own arguments under **`input`** — the host's
18
+ trigger is `{tool, sessionID, agent, messageID, id, input}`, where `sessionID`/`agent` are the
19
+ CALLER's and `input` is the argument object:
20
+
21
+ ```
22
+ { tool: "subagent",
23
+ sessionID: "ses_…", // the lead's own session
24
+ agent: "team",
25
+ input: { agent: "architect", background: true,
26
+ description: "注入形状取证", prompt: "…" } }
27
+ ```
28
+
29
+ `description` is a task name, `prompt` is the work. Only `description` is worth keeping
30
+ and only for the lead's own table: `prompt` is free text and never enters the trajectory
31
+ (the R6 rule binds a diagnostic as much as a guard).
32
+
33
+ ## 2. The ack — this is the seam that makes claiming a child possible
34
+
35
+ `[L]` The synchronous result of a background dispatch is small (98 tokens measured on an
36
+ earlier host build) and carries the child's identity twice:
37
+
38
+ ```
39
+ content[0].text = "The subagent is working in the background (sessionID: ses_f2417df8…).
40
+ You will be notified automatically when it finishes.
41
+ DO NOT sleep, poll for progress, ask the subagent for status, or
42
+ duplicate this subagent's work; …"
43
+ metadata = { sessionID: "ses_f2417df8…", status: "running", truncated: false }
44
+ ```
45
+
46
+ Two consequences that changed the design:
47
+
48
+ 1. **`execute.after` on tool `subagent` yields the child session id.** No `session.get`,
49
+ no `session.children`, no guessing: the host hands it over. Our `ctx.session.get`
50
+ bridge never resolved a shape on a live host, so a tree walk was not an option, and
51
+ this made the question moot.
52
+ 2. **`status:"running"` is the host telling us the work is open.** A synchronous child is
53
+ NOT distinguished by a missing id — read out of 2.0.18, the `subagent` result always
54
+ carries `metadata:{sessionID,status}`, and the completed case wraps the body itself:
55
+
56
+ ```js
57
+ content: G.status==="completed"
58
+ ? `<subagent sessionID="${G.sessionID}" state="completed">.${G.output}.</subagent>`
59
+ : G.output,
60
+ metadata:{ sessionID: G.sessionID, status: G.status }
61
+ ```
62
+
63
+ So what makes a synchronous child un-collectable is `status:"completed"` (the reply is
64
+ already in hand), not an absent id. Counting it as `settledAtOnce` rather than
65
+ `noChildId` is what keeps the two cases from being conflated in `tm_stats`.
66
+ 3. **The `execute.before` field is `input`, not `args`.** Also from the host's own trigger:
67
+
68
+ ```js
69
+ e.trigger("tool","execute.before",{tool,sessionID,agent,messageID,id,input:m})
70
+ ```
71
+
72
+ Reading `event.args` was the first live round's failure: the stash stayed empty, every
73
+ ack arrived unpaired, and each child was registered under a generic label instead of the
74
+ role and task it was dispatched for — `tm_stats` said "登记 1 个(1 个没配到派发行)" and
75
+ the unit test that now pins the paired label would have been red. A fake ctx that mirrors
76
+ the guessed shape passes a bug like this; the host's code does not.
77
+
78
+ The id is read from `metadata` first and from the ack sentence second, because a field
79
+ shape is exactly what a host upgrade changes.
80
+
81
+ ## 3. The answer is a message, not a tool result
82
+
83
+ `[L]` When the child finishes, the parent receives a synthetic message. From the export,
84
+ the message's own fields:
85
+
86
+ ```
87
+ type: "synthetic"
88
+ description: "注入形状取证"
89
+ metadata: { source: "subagent", childID: "ses_f2417df8…", agent: "architect", state: "completed" }
90
+ text: <subagent sessionID="ses_f2417df8…" state="completed" description="注入形状取证">
91
+ PROBE-OK
92
+ </subagent>
93
+ ```
94
+
95
+ Note the key names differ between the two moments: the ack says `sessionID`, the
96
+ injection says `childID` (and adds `source`, `agent`, `state`). A matcher that assumes one
97
+ spelling for both is the bug class this page exists to record — `parseHostEnvelope` in
98
+ `src/task-offload.ts` accepts both the v1 `<task id=` and the v2 `<subagent sessionID=`
99
+ wrapper for exactly this reason, and rewrites only the body, because the session id inside
100
+ the envelope is the pointer the lead uses to fetch the reply back.
101
+
102
+ What the plugin does NOT get on v2: a hook that hands us this message BEFORE the host
103
+ persists it. v1 was given `chat.message` at exactly that moment; the v2 plugin surface has no
104
+ equivalent. What we DO get is the message one step later, inside the parent's assembled
105
+ history at `session.hook("context")` — measured, because `applyV2CompletionWatch` settles the
106
+ child from it (§7). The distinction is the whole difference between the two claims we can
107
+ make: we can OBSERVE that the host wrote a completion (so `tm_join` may say 已完成 and name
108
+ where it read that), and we cannot REWRITE the body the model is about to receive. `tm_join`
109
+ therefore reports the child's state and provenance and says where the text is, rather than
110
+ printing a reply it never read.
111
+
112
+ ## 4. Ordering, measured, and the one inference it licenses
113
+
114
+ `[L]` From the export's timestamps (same session, milliseconds):
115
+
116
+ | moment | ts |
117
+ |---|---|
118
+ | child injected into the parent | …824730 |
119
+ | parent's next assistant message streamed | …824763 → …829026 |
120
+ | parent session `idle` | …829038 |
121
+
122
+ So the injection precedes the parent's idle by construction — the parent cannot go idle
123
+ with an undelivered notification. That single ordering fact is what licenses the fallback
124
+ in `src/tm/dispatch.ts`: a host-dispatched child still marked running when ITS PARENT goes
125
+ idle is settled as `settleSource: "parent-idle"` and printed as 推定已结算, with the reason
126
+ in the same line. The child's own `session.idle` on the event feed is the measured path for a
127
+ child THIS process dispatched (`settleSource: "event"`) — for a host `subagent` child it does
128
+ not arrive at all, which §7 records; the two are never printed the same way, because one is an
129
+ observation and the other is an inference from a host ordering we happened to see once.
130
+
131
+ ## 6. Live verification, 2026-09-26 (host 2.0.18, `lxns-uni/zai-org/GLM-5.3#max`)
132
+
133
+ Run against a sandbox config dir (`OPENCODE_CONFIG_DIR`, plugin loaded from
134
+ `vendor/team-mode`) with the user's real profile supplying credentials, so nothing was
135
+ written to the live global config. Same three-step prompt twice, before and after the
136
+ `input` fix:
137
+
138
+ | | acks seen | registered | unpaired | audit line's `agent` | the row `tm_join` printed |
139
+ |---|---|---|---|---|---|
140
+ | before | 1 | 1 | **1** | `subagent` (generic) | child id + (宿主 subagent 派发), role lost |
141
+ | after | 1 | 1 | **0** | `architect` | `ses_f237aec68ffe… · architect · "认领复验2" · 运行中 22s ·(宿主 subagent 派发)` |
142
+
143
+ So the claiming and the pairing are both observed on a live host, and the settle path was
144
+ observed in its honest state: the child was still `运行中` when the snapshot ran (its own
145
+ `session.idle` had not arrived and the parent had not gone idle yet), which is the row
146
+ saying what it knows rather than guessing.
147
+
148
+ **One observability caveat, measured in the same runs:** a single CLI invocation boots more
149
+ than one server process, each writing its own `v2-surface` row with ITS OWN counters. The
150
+ model pasted `确认 0 次 → 登记 0 个` from the sibling process while the process that
151
+ registered the child reported `1 → 1`. `tm_stats` therefore prints the run id beside those
152
+ counters — a bare 0 in a multi-process host is otherwise readable as "nothing was
153
+ dispatched", which is exactly the silence this feature removes.
154
+
155
+
156
+ ## 7. What is settled, and what is still open
157
+
158
+ `[L]` **The host does NOT forward a child session's `session.idle` to a plugin subscriber.**
159
+ Measured on 2.0.18 (round 5, 2026-09-26): a background child finished, its
160
+ `<subagent … state="completed">` injection reached the parent with the full report, and the
161
+ plugin feed produced neither an `idle` nor an `idle_presumed` line for that child — `tm_join`
162
+ kept showing it 运行中. So neither of the two paths this module originally offered works for a
163
+ host child mid-turn: the event never arrives, and the parent-idle presumption cannot fire
164
+ while the parent is still working. That is what `applyV2CompletionWatch` exists to fix: the
165
+ envelope the host itself writes, read out of the parent's assembled message history.
166
+
167
+ `[L]` **The seam that carries it is `session.hook("context")`, and settle-by-injection is
168
+ live-verified.** Getting there took two wrong answers, and both are counted so a wrong seam is
169
+ visible rather than silent:
170
+
171
+ | round | what was watched | what the host did |
172
+ |---|---|---|
173
+ | 6 | `session.prompt` + `session.model.request` | `prompt` fired **0** times; `model.request` fired **6** and carried no message list at all (the history is assembled separately from that payload) |
174
+ | 8 | `session.context` (+ `prompt` kept, counted, still 0) | fired 6, `completion_settled=1`, audit line `{"event":"idle_injection","child":"ses_f23386a7…","agent":"researcher","ms":53102}` |
175
+
176
+ The line is the third source of truth in `src/tm/dispatch.ts` (`settleSource: "injection"`,
177
+ printed as 已完成 with its provenance), and the `ms` is the child's own elapsed time, not our
178
+ wait. `completion_skipped` grows on every later round (4, then 5) because the scan is skipped
179
+ while nothing is open — which is the point of counting the skip: "did not look" and "looked and
180
+ saw nothing" stay distinguishable, and a hook that exists but is the WRONG one is exactly the
181
+ failure round 6 nearly hid.
182
+
183
+ `[U]` The full `status` vocabulary: 2.0.18's own code applies the `<subagent …>` wrapper only
184
+ when `G.status === "completed"`, so anything else is treated as open by
185
+ `hostChildIsOpen()` — whether `error` reaches a plugin-visible ack has not been seen.
186
+ `[U]` Whether a child of a child is ever dispatched: the config gives nesting depth one, and
187
+ the T3 rule says only the lead dispatches, so this page describes one level.