@te-river/opencode-team-mode 1.6.2 → 1.7.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 +117 -118
- package/README.zh-CN.md +98 -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 +33 -0
- package/dist/host/v2-capabilities.d.ts.map +1 -1
- package/dist/host/v2-capabilities.js +50 -1
- package/dist/host/v2-capabilities.js.map +1 -1
- package/dist/host/v2-compaction.d.ts +129 -0
- package/dist/host/v2-compaction.d.ts.map +1 -0
- package/dist/host/v2-compaction.js +346 -0
- package/dist/host/v2-compaction.js.map +1 -0
- 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 +2 -0
- package/dist/host/v2-events.d.ts.map +1 -1
- package/dist/host/v2-events.js +27 -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 +25 -0
- package/dist/host/v2-types.d.ts.map +1 -1
- package/dist/host/v2.d.ts +14 -12
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +500 -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 +320 -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 +109 -33
- package/docs/installation.md +9 -0
- package/docs/research/browser-pane.md +5 -0
- package/docs/v2-port-notes.md +87 -0
- package/package.json +1 -1
- package/scripts/gen-v2-config.mjs +246 -94
- package/scripts/lib/fake-ctx.mjs +75 -0
package/dist/host/v2.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The v2 personality: `Plugin.define({id, setup(ctx)})
|
|
2
|
+
* The v2 personality: `Plugin.define({id, setup(ctx)})` — the only personality
|
|
3
|
+
* since 1.7.0 (v1 support was removed).
|
|
3
4
|
*
|
|
4
5
|
* `Plugin.define` in @opencode/plugin@2.0.16 is literally `plugin => plugin`,
|
|
5
6
|
* so this module exports the plain object and the v2 SDK never becomes a
|
|
6
|
-
* runtime dependency
|
|
7
|
-
* installable by a v1 user who cannot resolve that package at all.
|
|
7
|
+
* runtime dependency.
|
|
8
8
|
*
|
|
9
9
|
* Scope of this file (milestone M2): build the governed runtime on a
|
|
10
10
|
* filesystem-backed client shim, register the tm_* tools through
|
|
@@ -13,17 +13,14 @@
|
|
|
13
13
|
* session hooks / compaction / description appends (#93), or the browser
|
|
14
14
|
* (#95). Those need this seam proven live first.
|
|
15
15
|
*
|
|
16
|
-
* Two
|
|
17
|
-
* discovered later:
|
|
16
|
+
* Two host limits are stated rather than discovered later:
|
|
18
17
|
* - creating agents: `AgentEditor` has no `add`, so the six roles must come
|
|
19
18
|
* from the user's config (the installer writes `agents/*.md`).
|
|
20
|
-
* - the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* this promotes unconditionally; `defaultAgent: false` in the plugin options
|
|
26
|
-
* opts out, exactly as on v1.
|
|
19
|
+
* - the default-agent promotion: `default(id)` has NO getter, so "only if the
|
|
20
|
+
* user did not choose" is not expressible — the choice is between never
|
|
21
|
+
* promoting and promoting every boot. The user's standing instruction
|
|
22
|
+
* (2026-09-25) is that Team is ALWAYS the default, so this promotes
|
|
23
|
+
* unconditionally; `defaultAgent: false` in the plugin options opts out.
|
|
27
24
|
*
|
|
28
25
|
* The official confirmation dialog is absent as well, and for a different
|
|
29
26
|
* reason: probed on the live host, a plugin has no way to raise one.
|
|
@@ -31,6 +28,7 @@
|
|
|
31
28
|
* the v2-worded refusal set below.
|
|
32
29
|
*/
|
|
33
30
|
import { agents } from "../agents.js";
|
|
31
|
+
import { normalizeAgentName } from "../identity.js";
|
|
34
32
|
import { DEFAULT_TTL_DAYS, resolveTtlMs, startBlackboardMaintenance } from "../blackboard.js";
|
|
35
33
|
import { parseExtraDeny, resolveEnvProtectMode } from "../envprotect.js";
|
|
36
34
|
import { createTmTools } from "../tm/index.js";
|
|
@@ -38,16 +36,21 @@ import { agentVisibilityLine } from "../tm/stats.js";
|
|
|
38
36
|
import { setAskUnavailableNote } from "../tm/perm-ask.js";
|
|
39
37
|
import { setPrivateSpacePolicy } from "../tm/webfetch.js";
|
|
40
38
|
import { blackboardNote } from "./note.js";
|
|
41
|
-
import { applyV2BackgroundForce, applyV2PermissionGuards, needsCoarseShellAsk } from "./v2-guard.js";
|
|
39
|
+
import { applyV2BackgroundForce, applyV2EnvFileInputGuard, applyV2PermissionGuards, applyV2ShellTimeoutClamp, needsCoarseShellAsk } from "./v2-guard.js";
|
|
42
40
|
import { applyV2EventFeed } from "./v2-events.js";
|
|
43
41
|
import { createV2SessionReader } from "./v2-session-client.js";
|
|
44
42
|
import { applyV2Probe, probeSummary } from "./v2-probe.js";
|
|
45
43
|
import { applyV2NativeOffload } from "./v2-offload.js";
|
|
44
|
+
import { applyV2ProbeChain } from "./v2-probe-chain.js";
|
|
46
45
|
import { applyV2SubagentRegistry, applyV2CompletionWatch } from "./v2-subagent.js";
|
|
47
46
|
import { applyV2BrowserGate, browserGateSummary } from "./v2-browser-gate.js";
|
|
48
47
|
import { seedWebfetchDomains } from "../tm/webfetch.js";
|
|
49
48
|
import { v2CapabilityRows } from "./v2-capabilities.js";
|
|
50
|
-
import { applyV2SessionLayer, removalPlan } from "./v2-session.js";
|
|
49
|
+
import { applyV2SessionLayer, removalPlan, resolveSplitConfig } from "./v2-session.js";
|
|
50
|
+
import { applyV2RetryGovernor, createRetryGovernor, resolveRetryConfig } from "./v2-retry.js";
|
|
51
|
+
import { applyV2EarlyCompaction, resolveCompactConfig } from "./v2-compaction.js";
|
|
52
|
+
import { applyV2ContextPrune, resolvePruneConfig } from "./v2-prune.js";
|
|
53
|
+
import { checkDefaultAgentRole, globalConfigDir } from "./v2-default-agent.js";
|
|
51
54
|
import { createStorageLedgerStore } from "../tm/ledger.js";
|
|
52
55
|
import { createTeamScope } from "./v2-scope.js";
|
|
53
56
|
import { bindV2Tool } from "./v2-tool.js";
|
|
@@ -69,8 +72,24 @@ function directoryOf(ctx) {
|
|
|
69
72
|
return loc.project.directory;
|
|
70
73
|
return process.cwd();
|
|
71
74
|
}
|
|
75
|
+
/** #49 feature 2: the concurrency cap. Default 3 (matches "at most 3 parallel
|
|
76
|
+
* reviewers" and leaves a normal user headroom); `0` disables it. A missing or
|
|
77
|
+
* unparseable value falls back to the default rather than to 0, because a typo
|
|
78
|
+
* must not silently turn a safety cap off. */
|
|
79
|
+
export function resolveMaxConcurrentSubagents(env = process.env) {
|
|
80
|
+
const raw = String(env.TM_MAX_CONCURRENT_SUBAGENTS ?? "").trim();
|
|
81
|
+
if (!raw)
|
|
82
|
+
return 3;
|
|
83
|
+
const n = Number.parseInt(raw, 10);
|
|
84
|
+
if (!Number.isFinite(n) || n < 0)
|
|
85
|
+
return 3;
|
|
86
|
+
return n;
|
|
87
|
+
}
|
|
72
88
|
export const v2Personality = {
|
|
73
|
-
|
|
89
|
+
// #45: the display id (host plugin list) — the npm package name. NOT the
|
|
90
|
+
// storage/audit names (`team-mode/selfcheck`, `team-mode/ledger/`,
|
|
91
|
+
// `team-mode-env-protect`), which stay put so existing data is not orphaned.
|
|
92
|
+
id: "@te-river/opencode-team-mode",
|
|
74
93
|
async setup(ctx) {
|
|
75
94
|
if (!ctx || typeof ctx.tool?.transform !== "function") {
|
|
76
95
|
// Nothing can be registered without the tool domain. Say it in the
|
|
@@ -80,6 +99,23 @@ export const v2Personality = {
|
|
|
80
99
|
return;
|
|
81
100
|
}
|
|
82
101
|
const directory = directoryOf(ctx);
|
|
102
|
+
// ---------- #38 runtime fallback: is the configured default role on disk? ----------
|
|
103
|
+
// The plugin cannot see config-directory roles through ctx.agent (probed: the
|
|
104
|
+
// editor snapshot holds only the 7 built-ins), so a `default_agent` naming a
|
|
105
|
+
// missing role would make the host fall back to `build` SILENTLY. Reading the
|
|
106
|
+
// global config from disk is the only way to say so. Never throws, never
|
|
107
|
+
// affects startup, and prints only the role name + the file path it looked for.
|
|
108
|
+
let defaultAgentCheck = { state: "no-config" };
|
|
109
|
+
try {
|
|
110
|
+
defaultAgentCheck = checkDefaultAgentRole({ configDir: globalConfigDir() });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
defaultAgentCheck = { state: "unreadable" };
|
|
114
|
+
}
|
|
115
|
+
if (defaultAgentCheck.state === "missing-role") {
|
|
116
|
+
console.warn(`[team-mode] 全局配置的 default_agent="${defaultAgentCheck.defaultAgent}" 指向的角色文件不存在(${defaultAgentCheck.roleFile}):` +
|
|
117
|
+
"宿主会静默退回 build,会话不再是 Team。重跑 scripts/install.ps1(Windows)或 scripts/install.sh 修复。");
|
|
118
|
+
}
|
|
83
119
|
// ---------- Team-scope isolation (#22, the user's requirement 2026-09-25) ----------
|
|
84
120
|
// Every v2 hook fires for EVERY agent on this host. This one object is what
|
|
85
121
|
// each layer asks before it mutates a request, a result, or a permission —
|
|
@@ -87,14 +123,24 @@ export const v2Personality = {
|
|
|
87
123
|
const scope = createTeamScope(Object.keys(agents));
|
|
88
124
|
setAskUnavailableNote(V2_NO_DIALOG_NOTE);
|
|
89
125
|
const options = (ctx.options ?? {});
|
|
90
|
-
|
|
126
|
+
// R6 is ARMED BY DEFAULT, matching v1 (which was always armed). The old form
|
|
127
|
+
// (`options.envProtect ? … : "off"`) made the env-file red line opt-IN, and
|
|
128
|
+
// `docs/installation-v2.md` never mentioned the option — so a default install
|
|
129
|
+
// read `.env` in plaintext while AGENTS.md called that rule "hard". Two
|
|
130
|
+
// explicit switches turn it off, and only those two: the plugin option
|
|
131
|
+
// `envProtect:false`, or `TM_ENV_PROTECT=off` (resolved below, which also
|
|
132
|
+
// fails CLOSED into "strict" for a typo). On v2 the shell face is `ask`
|
|
133
|
+
// (the host's own dialog) and the env-FILE face is `deny`; the resolved mode
|
|
134
|
+
// rides the boot row's `env_protect` field so the user can see which world
|
|
135
|
+
// they are in.
|
|
136
|
+
const envProtectMode = options.envProtect === false ? "off" : resolveEnvProtectMode(process.env.TM_ENV_PROTECT);
|
|
91
137
|
const envProtectExtra = parseExtraDeny(process.env.TM_ENV_PROTECT_EXTRA_DENY);
|
|
92
138
|
// ---------- the v2 network policy: no domain gate, IP red line only ----------
|
|
93
|
-
// The user's standing instruction
|
|
94
|
-
// access at all EXCEPT sensitive and internal addresses.
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
139
|
+
// The user's standing instruction (2026-09-25): do not block network
|
|
140
|
+
// access at all EXCEPT sensitive and internal addresses. A plugin cannot
|
|
141
|
+
// raise the host's official per-request dialog, so an allowlist would be a
|
|
142
|
+
// set of pages the agent can never see and no one can approve -- a gate with
|
|
143
|
+
// no door. `"*"` therefore
|
|
98
144
|
// replaces the DEFAULT here, and what still holds absolutely is `checkWebUrl`'s
|
|
99
145
|
// address policy underneath it: link-local / metadata / reserved ranges are a hard
|
|
100
146
|
// deny no config can open (and were never consentable), and private space
|
|
@@ -102,9 +148,9 @@ export const v2Personality = {
|
|
|
102
148
|
// tools, which have no dialog to ask with, and opened only by the host's own
|
|
103
149
|
// `effect:"ask"` for the native tools. IPv4-mapped and DNS64 spellings are
|
|
104
150
|
// unwrapped before that check, so the notation is not a way around it.
|
|
105
|
-
// An explicit TM_WEBFETCH_ALLOWED_DOMAINS always wins; this only changes
|
|
106
|
-
// default applies, and it is resolved from a COPY of the env so the
|
|
107
|
-
//
|
|
151
|
+
// An explicit TM_WEBFETCH_ALLOWED_DOMAINS always wins; this only changes
|
|
152
|
+
// which default applies, and it is resolved from a COPY of the env so the
|
|
153
|
+
// process env itself is untouched.
|
|
108
154
|
const v2Env = { ...process.env };
|
|
109
155
|
if (!String(v2Env.TM_WEBFETCH_ALLOWED_DOMAINS ?? "").trim())
|
|
110
156
|
v2Env.TM_WEBFETCH_ALLOWED_DOMAINS = "*";
|
|
@@ -129,9 +175,9 @@ export const v2Personality = {
|
|
|
129
175
|
// other half of that sentence is that a direct tool definition rides EVERY
|
|
130
176
|
// request (our own estimate for the full set is 9 528 tokens).
|
|
131
177
|
// Delivered as REAL tools by default: the user's call (2026-09-25) was "slim the
|
|
132
|
-
// description first, then go direct", and the
|
|
133
|
-
//
|
|
134
|
-
//
|
|
178
|
+
// description first, then go direct", and the descriptions are now slim enough
|
|
179
|
+
// that the measured per-role cost is 2 660 build-class / 4 726 tester /
|
|
180
|
+
// 6 391 researcher / 7 793 lead.
|
|
135
181
|
// `TM_V2_CODEMODE=off` restores catalog-only for anyone who wants the tokens back
|
|
136
182
|
// — and the boot note says which world this is, because in catalog mode most of
|
|
137
183
|
// our governance text never reaches the model at all.
|
|
@@ -145,10 +191,10 @@ export const v2Personality = {
|
|
|
145
191
|
// and the shutdown line reports both halves: `tools_codemode` (sent) and
|
|
146
192
|
// `tools_in_request` (what the assembled request actually carried).
|
|
147
193
|
const v2CodeModeDirect = /^(1|true|yes|on|direct)$/i.test(String(v2Env.TM_V2_CODEMODE ?? "").trim());
|
|
148
|
-
// No SDK client
|
|
149
|
-
// `session.*`
|
|
150
|
-
//
|
|
151
|
-
//
|
|
194
|
+
// No SDK client. The v2 plugin ctx carries no `file.read` / `find.text` /
|
|
195
|
+
// `session.*` / `pty.*` object, and the fs shim that used to stand in for the
|
|
196
|
+
// first two went away with `tm_read`/`tm_grep` (the native tools, governed at
|
|
197
|
+
// `execute.after`, are the file ladder now).
|
|
152
198
|
// What IS bridged is the one thing the collect path cannot be honest without:
|
|
153
199
|
// `tm_join` must be able to check whether a named session id is really the
|
|
154
200
|
// caller's child, and a live 2.0.16 run proved that without it the tool reports
|
|
@@ -180,13 +226,12 @@ export const v2Personality = {
|
|
|
180
226
|
const notes = [];
|
|
181
227
|
// ---------- register the governed tools ----------
|
|
182
228
|
const entries = Object.entries(tmRuntime.tools);
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
229
|
+
// The retired tools are not registered. The host's own `execute` (Code
|
|
230
|
+
// Mode) already runs "one program, N governed calls, zero round-trips, only
|
|
231
|
+
// the aggregate entering the context", and a live session showed our
|
|
186
232
|
// governed results still come back offloaded through it — so shipping a
|
|
187
233
|
// second batch runner would hand the model two tools for one job. The
|
|
188
|
-
//
|
|
189
|
-
// the prompts the v2 config files carry are forked by gen-v2-config.mjs.
|
|
234
|
+
// prompts the config files carry are forked by gen-v2-config.mjs.
|
|
190
235
|
const V2_UNREGISTERED = V1_ONLY_TOOLS;
|
|
191
236
|
const bindings = [];
|
|
192
237
|
const derived = [];
|
|
@@ -212,10 +257,10 @@ export const v2Personality = {
|
|
|
212
257
|
// shape, so name the tools and say how they were obtained.
|
|
213
258
|
const retired = entries.map(([name]) => name).filter((n) => V2_UNREGISTERED.has(n));
|
|
214
259
|
if (retired.length) {
|
|
215
|
-
// Deliberate absences are said, not silently missing: an agent that
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
notes.push(
|
|
260
|
+
// Deliberate absences are said, not silently missing: an agent that looks
|
|
261
|
+
// for `tm_read` needs to learn from the boot log that this is the design,
|
|
262
|
+
// not a failed install.
|
|
263
|
+
notes.push(`不注册 ${retired.join("/")} —— 同一件工作交给宿主自己的 read/grep/shell/execute(结果照样被 execute.after 治理)`);
|
|
219
264
|
}
|
|
220
265
|
if (derived.length) {
|
|
221
266
|
notes.push(`${derived.join("/")} 没有 zod 形状,参数表是从描述符文本推出来的(类型靠 name: type 前缀判定,不是 zod 保证)`);
|
|
@@ -250,14 +295,72 @@ export const v2Personality = {
|
|
|
250
295
|
// ---------- the permission guard (egress red line + R6 per command) ----------
|
|
251
296
|
// Installed BEFORE the agent transform because whether the config still needs
|
|
252
297
|
// the coarse `shell -> ask` escalation depends on this hook being there.
|
|
253
|
-
const guards = await applyV2PermissionGuards(ctx, {
|
|
298
|
+
const guards = await applyV2PermissionGuards(ctx, {
|
|
299
|
+
envProtectMode,
|
|
300
|
+
scope,
|
|
301
|
+
// #49 feature 2: the concurrency cap reads the plugin's ONE children
|
|
302
|
+
// registry (the same rows tm_join collects), so a session cannot exceed the
|
|
303
|
+
// cap by dispatching faster than the lead collects. `0` disables it.
|
|
304
|
+
runningChildren: (caller) => tmRuntime.runningChildren(caller),
|
|
305
|
+
maxConcurrent: resolveMaxConcurrentSubagents(v2Env),
|
|
306
|
+
onConcurrencyDenied: (info) => {
|
|
307
|
+
try {
|
|
308
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
309
|
+
tool: "host",
|
|
310
|
+
step_id: "v2-concurrency",
|
|
311
|
+
event: "denied",
|
|
312
|
+
caller: info.caller,
|
|
313
|
+
cap: info.cap,
|
|
314
|
+
running: info.running.length,
|
|
315
|
+
ids: info.running.map((c) => c.sessionID).join(","),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
});
|
|
254
323
|
// Said out loud because it is a promise with a boundary: the user's rule is
|
|
255
324
|
// that nothing outside Team may be touched, and the honest consequence is that
|
|
256
325
|
// the red lines we inject are therefore NOT protecting a build session either.
|
|
257
326
|
notes.push("已按 Team 作用域收口:工具面裁剪、温度、黑板提示、原生结果卸载、R6 与地址红线、subagent 强制后台,都只对我们六个角色的会话生效;build / plan 等其余模式保持宿主出厂行为(代价:那些会话也不由我们补红线,需要全局红线请让宿主自己的 permission 规则承担)");
|
|
258
327
|
registrations.push(...guards.registrations);
|
|
328
|
+
// #58: the SAME R6 file-path face, on the seam that sees the whole input.
|
|
329
|
+
// `permission.evaluate`'s resource for `grep`/`glob` is the PATTERN, not the
|
|
330
|
+
// search path, so the red line had a hole the width of those two tools —
|
|
331
|
+
// `read .env` was denied while `grep SECRET in .env` succeeded (2.0.24).
|
|
332
|
+
const envFileInput = await applyV2EnvFileInputGuard(ctx, { envProtectMode, scope, report: guards.report });
|
|
333
|
+
registrations.push(...envFileInput.registrations);
|
|
259
334
|
const bgForce = await applyV2BackgroundForce(ctx, { scope });
|
|
260
335
|
registrations.push(...bgForce.registrations);
|
|
336
|
+
// The shell timeout clamp (issue #6), ported here when the v1 personality was
|
|
337
|
+
// cut: the host gives `execute.before` a mutable `input`, the same seam
|
|
338
|
+
// `applyV2BackgroundForce` writes, so the protection survives the deletion —
|
|
339
|
+
// but only if it is counted, because a clamp nobody can see is a clamp a user
|
|
340
|
+
// reads as "the tool killed my probe for no reason".
|
|
341
|
+
const shellTimeout = await applyV2ShellTimeoutClamp(ctx, {
|
|
342
|
+
probeMs: tmRuntime.config.bashTimeoutProbeMs,
|
|
343
|
+
maxMs: tmRuntime.config.bashTimeoutMaxMs,
|
|
344
|
+
readonlyAllowed: tmRuntime.config.bashReadonlyAllowed,
|
|
345
|
+
scope,
|
|
346
|
+
onClamp: (info) => {
|
|
347
|
+
try {
|
|
348
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
349
|
+
tool: "shell",
|
|
350
|
+
step_id: "timeout-clamp",
|
|
351
|
+
event: "clamped",
|
|
352
|
+
via: info.via,
|
|
353
|
+
from_ms: info.from,
|
|
354
|
+
to_ms: info.to,
|
|
355
|
+
sessionID: info.sessionID ?? "",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
registrations.push(...shellTimeout.registrations);
|
|
261
364
|
// JIT governance over the HOST's tools, so the offload promise does not depend
|
|
262
365
|
// on which tool the model happened to pick (see v2-offload.ts for why a
|
|
263
366
|
// per-tool promise is no promise). Registered BEFORE the probe so the probe's
|
|
@@ -265,6 +368,29 @@ export const v2Personality = {
|
|
|
265
368
|
// teardown is a counter that does not exist.
|
|
266
369
|
const offload = await applyV2NativeOffload(ctx, { pipelines: tmRuntime.pipelines, scope });
|
|
267
370
|
registrations.push(...(await Promise.all(offload.registrations)));
|
|
371
|
+
// #35: the Code Mode adoption hint. Registered AFTER the offloader so its
|
|
372
|
+
// append lands on whatever text the offloader left (a rewritten body or the
|
|
373
|
+
// host's verbatim one) — the hint only ever ADDS a line, never rewrites.
|
|
374
|
+
const probeChain = applyV2ProbeChain(ctx, {
|
|
375
|
+
scope,
|
|
376
|
+
env: v2Env,
|
|
377
|
+
onAdvise: (info) => {
|
|
378
|
+
try {
|
|
379
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
380
|
+
tool: "host",
|
|
381
|
+
step_id: "probe-chain",
|
|
382
|
+
event: "advised",
|
|
383
|
+
native_tool: info.tool,
|
|
384
|
+
count: info.count,
|
|
385
|
+
sessionID: info.sessionID,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
registrations.push(...(await Promise.all(probeChain.registrations)));
|
|
268
394
|
// The host's own sub-agents, entered into tm_join's registry (decision 4,
|
|
269
395
|
// 2026-09-26). On v2 every dispatch IS the host's `subagent` tool, so without this
|
|
270
396
|
// the collect side had nothing to collect: tm_join answered 没有待收集的派发 about
|
|
@@ -302,6 +428,26 @@ export const v2Personality = {
|
|
|
302
428
|
allowlist: seedWebfetchDomains(tmRuntime.config.webfetchAllowedDomains),
|
|
303
429
|
scope,
|
|
304
430
|
env: v2Env,
|
|
431
|
+
// #14: the SERP rate limit's trajectory line. The gate has no store, so the
|
|
432
|
+
// sink is wired here — same shape as the shell-timeout clamp's `onClamp`.
|
|
433
|
+
onSerp: (info) => {
|
|
434
|
+
try {
|
|
435
|
+
tmRuntime.pipelines.store.appendTrajectory({
|
|
436
|
+
tool: "browser",
|
|
437
|
+
step_id: "serp-loop",
|
|
438
|
+
event: info.event,
|
|
439
|
+
engine: info.engine,
|
|
440
|
+
query: info.query,
|
|
441
|
+
host: info.host,
|
|
442
|
+
count: info.count,
|
|
443
|
+
limit: info.limit,
|
|
444
|
+
sessionID: info.sessionID,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
449
|
+
}
|
|
450
|
+
},
|
|
305
451
|
});
|
|
306
452
|
if (!browserGate.registrations.length) {
|
|
307
453
|
notes.push("原生 browser_* 的门禁没装上(ctx.tool.hook 不可用):宿主的 45 个浏览器工具在 Team 会话里也不受我们的域名 / 地址规则约束");
|
|
@@ -320,6 +466,12 @@ export const v2Personality = {
|
|
|
320
466
|
/** The role ids the HOST resolved in a real request — the only observation that
|
|
321
467
|
* separates "installed but invisible to the snapshot" from "not installed". */
|
|
322
468
|
let resolvedAgentIds = () => [];
|
|
469
|
+
/** #58: `tools_in_request` used to live ONLY on `v2-shutdown`, which the CLI
|
|
470
|
+
* never writes — so on a CLI run the field was unobservable. Late-bound for
|
|
471
|
+
* the same reason as the counters: the request layer registers BELOW the
|
|
472
|
+
* probe, and a closure that touched a not-yet-initialized `const` would throw
|
|
473
|
+
* inside `onSummary`, where the try/catch would swallow the whole row. */
|
|
474
|
+
let tmInRequestSurface = () => "unobserved";
|
|
323
475
|
/** A3: the in-process observation counters. Measured today: three runs wrote
|
|
324
476
|
* ZERO `v2-shutdown` rows, because the CLI path never reaches dispose — so the
|
|
325
477
|
* final totals have to survive on the throttled surface snapshot too, and a row
|
|
@@ -369,6 +521,9 @@ export const v2Personality = {
|
|
|
369
521
|
// teardown — dispose is not guaranteed to run, and a run that is
|
|
370
522
|
// interrupted is exactly the run whose counters someone needs.
|
|
371
523
|
...observationCounters("surface"),
|
|
524
|
+
// #58: the delivery fact the CLI could never see — it only rode
|
|
525
|
+
// `v2-shutdown`, which a CLI run never reaches.
|
|
526
|
+
tools_in_request: tmInRequestSurface(),
|
|
372
527
|
...summary,
|
|
373
528
|
});
|
|
374
529
|
}
|
|
@@ -404,7 +559,7 @@ export const v2Personality = {
|
|
|
404
559
|
// `TM_R6_FINE_ASK=off`, or no hook installed to hand the decision to.
|
|
405
560
|
const escalateShellAsk = envProtectMode !== "off" && needsCoarseShellAsk(process.env, guards.installed);
|
|
406
561
|
// Team is ALWAYS the default (the user's standing instruction — see the
|
|
407
|
-
// header for why
|
|
562
|
+
// header for why there is no conservative form). `default()` on
|
|
408
563
|
// an agent that does not exist would just make the host fall back to build
|
|
409
564
|
// silently, so it is gated on the role actually being present, and the
|
|
410
565
|
// refusal to promote is reported rather than hidden.
|
|
@@ -423,16 +578,16 @@ export const v2Personality = {
|
|
|
423
578
|
const { triples, unmapped } = triplesFromAgentPermission(agents[id]?.permission, { escalateShellAsk, agentName: id });
|
|
424
579
|
for (const u of unmapped)
|
|
425
580
|
unmappedActions.add(u);
|
|
426
|
-
// `V1_ONLY_TOOLS` is passed as the reclaim set: a triple naming
|
|
427
|
-
// action
|
|
428
|
-
//
|
|
429
|
-
//
|
|
581
|
+
// `V1_ONLY_TOOLS` is passed as the reclaim set: a triple naming a
|
|
582
|
+
// retired action can only have come from an earlier boot, and a
|
|
583
|
+
// permission rule for a tool the host has never heard of reads as a
|
|
584
|
+
// capability the user granted.
|
|
430
585
|
const merged = mergeTriples(existing.permissions, triples, V1_ONLY_TOOLS);
|
|
431
586
|
if (merged.changed)
|
|
432
587
|
editor.update(id, (a) => { a.permissions = merged.triples; });
|
|
433
588
|
}
|
|
434
589
|
if (promoteTeamDefault) {
|
|
435
|
-
// `editor.get("
|
|
590
|
+
// `editor.get("Team")` returning nothing is NOT evidence the host does
|
|
436
591
|
// not know the role — the live run is literally executing as `team` while
|
|
437
592
|
// this snapshot lists only the 7 built-ins, so the transform receives the
|
|
438
593
|
// agent set from BEFORE the config directory merged. Gating the
|
|
@@ -441,13 +596,13 @@ export const v2Personality = {
|
|
|
441
596
|
// the AFTER view is what gets recorded.
|
|
442
597
|
let ok = true;
|
|
443
598
|
try {
|
|
444
|
-
editor.default("
|
|
599
|
+
editor.default("Team");
|
|
445
600
|
}
|
|
446
601
|
catch {
|
|
447
602
|
ok = false;
|
|
448
603
|
}
|
|
449
604
|
defaultPromoted = ok;
|
|
450
|
-
defaultPostCheck = editor.get?.("
|
|
605
|
+
defaultPostCheck = editor.get?.("Team") ? "present-after" : "absent-after";
|
|
451
606
|
}
|
|
452
607
|
// Recorded HERE, at the moment of observation. The host defers this
|
|
453
608
|
// callback (a live boot reached the trajectory line with
|
|
@@ -486,11 +641,14 @@ export const v2Personality = {
|
|
|
486
641
|
return -1;
|
|
487
642
|
}
|
|
488
643
|
})(),
|
|
489
|
-
|
|
644
|
+
// The ids the transform callback can SEE — which is the set from BEFORE the config
|
|
645
|
+
// directory merges, so it never contains our six roles. Named for what it measures:
|
|
646
|
+
// the old name implied "the editor's ids" and the filter made it look truncated
|
|
647
|
+
// (2 of 7) when the truth is that the transform runs too early to see them.
|
|
648
|
+
agents_editor_ids_at_transform: (() => {
|
|
490
649
|
try {
|
|
491
650
|
return (editor.list?.() ?? [])
|
|
492
651
|
.map((a) => String(a?.id ?? "?"))
|
|
493
|
-
.filter((n) => wantedIds.includes(n) || n === "build" || n === "plan")
|
|
494
652
|
.join(" ");
|
|
495
653
|
}
|
|
496
654
|
catch {
|
|
@@ -508,7 +666,7 @@ export const v2Personality = {
|
|
|
508
666
|
notes.push("ctx.agent.transform 不存在,角色权限矩阵未规范化");
|
|
509
667
|
}
|
|
510
668
|
if (defaultPromoted === false) {
|
|
511
|
-
notes.push("Team 没有成为默认:editor.default(\"
|
|
669
|
+
notes.push("Team 没有成为默认:editor.default(\"Team\") 抛错了");
|
|
512
670
|
}
|
|
513
671
|
else if (defaultPromoted && defaultPostCheck !== "present-after") {
|
|
514
672
|
// Measured on a live 2.0.16 standalone boot: the call is accepted, `team` is
|
|
@@ -531,14 +689,13 @@ export const v2Personality = {
|
|
|
531
689
|
// ---------- the request layer ----------
|
|
532
690
|
// The whitelist was only ever a DENY, which stops a call but leaves the
|
|
533
691
|
// tool's description in every request. Here it becomes a request-level
|
|
534
|
-
// removal, and the
|
|
535
|
-
// board root) or into the agent config (temperature) ride the request
|
|
692
|
+
// removal, and the resolved board root and the temperature ride the request
|
|
536
693
|
// instead — a v2 agent is a config FILE and cannot carry a per-workspace
|
|
537
694
|
// path, and `temperature` is a legacy agent field the runner no longer
|
|
538
|
-
// sends. This also starts the blackboard TTL sweeper, which
|
|
539
|
-
// otherwise silently lacked: the note promises a sweep, so
|
|
540
|
-
// without the sweeper would be the exact overstated claim this
|
|
541
|
-
// built to refuse.
|
|
695
|
+
// sends. This also starts the blackboard TTL sweeper, which this
|
|
696
|
+
// personality otherwise silently lacked: the note promises a sweep, so
|
|
697
|
+
// shipping it without the sweeper would be the exact overstated claim this
|
|
698
|
+
// product is built to refuse.
|
|
542
699
|
const ttlMs = resolveTtlMs(options);
|
|
543
700
|
const ttlDays = Math.round(ttlMs / (24 * 60 * 60 * 1000)) || DEFAULT_TTL_DAYS;
|
|
544
701
|
const note = blackboardNote(startBlackboardMaintenance(directory, ttlMs), ttlDays);
|
|
@@ -573,7 +730,89 @@ export const v2Personality = {
|
|
|
573
730
|
}
|
|
574
731
|
}
|
|
575
732
|
}
|
|
576
|
-
const session = await applyV2SessionLayer(ctx, {
|
|
733
|
+
const session = await applyV2SessionLayer(ctx, {
|
|
734
|
+
temperature,
|
|
735
|
+
note,
|
|
736
|
+
noteAgents: ["Team"],
|
|
737
|
+
plan,
|
|
738
|
+
scope,
|
|
739
|
+
// #49 feature 4: 拆分任务. The layer owns no store, so the trajectory sink
|
|
740
|
+
// is wired here — same shape as the retry governor's `onEvent`.
|
|
741
|
+
split: resolveSplitConfig(v2Env),
|
|
742
|
+
onSplit: (row) => {
|
|
743
|
+
try {
|
|
744
|
+
tmRuntime.pipelines.store.appendTrajectory({ tool: "host", step_id: "v2-split", api: 2, ...row });
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
// #58: point the late-bound reader at the real observation now that the
|
|
752
|
+
// request layer exists, so `v2-surface` carries `tools_in_request` too.
|
|
753
|
+
tmInRequestSurface = () => (session.report.tmInRequestSurface ? "tm-in-request" : "catalog-only");
|
|
754
|
+
// #49 feature 3: 错峰重试. We have no seam that intercepts the model's own
|
|
755
|
+
// retry, so this layer only (1) recognises a provider throttle from the event
|
|
756
|
+
// feed, (2) injects an exact wait into the next request, and (3) denies new
|
|
757
|
+
// dispatches during the breaker's cooldown. Registered AFTER the permission
|
|
758
|
+
// guards so its `evaluate` hook runs after the concurrency cap — both only
|
|
759
|
+
// ever deny, so whichever fires first keeps its message and the stricter
|
|
760
|
+
// effect (deny) stands either way.
|
|
761
|
+
const retryConfig = resolveRetryConfig(v2Env);
|
|
762
|
+
// The layer owns no store, so the trajectory sink is wired here — same shape
|
|
763
|
+
// as the shell-timeout clamp's `onClamp` and the SERP guard's `onSerp`.
|
|
764
|
+
const retrySink = (row) => {
|
|
765
|
+
try {
|
|
766
|
+
tmRuntime.pipelines.store.appendTrajectory({ tool: "host", step_id: "v2-retry", api: 2, ...row });
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
/* the trajectory is an extra, never a reason to fail the call */
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const retryGovernor = createRetryGovernor({ ...retryConfig, onEvent: retrySink });
|
|
773
|
+
const retry = retryConfig.enabled
|
|
774
|
+
? await applyV2RetryGovernor(ctx, { scope, governor: retryGovernor, onEvent: retrySink })
|
|
775
|
+
: { registrations: [], report: retryGovernor.report };
|
|
776
|
+
registrations.push(...retry.registrations);
|
|
777
|
+
// Team's own early-compaction trigger. The host exposes no percentage knob (its
|
|
778
|
+
// `compaction` block is auto/prune/tail_turns/preserve_recent_tokens/reserved) and a
|
|
779
|
+
// 2.x plugin has no config domain at all, so "compact at 75% of the window" can only
|
|
780
|
+
// live here, in the package, enforced through `ctx.session.compact`.
|
|
781
|
+
const compactConfig = resolveCompactConfig(process.env);
|
|
782
|
+
const compaction = await applyV2EarlyCompaction(ctx, {
|
|
783
|
+
config: compactConfig,
|
|
784
|
+
scope,
|
|
785
|
+
// Every outcome gets its OWN trajectory line: the counters ride rows a CLI run
|
|
786
|
+
// never produces (`v2-shutdown` needs dispose, `v2-surface` is throttled), so
|
|
787
|
+
// without this the feature would be invisible in exactly the runs used to check it.
|
|
788
|
+
onEvent: (row) => {
|
|
789
|
+
try {
|
|
790
|
+
tmRuntime.pipelines.store.appendTrajectory({ tool: "host", step_id: "v2-compact", event: "compaction", api: 2, ...row });
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
/* a diagnostic that cannot be written never breaks the request it describes */
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
// Context Pruning (plan 功能 1, #49): the request layer trims the tool
|
|
798
|
+
// surface, the compaction layer decides WHEN to summarize, and this layer
|
|
799
|
+
// shrinks the HISTORY itself — settled messages become one-line pointers
|
|
800
|
+
// (handle / child id / re-run hint) once the session crosses a fraction of
|
|
801
|
+
// its window. Registered AFTER the session layer so its `context` hook runs
|
|
802
|
+
// on the same assembled request, and it never touches a message outside its
|
|
803
|
+
// plan (the newest message and every hard-protected one stay byte-exact).
|
|
804
|
+
const prune = await applyV2ContextPrune(ctx, {
|
|
805
|
+
config: resolvePruneConfig(process.env),
|
|
806
|
+
scope,
|
|
807
|
+
onEvent: (row) => {
|
|
808
|
+
try {
|
|
809
|
+
tmRuntime.pipelines.store.appendTrajectory({ tool: "host", step_id: "v2-prune", event: "prune", api: 2, ...row });
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
/* a diagnostic that cannot be written never breaks the request it describes */
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
});
|
|
577
816
|
// Everything the matrix reports now exists, so the late-bound closure can be
|
|
578
817
|
// pointed at the real observations.
|
|
579
818
|
v2Matrix = () => v2CapabilityRows({
|
|
@@ -584,6 +823,23 @@ export const v2Personality = {
|
|
|
584
823
|
offload,
|
|
585
824
|
sessionHooks: 2,
|
|
586
825
|
temperature,
|
|
826
|
+
// The prune layer's counters, handed to the matrix so its row can earn `ok` on a
|
|
827
|
+
// process that actually pruned something. Without this the row could only ever read
|
|
828
|
+
// `declared`, which is the "wired but never observed" shape the matrix exists to
|
|
829
|
+
// distinguish from "it ran".
|
|
830
|
+
prune: {
|
|
831
|
+
enabled: prune.report.enabled,
|
|
832
|
+
atPercent: prune.report.atPercent,
|
|
833
|
+
keepTailPercent: prune.report.keepTailPercent,
|
|
834
|
+
checked: prune.report.checked,
|
|
835
|
+
prunedMessages: prune.report.prunedMessages,
|
|
836
|
+
prunedTokens: prune.report.prunedTokens,
|
|
837
|
+
below: prune.report.below,
|
|
838
|
+
noLimit: prune.report.noLimit,
|
|
839
|
+
foreignSkipped: prune.report.foreignSkipped,
|
|
840
|
+
threw: prune.report.threw,
|
|
841
|
+
lastPercent: prune.report.lastPercent,
|
|
842
|
+
},
|
|
587
843
|
// Both derived, not asserted: "we think v2 has no X" has to come from the
|
|
588
844
|
// object we were handed, so a host that grows the seam shows up here
|
|
589
845
|
// automatically instead of leaving a stale "missing" row forever.
|
|
@@ -600,22 +856,48 @@ export const v2Personality = {
|
|
|
600
856
|
unknown: sessionReader.report.interruptUnknown,
|
|
601
857
|
error: sessionReader.report.interruptError,
|
|
602
858
|
},
|
|
859
|
+
// #39: the early-compaction seam's counters, read off the layer that calls it.
|
|
860
|
+
compact: {
|
|
861
|
+
enabled: compaction.report.enabled,
|
|
862
|
+
percent: compaction.report.percent,
|
|
863
|
+
checked: compaction.report.checked,
|
|
864
|
+
fired: compaction.report.fired,
|
|
865
|
+
confirmed: compaction.report.confirmed,
|
|
866
|
+
conflicts: compaction.report.conflicts,
|
|
867
|
+
threw: compaction.report.threw,
|
|
868
|
+
noLimit: compaction.report.noLimit,
|
|
869
|
+
source: compaction.report.lastSource,
|
|
870
|
+
lastPercent: compaction.report.lastPercent,
|
|
871
|
+
error: compaction.report.lastError,
|
|
872
|
+
wired: compaction.registrations.length > 0,
|
|
873
|
+
},
|
|
603
874
|
hasTodoSeam: typeof ctx.session?.todo === "function",
|
|
604
875
|
hasAsk: typeof ctx.tool === "function",
|
|
605
876
|
storageState: storageProbe.state,
|
|
606
877
|
scope,
|
|
607
878
|
});
|
|
608
879
|
registrations.push(...session.registrations);
|
|
880
|
+
registrations.push(...prune.registrations);
|
|
609
881
|
if (!session.registrations.length) {
|
|
610
882
|
notes.push("ctx.session.hook 不存在:工具面裁剪、温度、黑板根目录三项请求层治理都没装上");
|
|
611
883
|
}
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
884
|
+
// This personality had no event subscription at all (#8), so a child that
|
|
885
|
+
// settled two seconds after the dispatch stayed "running" for the whole wait
|
|
886
|
+
// budget and `tm_join` reported a state it had never observed — goal #6's
|
|
887
|
+
// failure shape, and a bug rather than a limitation now that
|
|
888
|
+
// `ctx.event.subscribe()` is measured to work.
|
|
889
|
+
const feed = await applyV2EventFeed(ctx, {
|
|
890
|
+
onEvent: (ev) => tmRuntime.observeDispatchEvent(ev),
|
|
891
|
+
// #39: the host publishes usage ONLY on the event feed (measured: the context hook's
|
|
892
|
+
// messages carry no tokens, while session.usage.updated fires hundreds of times), so
|
|
893
|
+
// the early-compaction layer is fed from here rather than from a second subscription.
|
|
894
|
+
onUsage: (data) => compaction.observeUsage(data),
|
|
895
|
+
// #49 feature 3: the retry governor's error tap. `session.error` is the only
|
|
896
|
+
// place a provider throttle is published; the governor classifies its text and
|
|
897
|
+
// arms the next-request directive / the cooldown. A tap that throws never
|
|
898
|
+
// closes the feed (the governor itself never throws).
|
|
899
|
+
onError: (data) => retryGovernor.observeEvent(data),
|
|
900
|
+
});
|
|
619
901
|
if (!feed.report.active) {
|
|
620
902
|
notes.push(`事件流没接通(${feed.report.stopped ?? "原因未知"}):tm_join 的结算检测只剩等待预算内的轮询,子代理结算了也要等到超时才报告`);
|
|
621
903
|
}
|
|
@@ -627,7 +909,7 @@ export const v2Personality = {
|
|
|
627
909
|
const ids = new Set(probe.report.agentsSeen);
|
|
628
910
|
for (const id of Object.keys(session.report.removed))
|
|
629
911
|
ids.add(id);
|
|
630
|
-
return [...ids].filter((id) => wantedIds.
|
|
912
|
+
return [...ids].filter((id) => wantedIds.some((w) => normalizeAgentName(w) === normalizeAgentName(id))).sort();
|
|
631
913
|
};
|
|
632
914
|
observationCounters = (source) => ({
|
|
633
915
|
counters_at: source,
|
|
@@ -636,12 +918,76 @@ export const v2Personality = {
|
|
|
636
918
|
guard_shell_matched: guards.report.shellMatched,
|
|
637
919
|
guard_strictened: guards.report.strictened,
|
|
638
920
|
guard_denied: guards.report.denied,
|
|
921
|
+
guard_envfile_denied: guards.report.envFileDenied,
|
|
922
|
+
guard_envfile_denied_by_input: guards.report.envFileDeniedByInput,
|
|
923
|
+
guard_envfile_input_seen: guards.report.envFileInputSeen,
|
|
924
|
+
guard_envfile_input_threw: guards.report.envFileInputThrew,
|
|
925
|
+
guard_envfile_input_foreign_skipped: guards.report.envFileInputForeignSkipped,
|
|
639
926
|
guard_foreign_skipped: guards.report.foreignSkipped,
|
|
927
|
+
// #49 feature 2: the concurrency cap — how many `subagent` evaluations it
|
|
928
|
+
// looked at, how many it denied, and the peak running count it saw.
|
|
929
|
+
concurrency_seen: guards.report.concurrencySeen,
|
|
930
|
+
concurrency_denied: guards.report.concurrencyDenied,
|
|
931
|
+
concurrency_running_max: guards.report.concurrencyRunningMax,
|
|
932
|
+
concurrency_threw: guards.report.concurrencyThrew,
|
|
640
933
|
subagent_seen: bgForce.report.seen,
|
|
641
934
|
subagent_forced: bgForce.report.forced,
|
|
642
|
-
|
|
935
|
+
shell_timeout_seen: shellTimeout.report.seen,
|
|
936
|
+
shell_timeout_clamped: shellTimeout.report.clamped,
|
|
937
|
+
shell_timeout_foreign_skipped: shellTimeout.report.foreignSkipped,
|
|
938
|
+
shell_timeout_threw: shellTimeout.report.threw,
|
|
939
|
+
// (a) The PLAN, not our hook's own cut. `session.report.removed` counts what
|
|
940
|
+
// THIS layer deleted from `event.tools`, and on a live host that is 0 because
|
|
941
|
+
// the permission layer already excluded the denied names before the request
|
|
942
|
+
// reached us — so the old field read as "nothing was trimmed" while the boot
|
|
943
|
+
// row's `request_removed_plan` said `team=7`. The whitelist's effect on the
|
|
944
|
+
// request IS the removal, so this reports the plan (aligned with
|
|
945
|
+
// `request_removed_plan`); the hook's own cut is an implementation detail that
|
|
946
|
+
// is 0 by design, not a fact the user needs a separate field for.
|
|
947
|
+
tools_removed: [...plan.entries()].map(([k, v]) => `${k}=${v.size}`).join(" "),
|
|
643
948
|
note_pushed: session.report.notePushed,
|
|
644
949
|
compaction_lines: session.report.compactionLines,
|
|
950
|
+
// #49 feature 4: 拆分任务 — briefs measured and advice injected.
|
|
951
|
+
split_seen: session.report.splitSeen,
|
|
952
|
+
split_advised: session.report.splitAdvised,
|
|
953
|
+
compact_enabled: compaction.report.enabled,
|
|
954
|
+
compact_at_percent: compaction.report.percent,
|
|
955
|
+
compact_checked: compaction.report.checked,
|
|
956
|
+
compact_usage_seen: compaction.report.usageSeen,
|
|
957
|
+
compact_usage_events: compaction.report.usageEvents,
|
|
958
|
+
compact_event_decided: compaction.report.eventDecided,
|
|
959
|
+
compact_fired: compaction.report.fired,
|
|
960
|
+
compact_confirmed: compaction.report.confirmed,
|
|
961
|
+
compact_source: compaction.report.lastSource,
|
|
962
|
+
compact_last_percent: compaction.report.lastPercent,
|
|
963
|
+
// #49 Context Pruning — the history-shrinking half of the budget work.
|
|
964
|
+
prune_enabled: prune.report.enabled,
|
|
965
|
+
prune_at_percent: prune.report.atPercent,
|
|
966
|
+
prune_keep_tail_percent: prune.report.keepTailPercent,
|
|
967
|
+
prune_checked: prune.report.checked,
|
|
968
|
+
prune_pruned_messages: prune.report.prunedMessages,
|
|
969
|
+
prune_pruned_tokens: prune.report.prunedTokens,
|
|
970
|
+
prune_unknown_parts: prune.report.unknownParts,
|
|
971
|
+
prune_skipped_protected: prune.report.skippedProtected,
|
|
972
|
+
prune_below: prune.report.below,
|
|
973
|
+
prune_no_limit: prune.report.noLimit,
|
|
974
|
+
prune_foreign_skipped: prune.report.foreignSkipped,
|
|
975
|
+
prune_threw: prune.report.threw,
|
|
976
|
+
prune_last_percent: prune.report.lastPercent,
|
|
977
|
+
// #49 feature 3: 错峰重试 — recognition counts, breaker trips, cooldown
|
|
978
|
+
// denials and the last computed wait, so "we backed off" is a number.
|
|
979
|
+
retry_enabled: retry.report.enabled,
|
|
980
|
+
retry_quota_seen: retry.report.quotaSeen,
|
|
981
|
+
retry_rate_seen: retry.report.rateSeen,
|
|
982
|
+
retry_transient_seen: retry.report.transientSeen,
|
|
983
|
+
retry_unknown_seen: retry.report.unknownSeen,
|
|
984
|
+
retry_break_trips: retry.report.breakTrips,
|
|
985
|
+
retry_cooldowns: retry.report.cooldowns,
|
|
986
|
+
retry_last_delay_ms: retry.report.lastDelayMs,
|
|
987
|
+
retry_directives_injected: retry.report.directivesInjected,
|
|
988
|
+
retry_cooldown_denied: retry.report.cooldownDenied,
|
|
989
|
+
retry_foreign_skipped: retry.report.foreignSkipped,
|
|
990
|
+
retry_threw: retry.report.threw,
|
|
645
991
|
scope_ours: scope.report.ours,
|
|
646
992
|
scope_foreign: scope.report.foreign,
|
|
647
993
|
scope_unknown: scope.report.unknown,
|
|
@@ -675,6 +1021,8 @@ export const v2Personality = {
|
|
|
675
1021
|
browser_gate_refused: browserGate.report.refused,
|
|
676
1022
|
browser_gate_leaked: browserGate.report.leaked,
|
|
677
1023
|
browser_gate_held: browserGate.report.held,
|
|
1024
|
+
browser_gate_serp_nav: browserGate.report.serpNav,
|
|
1025
|
+
browser_gate_serp_refused: browserGate.report.serpRefused,
|
|
678
1026
|
native_seen: offload.report.seen,
|
|
679
1027
|
native_ours: offload.report.ours,
|
|
680
1028
|
native_unmatched: offload.report.unmatched,
|
|
@@ -683,6 +1031,13 @@ export const v2Personality = {
|
|
|
683
1031
|
native_tokens_saved: offload.report.tokensSaved,
|
|
684
1032
|
native_capped: offload.report.capped,
|
|
685
1033
|
native_report_capped: offload.report.reportCapped,
|
|
1034
|
+
// #35: the Code Mode adoption hint — how many native file/command calls we
|
|
1035
|
+
// saw, and how many results actually carried the appended line.
|
|
1036
|
+
probe_chain_seen: probeChain.report.seen,
|
|
1037
|
+
probe_chain_advised: probeChain.report.advised,
|
|
1038
|
+
probe_chain_reset: probeChain.report.reset,
|
|
1039
|
+
probe_chain_foreign_skipped: probeChain.report.foreignSkipped,
|
|
1040
|
+
probe_chain_threw: probeChain.report.threw,
|
|
686
1041
|
});
|
|
687
1042
|
const removedPlanSizes = [...plan.entries()].map(([id, set]) => `${id}=${set.size}`).join(" ");
|
|
688
1043
|
try {
|
|
@@ -725,6 +1080,11 @@ export const v2Personality = {
|
|
|
725
1080
|
request_temperature: temperature === false ? "off" : temperature,
|
|
726
1081
|
request_removed_plan: removedPlanSizes,
|
|
727
1082
|
board_ttl_days: ttlDays,
|
|
1083
|
+
// #38: the runtime fallback's verdict — "ok" / "missing-role" / "no-default"
|
|
1084
|
+
// / "no-config" / "unreadable". `default_agent_missing` names the role only
|
|
1085
|
+
// when one is actually missing, so a clean boot prints an empty string.
|
|
1086
|
+
default_agent_check: defaultAgentCheck.state,
|
|
1087
|
+
default_agent_missing: defaultAgentCheck.state === "missing-role" ? defaultAgentCheck.defaultAgent ?? "" : "",
|
|
728
1088
|
env_protect: envProtectMode,
|
|
729
1089
|
note: notes.join(" · "),
|
|
730
1090
|
});
|
|
@@ -732,9 +1092,9 @@ export const v2Personality = {
|
|
|
732
1092
|
catch {
|
|
733
1093
|
/* the trajectory is an extra here, never a reason to fail the boot */
|
|
734
1094
|
}
|
|
735
|
-
// A
|
|
736
|
-
// silently broken: these
|
|
737
|
-
//
|
|
1095
|
+
// A user reading their own logs would otherwise conclude the plugin was
|
|
1096
|
+
// silently broken: these facts are the visible difference between a working
|
|
1097
|
+
// install and a silent one.
|
|
738
1098
|
// A2: three states, ONE definition. The sentence that used to live here —
|
|
739
1099
|
// `配置里缺角色:…` — asserted a fact about files from a snapshot that is blind by
|
|
740
1100
|
// construction (`ctx.agent.transform` receives the agent set from before the
|
|
@@ -773,11 +1133,76 @@ export const v2Personality = {
|
|
|
773
1133
|
guard_shell_matched: guards.report.shellMatched,
|
|
774
1134
|
guard_strictened: guards.report.strictened,
|
|
775
1135
|
guard_denied: guards.report.denied,
|
|
1136
|
+
guard_envfile_denied: guards.report.envFileDenied,
|
|
1137
|
+
guard_envfile_denied_by_input: guards.report.envFileDeniedByInput,
|
|
1138
|
+
guard_envfile_input_seen: guards.report.envFileInputSeen,
|
|
1139
|
+
guard_envfile_input_threw: guards.report.envFileInputThrew,
|
|
1140
|
+
guard_envfile_input_foreign_skipped: guards.report.envFileInputForeignSkipped,
|
|
1141
|
+
concurrency_seen: guards.report.concurrencySeen,
|
|
1142
|
+
concurrency_denied: guards.report.concurrencyDenied,
|
|
1143
|
+
concurrency_running_max: guards.report.concurrencyRunningMax,
|
|
1144
|
+
concurrency_threw: guards.report.concurrencyThrew,
|
|
776
1145
|
subagent_seen: bgForce.report.seen,
|
|
777
1146
|
subagent_forced: bgForce.report.forced,
|
|
778
|
-
|
|
1147
|
+
shell_timeout_seen: shellTimeout.report.seen,
|
|
1148
|
+
shell_timeout_clamped: shellTimeout.report.clamped,
|
|
1149
|
+
shell_timeout_foreign_skipped: shellTimeout.report.foreignSkipped,
|
|
1150
|
+
shell_timeout_threw: shellTimeout.report.threw,
|
|
779
1151
|
note_pushed: session.report.notePushed,
|
|
780
1152
|
compaction_lines: session.report.compactionLines,
|
|
1153
|
+
// #49 feature 4: 拆分任务 — the same counter set as the surface row.
|
|
1154
|
+
split_seen: session.report.splitSeen,
|
|
1155
|
+
split_advised: session.report.splitAdvised,
|
|
1156
|
+
// Early compaction, counted per outcome so the claim is checkable: `fired` is
|
|
1157
|
+
// what we admitted, `confirmed` what the host accepted, and a `no_limit` with no
|
|
1158
|
+
// `fired` means we never found a denominator — not that the window was empty.
|
|
1159
|
+
compact_enabled: compaction.report.enabled,
|
|
1160
|
+
compact_at_percent: compaction.report.percent,
|
|
1161
|
+
compact_checked: compaction.report.checked,
|
|
1162
|
+
compact_usage_seen: compaction.report.usageSeen,
|
|
1163
|
+
compact_usage_events: compaction.report.usageEvents,
|
|
1164
|
+
compact_event_decided: compaction.report.eventDecided,
|
|
1165
|
+
compact_fired: compaction.report.fired,
|
|
1166
|
+
compact_confirmed: compaction.report.confirmed,
|
|
1167
|
+
compact_below: compaction.report.below,
|
|
1168
|
+
compact_deduped: compaction.report.deduped,
|
|
1169
|
+
compact_no_usage: compaction.report.noUsage,
|
|
1170
|
+
compact_no_limit: compaction.report.noLimit,
|
|
1171
|
+
compact_conflicts: compaction.report.conflicts,
|
|
1172
|
+
compact_threw: compaction.report.threw,
|
|
1173
|
+
compact_foreign_skipped: compaction.report.foreignSkipped,
|
|
1174
|
+
compact_source: compaction.report.lastSource,
|
|
1175
|
+
compact_last_percent: compaction.report.lastPercent,
|
|
1176
|
+
compact_error: compaction.report.lastError,
|
|
1177
|
+
// #49 Context Pruning — per-outcome, so "we pruned nothing" is
|
|
1178
|
+
// distinguishable from "we never found a denominator" (`no_limit`).
|
|
1179
|
+
prune_enabled: prune.report.enabled,
|
|
1180
|
+
prune_at_percent: prune.report.atPercent,
|
|
1181
|
+
prune_keep_tail_percent: prune.report.keepTailPercent,
|
|
1182
|
+
prune_checked: prune.report.checked,
|
|
1183
|
+
prune_pruned_messages: prune.report.prunedMessages,
|
|
1184
|
+
prune_pruned_tokens: prune.report.prunedTokens,
|
|
1185
|
+
prune_unknown_parts: prune.report.unknownParts,
|
|
1186
|
+
prune_skipped_protected: prune.report.skippedProtected,
|
|
1187
|
+
prune_below: prune.report.below,
|
|
1188
|
+
prune_no_limit: prune.report.noLimit,
|
|
1189
|
+
prune_no_messages: prune.report.noMessages,
|
|
1190
|
+
prune_foreign_skipped: prune.report.foreignSkipped,
|
|
1191
|
+
prune_threw: prune.report.threw,
|
|
1192
|
+
prune_last_percent: prune.report.lastPercent,
|
|
1193
|
+
// #49 feature 3: 错峰重试 — the same counter set as the surface row.
|
|
1194
|
+
retry_enabled: retry.report.enabled,
|
|
1195
|
+
retry_quota_seen: retry.report.quotaSeen,
|
|
1196
|
+
retry_rate_seen: retry.report.rateSeen,
|
|
1197
|
+
retry_transient_seen: retry.report.transientSeen,
|
|
1198
|
+
retry_unknown_seen: retry.report.unknownSeen,
|
|
1199
|
+
retry_break_trips: retry.report.breakTrips,
|
|
1200
|
+
retry_cooldowns: retry.report.cooldowns,
|
|
1201
|
+
retry_last_delay_ms: retry.report.lastDelayMs,
|
|
1202
|
+
retry_directives_injected: retry.report.directivesInjected,
|
|
1203
|
+
retry_cooldown_denied: retry.report.cooldownDenied,
|
|
1204
|
+
retry_foreign_skipped: retry.report.foreignSkipped,
|
|
1205
|
+
retry_threw: retry.report.threw,
|
|
781
1206
|
// Accumulates for the whole process, so it can only be written at
|
|
782
1207
|
// teardown: this is the number that answers "did the feed stay alive, and
|
|
783
1208
|
// did the host rename the event types under us after the last upgrade?"
|
|
@@ -789,6 +1214,8 @@ export const v2Personality = {
|
|
|
789
1214
|
browser_gate_refused: browserGate.report.refused,
|
|
790
1215
|
browser_gate_leaked: browserGate.report.leaked,
|
|
791
1216
|
browser_gate_held: browserGate.report.held,
|
|
1217
|
+
browser_gate_serp_nav: browserGate.report.serpNav,
|
|
1218
|
+
browser_gate_serp_refused: browserGate.report.serpRefused,
|
|
792
1219
|
browser_gate_note: browserGateSummary(browserGate.report),
|
|
793
1220
|
// What the model could actually call, read off the assembled requests —
|
|
794
1221
|
// the half of the delivery story the sent flag cannot tell.
|
|
@@ -845,7 +1272,7 @@ export const v2Personality = {
|
|
|
845
1272
|
/* a host that already tore down its own registry has nothing left to free */
|
|
846
1273
|
}
|
|
847
1274
|
}
|
|
848
|
-
//
|
|
1275
|
+
// A tool may own a child process — teardown must await it.
|
|
849
1276
|
await tmRuntime.dispose();
|
|
850
1277
|
};
|
|
851
1278
|
},
|