@te-river/opencode-team-mode 1.6.2 → 1.7.1
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 +114 -118
- package/README.zh-CN.md +95 -100
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +89 -79
- package/dist/agents.js.map +1 -1
- package/dist/approval-gate.d.ts +8 -9
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +13 -14
- package/dist/approval-gate.js.map +1 -1
- package/dist/capabilities.d.ts +11 -37
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +11 -141
- package/dist/capabilities.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/envprotect/hook.d.ts +12 -31
- package/dist/envprotect/hook.d.ts.map +1 -1
- package/dist/envprotect/hook.js +13 -91
- package/dist/envprotect/hook.js.map +1 -1
- package/dist/envprotect/patterns.d.ts +6 -4
- package/dist/envprotect/patterns.d.ts.map +1 -1
- package/dist/envprotect/patterns.js +6 -4
- package/dist/envprotect/patterns.js.map +1 -1
- package/dist/envprotect.d.ts +1 -1
- package/dist/envprotect.d.ts.map +1 -1
- package/dist/envprotect.js +1 -1
- package/dist/envprotect.js.map +1 -1
- package/dist/host/v2-browser-gate.d.ts +19 -1
- package/dist/host/v2-browser-gate.d.ts.map +1 -1
- package/dist/host/v2-browser-gate.js +47 -6
- package/dist/host/v2-browser-gate.js.map +1 -1
- package/dist/host/v2-capabilities.d.ts +16 -0
- package/dist/host/v2-capabilities.d.ts.map +1 -1
- package/dist/host/v2-capabilities.js +22 -1
- package/dist/host/v2-capabilities.js.map +1 -1
- package/dist/host/v2-default-agent.d.ts +37 -0
- package/dist/host/v2-default-agent.d.ts.map +1 -0
- package/dist/host/v2-default-agent.js +128 -0
- package/dist/host/v2-default-agent.js.map +1 -0
- package/dist/host/v2-events.d.ts +1 -0
- package/dist/host/v2-events.d.ts.map +1 -1
- package/dist/host/v2-events.js +14 -0
- package/dist/host/v2-events.js.map +1 -1
- package/dist/host/v2-guard.d.ts +178 -0
- package/dist/host/v2-guard.d.ts.map +1 -1
- package/dist/host/v2-guard.js +324 -8
- package/dist/host/v2-guard.js.map +1 -1
- package/dist/host/v2-offload.d.ts +1 -1
- package/dist/host/v2-offload.js +2 -2
- package/dist/host/v2-permissions.d.ts +45 -44
- package/dist/host/v2-permissions.d.ts.map +1 -1
- package/dist/host/v2-permissions.js +48 -49
- package/dist/host/v2-permissions.js.map +1 -1
- package/dist/host/v2-probe-chain.d.ts +93 -0
- package/dist/host/v2-probe-chain.d.ts.map +1 -0
- package/dist/host/v2-probe-chain.js +146 -0
- package/dist/host/v2-probe-chain.js.map +1 -0
- package/dist/host/v2-prune.d.ts +157 -0
- package/dist/host/v2-prune.d.ts.map +1 -0
- package/dist/host/v2-prune.js +507 -0
- package/dist/host/v2-prune.js.map +1 -0
- package/dist/host/v2-retry.d.ts +143 -0
- package/dist/host/v2-retry.d.ts.map +1 -0
- package/dist/host/v2-retry.js +315 -0
- package/dist/host/v2-retry.js.map +1 -0
- package/dist/host/v2-scope.d.ts.map +1 -1
- package/dist/host/v2-scope.js +14 -7
- package/dist/host/v2-scope.js.map +1 -1
- package/dist/host/v2-session-client.d.ts +24 -0
- package/dist/host/v2-session-client.d.ts.map +1 -1
- package/dist/host/v2-session-client.js +243 -3
- package/dist/host/v2-session-client.js.map +1 -1
- package/dist/host/v2-session.d.ts +72 -1
- package/dist/host/v2-session.d.ts.map +1 -1
- package/dist/host/v2-session.js +154 -10
- package/dist/host/v2-session.js.map +1 -1
- package/dist/host/v2-tool.d.ts +2 -3
- package/dist/host/v2-tool.d.ts.map +1 -1
- package/dist/host/v2-tool.js +2 -3
- package/dist/host/v2-tool.js.map +1 -1
- package/dist/host/v2-types.d.ts +30 -0
- package/dist/host/v2-types.d.ts.map +1 -1
- package/dist/host/v2-usage.d.ts +50 -0
- package/dist/host/v2-usage.d.ts.map +1 -0
- package/dist/host/v2-usage.js +82 -0
- package/dist/host/v2-usage.js.map +1 -0
- package/dist/host/v2.d.ts +14 -12
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +430 -73
- package/dist/host/v2.js.map +1 -1
- package/dist/identity.d.ts +27 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +34 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +9 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -27
- package/dist/index.js.map +1 -1
- package/dist/prompts/lead.d.ts +1 -1
- package/dist/prompts/lead.d.ts.map +1 -1
- package/dist/prompts/lead.js +38 -25
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/shared.d.ts +2 -2
- package/dist/prompts/shared.d.ts.map +1 -1
- package/dist/prompts/shared.js +66 -56
- package/dist/prompts/shared.js.map +1 -1
- package/dist/prompts/specialists.d.ts +2 -2
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +53 -83
- package/dist/prompts/specialists.js.map +1 -1
- package/dist/tm/args-schema.d.ts +7 -32
- package/dist/tm/args-schema.d.ts.map +1 -1
- package/dist/tm/args-schema.js +12 -153
- package/dist/tm/args-schema.js.map +1 -1
- package/dist/tm/board.d.ts +10 -9
- package/dist/tm/board.d.ts.map +1 -1
- package/dist/tm/board.js +11 -10
- package/dist/tm/board.js.map +1 -1
- package/dist/tm/config-files.d.ts +81 -0
- package/dist/tm/config-files.d.ts.map +1 -0
- package/dist/tm/config-files.js +209 -0
- package/dist/tm/config-files.js.map +1 -0
- package/dist/tm/config-layers.d.ts +117 -0
- package/dist/tm/config-layers.d.ts.map +1 -0
- package/dist/tm/config-layers.js +317 -0
- package/dist/tm/config-layers.js.map +1 -0
- package/dist/tm/config.d.ts +10 -101
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +17 -71
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/dispatch.d.ts +115 -21
- package/dist/tm/dispatch.d.ts.map +1 -1
- package/dist/tm/dispatch.js +424 -67
- package/dist/tm/dispatch.js.map +1 -1
- package/dist/tm/egress.d.ts +19 -7
- package/dist/tm/egress.d.ts.map +1 -1
- package/dist/tm/egress.js +21 -9
- package/dist/tm/egress.js.map +1 -1
- package/dist/tm/index.d.ts +23 -20
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +117 -116
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +9 -2
- package/dist/tm/ledger.d.ts.map +1 -1
- package/dist/tm/ledger.js +4 -1
- package/dist/tm/ledger.js.map +1 -1
- package/dist/tm/perm-ask.d.ts +3 -3
- package/dist/tm/perm-ask.d.ts.map +1 -1
- package/dist/tm/perm-ask.js +3 -3
- package/dist/tm/perm-ask.js.map +1 -1
- package/dist/tm/pipelines.d.ts +12 -11
- package/dist/tm/pipelines.d.ts.map +1 -1
- package/dist/tm/pipelines.js +40 -207
- package/dist/tm/pipelines.js.map +1 -1
- package/dist/tm/preview.d.ts +1 -1
- package/dist/tm/preview.js +3 -3
- package/dist/tm/preview.js.map +1 -1
- package/dist/tm/refs.d.ts +31 -4
- package/dist/tm/refs.d.ts.map +1 -1
- package/dist/tm/refs.js +48 -4
- package/dist/tm/refs.js.map +1 -1
- package/dist/tm/result.d.ts +1 -1
- package/dist/tm/result.d.ts.map +1 -1
- package/dist/tm/result.js +4 -3
- package/dist/tm/result.js.map +1 -1
- package/dist/tm/search.d.ts +2 -2
- package/dist/tm/search.js +7 -7
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/serp-loop.d.ts +19 -7
- package/dist/tm/serp-loop.d.ts.map +1 -1
- package/dist/tm/serp-loop.js +24 -9
- package/dist/tm/serp-loop.js.map +1 -1
- package/dist/tm/stats.d.ts +12 -27
- package/dist/tm/stats.d.ts.map +1 -1
- package/dist/tm/stats.js +43 -45
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/store.d.ts +18 -5
- package/dist/tm/store.d.ts.map +1 -1
- package/dist/tm/store.js +39 -9
- package/dist/tm/store.js.map +1 -1
- package/dist/tm/tools.d.ts +7 -5
- package/dist/tm/tools.d.ts.map +1 -1
- package/dist/tm/tools.js +10 -41
- package/dist/tm/tools.js.map +1 -1
- package/dist/tm/webfetch.d.ts +1 -1
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +38 -23
- package/dist/tm/webfetch.js.map +1 -1
- package/dist/types.d.ts +40 -123
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -21
- package/dist/types.js.map +1 -1
- package/docs/installation-v2.md +133 -33
- package/docs/installation.md +9 -0
- package/docs/research/browser-pane.md +5 -0
- package/docs/v2-port-notes.md +133 -0
- package/package.json +1 -1
- package/scripts/gen-v2-config.mjs +246 -94
- package/scripts/lib/fake-ctx.mjs +67 -0
package/dist/tm/dispatch.js
CHANGED
|
@@ -28,7 +28,20 @@
|
|
|
28
28
|
* through the SAME governance pipeline as every other tm_* tool, so a long
|
|
29
29
|
* sub-agent report arrives as a handle + an 80-token preview the lead can page
|
|
30
30
|
* through with tm_fetch — not kilotokens dumped inline.
|
|
31
|
+
*
|
|
32
|
+
* Steering (2026-10-04): the same tool is also the lead's INTERJECTION channel —
|
|
33
|
+
* `steer` puts a line into a running child's inbox (`Session.Inbox.Delivery =
|
|
34
|
+
* "steer" | "queue"`, "Steering wakes session execution."), `unread` lists that
|
|
35
|
+
* child's undelivered items by id and shape, `unsend` withdraws one that has not
|
|
36
|
+
* been delivered yet. No new tool was registered for it: a new tool name would
|
|
37
|
+
* have to move the whitelist in agents.ts, the permission triples in
|
|
38
|
+
* v2-permissions.ts and the request-layer deletion logic at once, and three
|
|
39
|
+
* places that can drift are worse than one argument set on a tool the lead
|
|
40
|
+
* already calls. What the host does NOT give is tool-level cancellation — the
|
|
41
|
+
* only thing stoppable is the whole active execution (`interrupt`) — so nothing
|
|
42
|
+
* here pretends that granularity exists.
|
|
31
43
|
*/
|
|
44
|
+
import { sameAgent } from "../identity.js";
|
|
32
45
|
import { tmError, toToolResult } from "./result.js";
|
|
33
46
|
import { unwrapClientResult } from "./client-unwrap.js";
|
|
34
47
|
import { detectContentType } from "./preview.js";
|
|
@@ -70,35 +83,6 @@ export function openHostTodos(todos) {
|
|
|
70
83
|
}
|
|
71
84
|
return open;
|
|
72
85
|
}
|
|
73
|
-
/** #80: the line tm_join adds when a SETTLED child still owns a browser window.
|
|
74
|
-
* Takes only the leases already filtered to settled children, so the ownership
|
|
75
|
-
* rule stays where the child registry is; this function's whole job is to say
|
|
76
|
-
* it in the one shape that survives the lead's skimming — and to refuse to say
|
|
77
|
-
* it for a window that was never ours. */
|
|
78
|
-
export function leaseTripwire(held, callerSessionID) {
|
|
79
|
-
if (!held.length)
|
|
80
|
-
return null;
|
|
81
|
-
const isMine = (l) => !!callerSessionID && l.owner === callerSessionID;
|
|
82
|
-
const mine = held.filter(isMine);
|
|
83
|
-
const theirs = held.filter((l) => !isMine(l));
|
|
84
|
-
const at = (l) => `空闲 ${Math.round(l.idleMs / 1000)}s`;
|
|
85
|
-
const parts = [];
|
|
86
|
-
if (theirs.length) {
|
|
87
|
-
parts.push(`⚠ ${theirs.length} 个浏览器还开着(子代理已结算,但它没 close):` +
|
|
88
|
-
theirs.map((l) => `${l.id}(${l.agent || "未知角色"} · ${at(l)})`).join("、") +
|
|
89
|
-
`\n要么让它 close 并引用工具自己的三种裁决之一(已确认关闭 / 进程未核验 / 警告:关闭未完全成功),` +
|
|
90
|
-
`要么由你向用户写明为什么留着。不要替它写"已关闭"——那是把没人核实过的结论交付出去。`);
|
|
91
|
-
}
|
|
92
|
-
// The caller's own window is the one the user is looking at, and the live
|
|
93
|
-
// recheck proved this branch was the missing one: a lead that keeps a browser
|
|
94
|
-
// across rounds is legitimate, so this is a reminder, not an accusation —
|
|
95
|
-
// but silence is the defect.
|
|
96
|
-
for (const l of mine) {
|
|
97
|
-
parts.push(`(你自己还占着 ${l.id},${at(l)}。收尾前 action:"close" 并引用工具的裁决句;` +
|
|
98
|
-
`如果确实要跨轮留着,就在回复里向用户说明为什么。)`);
|
|
99
|
-
}
|
|
100
|
-
return parts.join("\n");
|
|
101
|
-
}
|
|
102
86
|
/** Resolve the host session API defensively — `messages` is what tm_join
|
|
103
87
|
* cannot work without (the collected reply); a host without it means the
|
|
104
88
|
* caller must degrade, and `create`/`promptAsync` are deliberately NOT part
|
|
@@ -311,6 +295,20 @@ export function summarizeStates(children) {
|
|
|
311
295
|
}
|
|
312
296
|
return { running, idle, error };
|
|
313
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* The children of ONE caller that are still running — the concurrency cap's
|
|
300
|
+
* data source (#49 feature 2). Pure, and reads the SAME registry `tm_join`
|
|
301
|
+
* uses rather than a second bookkeeping table: the rows already carry
|
|
302
|
+
* `parentSessionID`, so "how many of THIS session's children are live" is a
|
|
303
|
+
* filter, not a new store. An empty caller id matches nothing (never "all"),
|
|
304
|
+
* because a missing session id must not turn one session's cap into a global one.
|
|
305
|
+
*/
|
|
306
|
+
export function runningChildrenOf(children, callerSessionID) {
|
|
307
|
+
const caller = String(callerSessionID ?? "").trim();
|
|
308
|
+
if (!caller)
|
|
309
|
+
return [];
|
|
310
|
+
return children.filter((c) => c.parentSessionID === caller && c.state === "running");
|
|
311
|
+
}
|
|
314
312
|
/**
|
|
315
313
|
* Sleep used only by tm_join's bounded wait. Deliberately a plain timer:
|
|
316
314
|
* it never holds the event loop (the wait is awaited, not spun) and it is
|
|
@@ -401,8 +399,202 @@ export function cancelOutcomeParts(tally) {
|
|
|
401
399
|
}
|
|
402
400
|
return out;
|
|
403
401
|
}
|
|
402
|
+
/** Read the acceptance verdict out of the v2 bridge's `{data:{outcome,inboxID,seam,message}}`.
|
|
403
|
+
* Anything unrecognisable is `not-steered`, never `steered` — a shape we cannot read is not
|
|
404
|
+
* evidence a child was told anything. The id check is applied HERE as well as in the seam:
|
|
405
|
+
* a `steered` that carries no `^msg_` id contradicts itself, and the conservative reading
|
|
406
|
+
* wins. */
|
|
407
|
+
export function steerOutcomeOf(res) {
|
|
408
|
+
const un = unwrapClientResult(res);
|
|
409
|
+
if (!un.ok)
|
|
410
|
+
return { outcome: "not-steered", note: un.message };
|
|
411
|
+
const data = un.data;
|
|
412
|
+
const o = String(data?.outcome ?? "");
|
|
413
|
+
const seam = typeof data?.seam === "string" && data.seam ? data.seam : undefined;
|
|
414
|
+
const note = typeof data?.message === "string" && data.message ? data.message : undefined;
|
|
415
|
+
const known = ["steered", "not-steered", "no-seam"];
|
|
416
|
+
if (!known.includes(o)) {
|
|
417
|
+
return {
|
|
418
|
+
outcome: "not-steered",
|
|
419
|
+
note: o ? `宿主返回了没认出的结果(${shorten(o, 24)})` : (note ?? "宿主没有回 inbox id"),
|
|
420
|
+
...(seam ? { seam } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (o === "no-seam")
|
|
424
|
+
return { outcome: "no-seam", ...(note ? { note } : {}) };
|
|
425
|
+
const raw = typeof data?.inboxID === "string" ? data.inboxID.trim() : "";
|
|
426
|
+
const inboxID = /^msg_/.test(raw) ? raw : "";
|
|
427
|
+
if (o === "steered") {
|
|
428
|
+
if (!inboxID)
|
|
429
|
+
return { outcome: "not-steered", note: "宿主说受理但没有给出 ^msg_ 形状的 inbox id,无法确认入队", ...(seam ? { seam } : {}) };
|
|
430
|
+
return { outcome: "steered", inboxID, ...(seam ? { seam } : {}) };
|
|
431
|
+
}
|
|
432
|
+
return { outcome: "not-steered", ...(note ? { note } : {}), ...(seam ? { seam } : {}) };
|
|
433
|
+
}
|
|
434
|
+
/** What the lead reads, per outcome. Only the first may carry a success word, and none of
|
|
435
|
+
* them may claim the child has seen the text. */
|
|
436
|
+
export function steerVerdictLine(outcome, opts) {
|
|
437
|
+
const seam = opts?.seam ? ` · seam=${opts.seam}` : "";
|
|
438
|
+
switch (outcome) {
|
|
439
|
+
case "steered":
|
|
440
|
+
return `已受理${seam} · inbox=${opts?.inboxID ?? "?"} —— 宿主把它放进该子会话的 inbox,并在步边界唤醒执行(文档原句 "Steering wakes session execution.")。这不等于子代理已经读到这条插话。`;
|
|
441
|
+
case "no-seam":
|
|
442
|
+
return `这个宿主没给插话的缝(ctx.session 既没有 prompt 也没有 synthetic),未发送${opts?.note ? `:${shorten(opts.note, 80)}` : ""}`;
|
|
443
|
+
case "not-steered":
|
|
444
|
+
return `未插话${seam}:${shorten(opts?.note ?? "宿主没有回 inbox id", 120)}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
export function steerOutcomeLabel(o) {
|
|
448
|
+
return o === "steered" ? "已受理" : o === "not-steered" ? "未插话" : "无插话缝";
|
|
449
|
+
}
|
|
450
|
+
/** Fixed order so a rerun prints the same line; an absent outcome prints nothing, not "0". */
|
|
451
|
+
export function steerOutcomeParts(tally) {
|
|
452
|
+
const order = ["steered", "not-steered", "no-seam"];
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const o of order) {
|
|
455
|
+
const n = tally[o];
|
|
456
|
+
if (n)
|
|
457
|
+
out.push(`${n} ${steerOutcomeLabel(o)}`);
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
export function unsendOutcomeOf(res) {
|
|
462
|
+
const un = unwrapClientResult(res);
|
|
463
|
+
if (!un.ok)
|
|
464
|
+
return { outcome: "threw", note: un.message };
|
|
465
|
+
const data = un.data;
|
|
466
|
+
const o = String(data?.outcome ?? "");
|
|
467
|
+
const known = ["cancelled", "noop", "threw", "no-seam"];
|
|
468
|
+
const note = typeof data?.message === "string" && data.message ? data.message : undefined;
|
|
469
|
+
if (!known.includes(o)) {
|
|
470
|
+
// The host answered with something we cannot read. It is NOT a cancel, and calling a
|
|
471
|
+
// parse miss a refusal would be inventing a host verdict — so it keeps the "answered,
|
|
472
|
+
// named nothing" reading with what it saw attached.
|
|
473
|
+
return { outcome: "noop", note: o ? `宿主返回了没认出的结果(${shorten(o, 24)})` : (note ?? "宿主应答但没有指认被撤回的项") };
|
|
474
|
+
}
|
|
475
|
+
if (o === "cancelled") {
|
|
476
|
+
const raw = typeof data?.inboxID === "string" ? data.inboxID.trim() : "";
|
|
477
|
+
if (!/^msg_/.test(raw))
|
|
478
|
+
return { outcome: "noop", note: "宿主回说撤回但没有给出 ^msg_ 形状的 id" };
|
|
479
|
+
return { outcome: "cancelled", inboxID: raw };
|
|
480
|
+
}
|
|
481
|
+
if (o === "threw")
|
|
482
|
+
return { outcome: "threw", ...(note ? { note } : {}) };
|
|
483
|
+
if (o === "no-seam")
|
|
484
|
+
return { outcome: "no-seam", ...(note ? { note } : {}) };
|
|
485
|
+
return { outcome: "noop", ...(note ? { note } : {}) };
|
|
486
|
+
}
|
|
487
|
+
export function unsendVerdictLine(outcome, opts) {
|
|
488
|
+
switch (outcome) {
|
|
489
|
+
case "cancelled":
|
|
490
|
+
return `已撤回(宿主回指 inbox=${opts?.inboxID ?? "?"})—— 它此前还没被投递`;
|
|
491
|
+
case "noop":
|
|
492
|
+
return `宿主应答了但没有指认被撤回的项${opts?.note ? `(${shorten(opts.note, 80)})` : ""} —— 按文档 "Unavailable items are a no-op",已投递或不存在的项就是这个结果,所以我没有撤回任何东西可报告`;
|
|
493
|
+
case "threw":
|
|
494
|
+
return `撤回被宿主拒绝:${shorten(opts?.note ?? "", 120)}`;
|
|
495
|
+
case "no-seam":
|
|
496
|
+
return "这个宿主没给 session.inbox.cancel 的缝,未发送撤回";
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
export function unsendOutcomeLabel(o) {
|
|
500
|
+
return o === "cancelled" ? "已撤回" : o === "noop" ? "应答未指认" : o === "threw" ? "被宿主拒绝" : "无撤回缝";
|
|
501
|
+
}
|
|
502
|
+
export function unsendOutcomeParts(tally) {
|
|
503
|
+
const order = ["cancelled", "noop", "threw", "no-seam"];
|
|
504
|
+
const out = [];
|
|
505
|
+
for (const o of order) {
|
|
506
|
+
const n = tally[o];
|
|
507
|
+
if (n)
|
|
508
|
+
out.push(`${n} ${unsendOutcomeLabel(o)}`);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
/** `unsend` arrives in three shapes and NONE of them is guessed at: a real object, a JSON
|
|
513
|
+
* string (models send objects as text — the exact `ids` failure this file already records),
|
|
514
|
+
* or the compact `"ses_…|msg_…"`. A missing field is an error, never a default, because
|
|
515
|
+
* withdrawing the wrong inbox item is a write to someone else's queue. */
|
|
516
|
+
export function parseUnsendArg(raw) {
|
|
517
|
+
let o = null;
|
|
518
|
+
if (raw && typeof raw === "object")
|
|
519
|
+
o = raw;
|
|
520
|
+
else if (typeof raw === "string") {
|
|
521
|
+
const s = raw.trim();
|
|
522
|
+
if (s.startsWith("{")) {
|
|
523
|
+
try {
|
|
524
|
+
const parsed = JSON.parse(s);
|
|
525
|
+
if (parsed && typeof parsed === "object")
|
|
526
|
+
o = parsed;
|
|
527
|
+
else
|
|
528
|
+
return { ok: false, error: `unsend 的 JSON 文本不是一个对象(收到 ${shorten(s, 60)})` };
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return { ok: false, error: `unsend 的 JSON 文本解析失败(收到 ${shorten(s, 60)})` };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
const m = /^(ses[\w-]*)\s*[|/]\s*(msg_[\w-]+)$/.exec(s);
|
|
536
|
+
if (!m)
|
|
537
|
+
return { ok: false, error: `unsend 需要 {"sessionID":"ses_…","inboxID":"msg_…"}(或 "ses_…|msg_…"),收到 ${shorten(s, 60)}` };
|
|
538
|
+
o = { sessionID: m[1], inboxID: m[2] };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
return { ok: false, error: `unsend 需要对象 {sessionID, inboxID},收到 ${raw === undefined ? "undefined" : typeof raw}` };
|
|
543
|
+
}
|
|
544
|
+
const sessionID = String(o?.sessionID ?? o?.id ?? "").trim();
|
|
545
|
+
const inboxID = String(o?.inboxID ?? o?.inbox_id ?? o?.messageID ?? "").trim();
|
|
546
|
+
if (!sessionID.startsWith("ses"))
|
|
547
|
+
return { ok: false, error: `unsend.sessionID 不是一个会话 id(收到 ${shorten(sessionID || "(空)", 40)})` };
|
|
548
|
+
if (!/^msg_/.test(inboxID))
|
|
549
|
+
return { ok: false, error: `unsend.inboxID 必须匹配宿主文档的 ^msg_ 模式(收到 ${shorten(inboxID || "(空)", 40)})` };
|
|
550
|
+
return { ok: true, sessionID, inboxID };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Undelivered inbox items, reduced to a NAME and a SHAPE — never the body. An item's text
|
|
554
|
+
* is the lead's own interjection or a child's brief, and the R6 口径 binds a read-back as
|
|
555
|
+
* much as a diagnostic, so the answer can say "there are 3, here are their ids" and the
|
|
556
|
+
* caller can `unsend` one without any of their content entering the parent's context.
|
|
557
|
+
* `skipped` is counted rather than silent: an item whose id does not match the documented
|
|
558
|
+
* `^msg_` pattern is a host shape we did not recognise, not an item that does not exist.
|
|
559
|
+
*/
|
|
560
|
+
export function inboxItemLines(list, max = 20) {
|
|
561
|
+
if (!Array.isArray(list))
|
|
562
|
+
return { lines: [], total: 0, skipped: 0 };
|
|
563
|
+
const lines = [];
|
|
564
|
+
let skipped = 0;
|
|
565
|
+
for (const item of list.slice(0, max)) {
|
|
566
|
+
if (!item || typeof item !== "object") {
|
|
567
|
+
skipped++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const rec = item;
|
|
571
|
+
const info = rec.info && typeof rec.info === "object" ? rec.info : undefined;
|
|
572
|
+
const id = String(rec.inboxID ?? rec.id ?? info?.id ?? "").trim();
|
|
573
|
+
if (!/^msg_/.test(id)) {
|
|
574
|
+
skipped++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const kind = String(rec.kind ?? rec.role ?? info?.role ?? rec.type ?? "")
|
|
578
|
+
.trim()
|
|
579
|
+
.toLowerCase()
|
|
580
|
+
.slice(0, 16);
|
|
581
|
+
const delivery = String(rec.delivery ?? info?.delivery ?? "").trim().toLowerCase().slice(0, 8);
|
|
582
|
+
const parts = [`id=${id}`];
|
|
583
|
+
if (kind)
|
|
584
|
+
parts.push(`kind=${kind}`);
|
|
585
|
+
if (delivery)
|
|
586
|
+
parts.push(`delivery=${delivery}`);
|
|
587
|
+
// a LENGTH only — the text itself is never copied into the parent's reply
|
|
588
|
+
if (typeof rec.text === "string")
|
|
589
|
+
parts.push(`textlen=${rec.text.length}`);
|
|
590
|
+
lines.push(parts.join(" · "));
|
|
591
|
+
}
|
|
592
|
+
if (list.length > max)
|
|
593
|
+
skipped += list.length - max;
|
|
594
|
+
return { lines, total: list.length, skipped };
|
|
595
|
+
}
|
|
404
596
|
export function buildDispatchTools(deps) {
|
|
405
|
-
const leadAgent = deps.leadAgent ?? "
|
|
597
|
+
const leadAgent = deps.leadAgent ?? "Team";
|
|
406
598
|
const targets = deps.targets ?? DISPATCH_TARGETS;
|
|
407
599
|
const maxWaitMs = deps.maxWaitMs ?? 60_000;
|
|
408
600
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -428,8 +620,8 @@ export function buildDispatchTools(deps) {
|
|
|
428
620
|
return;
|
|
429
621
|
let un;
|
|
430
622
|
try {
|
|
431
|
-
// PROPERTY-ACCESS CALL — the SDK's endpoints need their receiver
|
|
432
|
-
//
|
|
623
|
+
// PROPERTY-ACCESS CALL — the SDK's endpoints need their receiver;
|
|
624
|
+
// `const f = api.status` then
|
|
433
625
|
// `f(...)` threw "Cannot read properties of undefined (reading
|
|
434
626
|
// 'client')" on the live host and killed every dispatch.
|
|
435
627
|
un = unwrapClientResult(await api.status(directory ? { query: { directory } } : {}));
|
|
@@ -597,11 +789,50 @@ export function buildDispatchTools(deps) {
|
|
|
597
789
|
}
|
|
598
790
|
}
|
|
599
791
|
}
|
|
792
|
+
/** #steer — the PARENTAGE GATE, and a hard one: an interjection is a WRITE into
|
|
793
|
+
* someone else's conversation, so it takes the lock `claimNamedChild` takes and takes
|
|
794
|
+
* it BEFORE anything is sent — the host itself must say `parentID === the caller`.
|
|
795
|
+
* Three shapes are all refusals and all fail closed: no seam to ask, a call that
|
|
796
|
+
* threw, and a session whose parent the host did not name (an `unknown` ownership is
|
|
797
|
+
* never treated as ours — the same rule `v2-scope` applies to sessions). A foreign
|
|
798
|
+
* parent is NAMED, because "whose child is it" is the question the lead has next. */
|
|
799
|
+
async function parentageOf(sid, parent, directory) {
|
|
800
|
+
if (!sid)
|
|
801
|
+
return { ok: false, why: "没有点名子会话 id" };
|
|
802
|
+
if (!parent)
|
|
803
|
+
return { ok: false, why: "调用方会话 id 缺失,无法核对归属" };
|
|
804
|
+
if (sid === parent)
|
|
805
|
+
return { ok: false, why: "那是你自己的会话,不是子代理" };
|
|
806
|
+
if (typeof api?.get !== "function") {
|
|
807
|
+
return { ok: false, why: "这个宿主客户端没有给出读取会话的缝(session.get),我无法确认它是你的子会话,因此没有发送" };
|
|
808
|
+
}
|
|
809
|
+
let un;
|
|
810
|
+
try {
|
|
811
|
+
un = unwrapClientResult(await api.get({ path: { id: sid }, ...(directory ? { query: { directory } } : {}) }));
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
return { ok: false, why: `向宿主核对归属失败:${describeHostError(err?.message ?? err)}` };
|
|
815
|
+
}
|
|
816
|
+
if (!un.ok)
|
|
817
|
+
return { ok: false, why: `宿主没有给出这个会话的信息:${shorten(un.message ?? "", 90)}` };
|
|
818
|
+
const info = un.data;
|
|
819
|
+
const pid = String(info?.parentID ?? "").trim();
|
|
820
|
+
if (!pid)
|
|
821
|
+
return { ok: false, why: "宿主没有回 parentID —— 归属未知,未知不当作自己的,未发送" };
|
|
822
|
+
if (pid !== parent) {
|
|
823
|
+
// Both ids printed in full (bounded, not to 24): "whose child is it" is the question
|
|
824
|
+
// the lead has next, and a truncated id is one it cannot act on.
|
|
825
|
+
return { ok: false, why: `它是会话 ${shorten(pid, 40)} 的子代理,不是本会话(${shorten(parent, 40)})的 —— 不是我的,我不碰` };
|
|
826
|
+
}
|
|
827
|
+
const rawAgent = String(info?.agent ?? "").trim().toLowerCase();
|
|
828
|
+
return { ok: true, agent: rawAgent || children.get(sid)?.agent || "subagent" };
|
|
829
|
+
}
|
|
600
830
|
const join = {
|
|
601
831
|
description: `Collect the sub-agent work attached to THIS session — the read-back side of delegation. You reach for it in two situations: the host's background \`task\` finished and TeamMode replaced its injected reply with a preview (pull the whole thing back with \`{ ids: ["<child session>"] }\`), or a dispatched child from before this build is still open and needs collecting or cancelling.
|
|
602
832
|
- No args: status snapshot of every open child of THIS session — running / idle(done) / error, with elapsed seconds. Cheap and non-blocking: use it to decide whether to keep working or start merging.
|
|
603
833
|
- { waitMs: 30000 }: bounded wait (capped at ${waitLabel(maxWaitMs)}) until every child settles. A wait BLOCKS YOU — your turn is parked in this tool call, so it is the synchronous \`task\` experience with none of its visibility. Wait once, briefly, and only when the very next step needs the answer; a second consecutive wait after nothing settled is cut to 10s and answered with what to do instead. Never wait for a child whose result you do not need — abort it instead ({ cancel: true }).
|
|
604
834
|
- { ids: [...] }: restrict to those child sessions — a host \`task\` child is collectable when you NAME it (its parentage is verified against the host's own session tree, never assumed); { cancel: true }: abort still-running ones.
|
|
835
|
+
- Steer a child while it runs — the same call, no separate tool: { steer: "先停手,验收改成 X", ids: ["<child>"] } admits the line into that child's inbox with delivery "steer" (or "queue") and the host wakes execution at the step boundary. 已受理 is the whole claim: it is NOT proof the child read it, and the reply never says 已送达. { unread: true, ids: ["<child>"] } lists that child's undelivered inbox items (ids + shape only, never the body); { unsend: {"sessionID":"ses_…","inboxID":"msg_…"} } withdraws one that has not been delivered — an already-delivered item is the documented no-op and is reported as one. All three ask the host for parentage FIRST and refuse a session that is not yours; a host without the seam answers no-seam instead of pretending.
|
|
605
836
|
- A plugin/host restart does NOT orphan a child: those the host still lists under your session are re-adopted automatically (their rows are marked 接管), and one that answered while nobody was listening is reported 已完成, not lost.
|
|
606
837
|
- Replies come back through the offload pipeline: a long sub-agent report arrives as a handle + ≤80-token preview (page it with tm_fetch), so a batch of parallel work does not multiply your context. STATUS: blocked/failed children are surfaced first, always.`,
|
|
607
838
|
args: {
|
|
@@ -609,6 +840,26 @@ export function buildDispatchTools(deps) {
|
|
|
609
840
|
waitMs: { descriptor: `waitMs: number (optional 0..${maxWaitMs} — bounded wait; a SECOND consecutive wait is cut to 10s, because waiting is not parallel work)` },
|
|
610
841
|
cancel: { descriptor: "cancel: true (abort the still-running children in this set)" },
|
|
611
842
|
includeText: { descriptor: "includeText: false to get only the status table (default true)" },
|
|
843
|
+
// #steer — the four interjection args. They ride the DESCRIPTOR channel (tm_join
|
|
844
|
+
// builds its args without zod), and on v2 that channel is what the host serialises
|
|
845
|
+
// into the parameter spec, so the head token decides the type: `string` → string,
|
|
846
|
+
// `steer|queue` → an enum, `true` → boolean. `unsend` is described as a STRING on
|
|
847
|
+
// purpose — the descriptor parser has no object rule, and a parameter the schema
|
|
848
|
+
// calls an object while the host validates strictly is a parameter the model cannot
|
|
849
|
+
// send — so the object shape arrives as its JSON text, and execute() also accepts a
|
|
850
|
+
// real object and the compact "ses_…|msg_…" form (parseUnsendArg).
|
|
851
|
+
steer: {
|
|
852
|
+
descriptor: 'steer: string (optional — one line to interject into the child named by ids; the host admits it as inbox input and wakes execution at the step boundary, so 已受理 is not the same claim as 已送达. Needs exactly one id, and parentage is verified with the host first.)',
|
|
853
|
+
},
|
|
854
|
+
delivery: {
|
|
855
|
+
descriptor: "delivery: steer|queue (optional, default steer — applies to steer only. Any other value is refused rather than silently defaulted.)",
|
|
856
|
+
},
|
|
857
|
+
unread: {
|
|
858
|
+
descriptor: "unread: true (optional — list the undelivered inbox items of the child named by ids: ids and shape only, never the body)",
|
|
859
|
+
},
|
|
860
|
+
unsend: {
|
|
861
|
+
descriptor: 'unsend: string (optional — the JSON object as text {"sessionID":"ses_…","inboxID":"msg_…"}, or "ses_…|msg_…"; withdraws an item that has not been delivered yet. An unavailable item is the documented no-op and is reported as one, not as a success.)',
|
|
862
|
+
},
|
|
612
863
|
},
|
|
613
864
|
execute: async (rawArgs, ctx) => {
|
|
614
865
|
const tool = "tm_join";
|
|
@@ -616,7 +867,7 @@ export function buildDispatchTools(deps) {
|
|
|
616
867
|
const args = (rawArgs ?? {});
|
|
617
868
|
const c = (ctx ?? {});
|
|
618
869
|
const caller = String(c.agent ?? "").trim();
|
|
619
|
-
if (caller && caller
|
|
870
|
+
if (caller && !sameAgent(caller, leadAgent)) {
|
|
620
871
|
return toToolResult(tmError(tool, "governance", `只有 ${leadAgent} 能收集派发结果。`));
|
|
621
872
|
}
|
|
622
873
|
const parent = String(c.sessionID ?? "").trim();
|
|
@@ -657,37 +908,152 @@ export function buildDispatchTools(deps) {
|
|
|
657
908
|
}
|
|
658
909
|
}
|
|
659
910
|
}
|
|
660
|
-
// #
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
911
|
+
// ── #steer: 插话 / 未读 / 撤回 ──────────────────────────────────────────────
|
|
912
|
+
// Three new actions, each answered and returned HERE. A call that names none of
|
|
913
|
+
// them falls through to the collect/wait flow below untouched, which is what keeps
|
|
914
|
+
// the shipped `ids` / `waitMs` / `cancel` / `includeText` semantics byte-exact.
|
|
915
|
+
const steerText = typeof args.steer === "string" ? args.steer.trim() : "";
|
|
916
|
+
const wantsUnread = args.unread === true || args.unread === "true";
|
|
917
|
+
const unsendRequested = args.unsend !== undefined && args.unsend !== null && args.unsend !== "";
|
|
918
|
+
const steerActions = (steerText ? 1 : 0) + (wantsUnread ? 1 : 0) + (unsendRequested ? 1 : 0);
|
|
919
|
+
if (!steerActions && args.steer !== undefined) {
|
|
920
|
+
return toToolResult(tmError(tool, "args", "steer 需要非空文本(只要状态就去掉它,或改用 unread / cancel)。"));
|
|
921
|
+
}
|
|
922
|
+
if (steerActions > 1) {
|
|
923
|
+
return toToolResult(tmError(tool, "args", "一次只能做一件插话动作:steer / unread / unsend 三选一 —— 合在一起,三种结论就没法分开计数了。"));
|
|
924
|
+
}
|
|
925
|
+
if (steerActions) {
|
|
926
|
+
const deliveryRaw = typeof args.delivery === "string" ? args.delivery.trim().toLowerCase() : "";
|
|
927
|
+
if (args.delivery !== undefined && deliveryRaw !== "steer" && deliveryRaw !== "queue") {
|
|
928
|
+
return toToolResult(tmError(tool, "args", `delivery 只能是 "steer" 或 "queue"(收到 ${shorten(String(args.delivery ?? ""), 24)}),未发送。`));
|
|
673
929
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
930
|
+
// The documented default is not something we let the host decide: an omitted
|
|
931
|
+
// `delivery` is spelled `steer` here, so the value in the reply is the value that
|
|
932
|
+
// was sent rather than a guess about this build's inbox scheduler.
|
|
933
|
+
const delivery = deliveryRaw === "queue" ? "queue" : "steer";
|
|
934
|
+
const unsendParsed = unsendRequested ? parseUnsendArg(args.unsend) : null;
|
|
935
|
+
if (unsendParsed && !unsendParsed.ok)
|
|
936
|
+
return toToolResult(tmError(tool, "args", `${unsendParsed.error}。`));
|
|
937
|
+
const unsendID = unsendParsed && unsendParsed.ok ? unsendParsed.inboxID : "";
|
|
938
|
+
let target = unsendID ? (unsendParsed && unsendParsed.ok ? unsendParsed.sessionID : "") : "";
|
|
939
|
+
if (!target) {
|
|
940
|
+
if (!idFilter) {
|
|
941
|
+
return toToolResult(tmError(tool, "args", `${steerText ? "steer" : "unread"} 要用 ids 点名一个子会话:{ ids: ["ses_…"], ${steerText ? "steer" : "unread"}: … }。`));
|
|
942
|
+
}
|
|
943
|
+
if (idFilter.size !== 1) {
|
|
944
|
+
return toToolResult(tmError(tool, "args", `插话只能针对一个子会话(ids 里有 ${idFilter.size} 个)。`));
|
|
945
|
+
}
|
|
946
|
+
target = [...idFilter][0];
|
|
677
947
|
}
|
|
678
|
-
const
|
|
679
|
-
if (
|
|
948
|
+
const gate = await parentageOf(target, parent, directory);
|
|
949
|
+
if (!gate.ok) {
|
|
680
950
|
log({
|
|
681
951
|
step_id: "join",
|
|
682
|
-
event: "
|
|
683
|
-
|
|
684
|
-
|
|
952
|
+
event: unsendID ? "unsend_refused" : steerText ? "steer_refused" : "unread_refused",
|
|
953
|
+
child: target,
|
|
954
|
+
reason: "parentage",
|
|
685
955
|
});
|
|
956
|
+
return toToolResult(`没有发送:${gate.why}。`);
|
|
686
957
|
}
|
|
687
|
-
|
|
688
|
-
|
|
958
|
+
const row = children.get(target);
|
|
959
|
+
const rowLine = row
|
|
960
|
+
? renderChildLine(row, (row.finishedAt ?? now()) - row.startedAt)
|
|
961
|
+
: `${target} · ${gate.agent} ·(本进程没有它的登记行,归属由宿主确认)`;
|
|
962
|
+
if (steerText) {
|
|
963
|
+
let outcome = "no-seam";
|
|
964
|
+
let inboxID;
|
|
965
|
+
let note;
|
|
966
|
+
let seam;
|
|
967
|
+
if (api && api.v2SteerSeam === true && typeof api.prompt === "function") {
|
|
968
|
+
const got = steerOutcomeOf(await api.prompt({ sessionID: target, text: steerText, delivery }));
|
|
969
|
+
outcome = got.outcome;
|
|
970
|
+
inboxID = got.inboxID;
|
|
971
|
+
note = got.note;
|
|
972
|
+
seam = got.seam;
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
// The v1 client is NOT called: its `session.prompt` takes `{path,body}` with
|
|
976
|
+
// no `delivery` field, so firing it would be a write with the wrong contract
|
|
977
|
+
// and a verdict read off whatever came back.
|
|
978
|
+
note = "这个客户端不是 v2 的 ctx.session 桥(v1 的 session.prompt 走 {path,body},没有 delivery 字段),未调用";
|
|
979
|
+
}
|
|
980
|
+
log({
|
|
981
|
+
step_id: "join",
|
|
982
|
+
event: "steer",
|
|
983
|
+
child: target,
|
|
984
|
+
agent: gate.agent,
|
|
985
|
+
delivery,
|
|
986
|
+
outcome,
|
|
987
|
+
ok: outcome === "steered",
|
|
988
|
+
...(inboxID ? { inboxID } : {}),
|
|
989
|
+
...(seam ? { seam } : {}),
|
|
990
|
+
// the LENGTH, never the text — R6 binds the audit trail as much as the reply
|
|
991
|
+
chars: steerText.length,
|
|
992
|
+
});
|
|
993
|
+
const tally = {};
|
|
994
|
+
tally[outcome] = 1;
|
|
995
|
+
log({ step_id: "join", event: "steer_summary", count: 1, outcomes: steerOutcomeParts(tally).join(" / ") });
|
|
996
|
+
const wake = row && row.state !== "running"
|
|
997
|
+
? `\n它现在不是运行中(${row.state === "error" ? "失败" : "已结算"});按文档 steer 会唤醒它的执行,但唤醒之后它做什么,要等它结算才看得到。`
|
|
998
|
+
: "";
|
|
999
|
+
return toToolResult(`插话目标:${rowLine}\n插话结果:${steerVerdictLine(outcome, { inboxID, note, seam })}\n插话计数:${steerOutcomeParts(tally).join(" · ")}${wake}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (wantsUnread) {
|
|
1002
|
+
if (!api || api.v2SteerSeam !== true || typeof api.inboxList !== "function") {
|
|
1003
|
+
log({ step_id: "join", event: "unread", child: target, outcome: "no-seam" });
|
|
1004
|
+
return toToolResult(`未读列表没有查询:这个宿主没给 session.inbox.list 的缝(v1 客户端没有这条契约),未发送。\n目标:${rowLine}`);
|
|
1005
|
+
}
|
|
1006
|
+
const un = unwrapClientResult(await api.inboxList({ sessionID: target }));
|
|
1007
|
+
const data = un.ok
|
|
1008
|
+
? un.data
|
|
1009
|
+
: null;
|
|
1010
|
+
const seamOut = String(data?.outcome ?? "");
|
|
1011
|
+
const spelling = typeof data?.spelling === "string" ? data.spelling : "";
|
|
1012
|
+
if (!un.ok || seamOut === "threw" || seamOut === "no-seam") {
|
|
1013
|
+
const why = seamOut === "no-seam"
|
|
1014
|
+
? "这个宿主没给 session.inbox.list 的缝(两种拼写都试了)"
|
|
1015
|
+
: `查询被宿主拒绝:${shorten(String(data?.message ?? (un.ok ? "" : un.message) ?? ""), 120)}`;
|
|
1016
|
+
log({ step_id: "join", event: "unread", child: target, outcome: seamOut === "no-seam" ? "no-seam" : "threw" });
|
|
1017
|
+
return toToolResult(`未读列表没有查到:${why}。\n目标:${rowLine}`);
|
|
1018
|
+
}
|
|
1019
|
+
const listed = inboxItemLines(data?.items);
|
|
1020
|
+
log({
|
|
1021
|
+
step_id: "join",
|
|
1022
|
+
event: "unread",
|
|
1023
|
+
child: target,
|
|
1024
|
+
outcome: "listed",
|
|
1025
|
+
count: listed.lines.length,
|
|
1026
|
+
total: listed.total,
|
|
1027
|
+
skipped: listed.skipped,
|
|
1028
|
+
...(spelling ? { spelling } : {}),
|
|
1029
|
+
});
|
|
1030
|
+
const trimmed = listed.total > listed.lines.length
|
|
1031
|
+
? `(宿主返回 ${listed.total} 项,${listed.skipped} 项的 id 不匹配 ^msg_ 或超出 20 条上限,没有列出)`
|
|
1032
|
+
: "";
|
|
1033
|
+
return toToolResult(`未投递的插话项(${target}):${listed.lines.length} 条${trimmed}\n` +
|
|
1034
|
+
`${listed.lines.join("\n") || "(没有未投递项)"}\n` +
|
|
1035
|
+
`(只给 id 与形状,正文不进这里 —— 要撤回某条就 unsend 它的 id)\n目标:${rowLine}`);
|
|
1036
|
+
}
|
|
1037
|
+
// unsend — the only way to take back a line that has not been delivered yet.
|
|
1038
|
+
if (!api || api.v2SteerSeam !== true || typeof api.inboxCancel !== "function") {
|
|
1039
|
+
log({ step_id: "join", event: "unsend", child: target, inboxID: unsendID, outcome: "no-seam" });
|
|
1040
|
+
return toToolResult(`撤回没有发送:这个宿主没给 session.inbox.cancel 的缝(v1 客户端没有这条契约)。\n目标:${rowLine}`);
|
|
1041
|
+
}
|
|
1042
|
+
const got = unsendOutcomeOf(await api.inboxCancel({ sessionID: target, inboxID: unsendID }));
|
|
1043
|
+
const tallyU = {};
|
|
1044
|
+
tallyU[got.outcome] = 1;
|
|
1045
|
+
log({
|
|
1046
|
+
step_id: "join",
|
|
1047
|
+
event: "unsend",
|
|
1048
|
+
child: target,
|
|
1049
|
+
inboxID: unsendID,
|
|
1050
|
+
outcome: got.outcome,
|
|
1051
|
+
ok: got.outcome === "cancelled",
|
|
1052
|
+
});
|
|
1053
|
+
log({ step_id: "join", event: "unsend_summary", count: 1, outcomes: unsendOutcomeParts(tallyU).join(" / ") });
|
|
1054
|
+
return toToolResult(`撤回目标:${target} · inbox=${unsendID}\n结果:${unsendVerdictLine(got.outcome, { inboxID: got.inboxID, note: got.note })}\n撤回计数:${unsendOutcomeParts(tallyU).join(" · ")}\n${rowLine}`);
|
|
1055
|
+
}
|
|
689
1056
|
if (!mine.length) {
|
|
690
|
-
const lease = leaseLine();
|
|
691
1057
|
// The parenthetical is now chosen by whether we actually looked, so a
|
|
692
1058
|
// host we cannot query no longer reports as "confirmed: nothing there".
|
|
693
1059
|
const saw = adoption.looked;
|
|
@@ -715,8 +1081,7 @@ export function buildDispatchTools(deps) {
|
|
|
715
1081
|
// the transcript, and the honest reason is that a synchronous host
|
|
716
1082
|
// task never enters this registry at all.
|
|
717
1083
|
`如果你用的是同步 \`task\`:宿主自己就把结果收回来了,它从不登记在这里,这不是派发失败。` +
|
|
718
|
-
`只有后台 \`task {background:true}\`(或显式带 ids)需要 tm_join 来取。`
|
|
719
|
-
(lease ? `\n${lease}` : ""));
|
|
1084
|
+
`只有后台 \`task {background:true}\`(或显式带 ids)需要 tm_join 来取。`);
|
|
720
1085
|
}
|
|
721
1086
|
await settleAdopted(mine, directory);
|
|
722
1087
|
const waitMs = Math.max(0, Math.min(maxWaitMs, Number(args.waitMs) || 0));
|
|
@@ -887,14 +1252,6 @@ export function buildDispatchTools(deps) {
|
|
|
887
1252
|
});
|
|
888
1253
|
}
|
|
889
1254
|
}
|
|
890
|
-
// #80/#86/#87: THE LEASE TRIPWIRE — one computation (leaseLine above),
|
|
891
|
-
// attached to every answer this tool gives, so a settled round and an
|
|
892
|
-
// empty one cannot disagree about whether a window is still open.
|
|
893
|
-
if (settled) {
|
|
894
|
-
const lease = leaseLine();
|
|
895
|
-
if (lease)
|
|
896
|
-
header.push(lease);
|
|
897
|
-
}
|
|
898
1255
|
const wantText = !(args.includeText === false || args.includeText === "false");
|
|
899
1256
|
const collectible = mine.filter((r) => r.state !== "running");
|
|
900
1257
|
const blocks = [];
|