@te-river/opencode-team-mode 1.6.1 → 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.
Files changed (201) hide show
  1. package/README.md +117 -118
  2. package/README.zh-CN.md +98 -100
  3. package/dist/agents.d.ts.map +1 -1
  4. package/dist/agents.js +89 -79
  5. package/dist/agents.js.map +1 -1
  6. package/dist/approval-gate.d.ts +8 -9
  7. package/dist/approval-gate.d.ts.map +1 -1
  8. package/dist/approval-gate.js +13 -14
  9. package/dist/approval-gate.js.map +1 -1
  10. package/dist/capabilities.d.ts +11 -37
  11. package/dist/capabilities.d.ts.map +1 -1
  12. package/dist/capabilities.js +11 -141
  13. package/dist/capabilities.js.map +1 -1
  14. package/dist/commands.js +1 -1
  15. package/dist/envprotect/hook.d.ts +12 -31
  16. package/dist/envprotect/hook.d.ts.map +1 -1
  17. package/dist/envprotect/hook.js +13 -91
  18. package/dist/envprotect/hook.js.map +1 -1
  19. package/dist/envprotect/patterns.d.ts +6 -4
  20. package/dist/envprotect/patterns.d.ts.map +1 -1
  21. package/dist/envprotect/patterns.js +6 -4
  22. package/dist/envprotect/patterns.js.map +1 -1
  23. package/dist/envprotect.d.ts +1 -1
  24. package/dist/envprotect.d.ts.map +1 -1
  25. package/dist/envprotect.js +1 -1
  26. package/dist/envprotect.js.map +1 -1
  27. package/dist/host/note.d.ts.map +1 -1
  28. package/dist/host/note.js +8 -5
  29. package/dist/host/note.js.map +1 -1
  30. package/dist/host/v2-browser-gate.d.ts +19 -1
  31. package/dist/host/v2-browser-gate.d.ts.map +1 -1
  32. package/dist/host/v2-browser-gate.js +47 -6
  33. package/dist/host/v2-browser-gate.js.map +1 -1
  34. package/dist/host/v2-capabilities.d.ts +43 -0
  35. package/dist/host/v2-capabilities.d.ts.map +1 -1
  36. package/dist/host/v2-capabilities.js +77 -1
  37. package/dist/host/v2-capabilities.js.map +1 -1
  38. package/dist/host/v2-compaction.d.ts +129 -0
  39. package/dist/host/v2-compaction.d.ts.map +1 -0
  40. package/dist/host/v2-compaction.js +346 -0
  41. package/dist/host/v2-compaction.js.map +1 -0
  42. package/dist/host/v2-default-agent.d.ts +37 -0
  43. package/dist/host/v2-default-agent.d.ts.map +1 -0
  44. package/dist/host/v2-default-agent.js +128 -0
  45. package/dist/host/v2-default-agent.js.map +1 -0
  46. package/dist/host/v2-events.d.ts +2 -0
  47. package/dist/host/v2-events.d.ts.map +1 -1
  48. package/dist/host/v2-events.js +27 -0
  49. package/dist/host/v2-events.js.map +1 -1
  50. package/dist/host/v2-guard.d.ts +178 -0
  51. package/dist/host/v2-guard.d.ts.map +1 -1
  52. package/dist/host/v2-guard.js +324 -8
  53. package/dist/host/v2-guard.js.map +1 -1
  54. package/dist/host/v2-offload.d.ts +1 -1
  55. package/dist/host/v2-offload.js +2 -2
  56. package/dist/host/v2-permissions.d.ts +45 -44
  57. package/dist/host/v2-permissions.d.ts.map +1 -1
  58. package/dist/host/v2-permissions.js +48 -49
  59. package/dist/host/v2-permissions.js.map +1 -1
  60. package/dist/host/v2-probe-chain.d.ts +93 -0
  61. package/dist/host/v2-probe-chain.d.ts.map +1 -0
  62. package/dist/host/v2-probe-chain.js +146 -0
  63. package/dist/host/v2-probe-chain.js.map +1 -0
  64. package/dist/host/v2-prune.d.ts +157 -0
  65. package/dist/host/v2-prune.d.ts.map +1 -0
  66. package/dist/host/v2-prune.js +507 -0
  67. package/dist/host/v2-prune.js.map +1 -0
  68. package/dist/host/v2-retry.d.ts +143 -0
  69. package/dist/host/v2-retry.d.ts.map +1 -0
  70. package/dist/host/v2-retry.js +315 -0
  71. package/dist/host/v2-retry.js.map +1 -0
  72. package/dist/host/v2-scope.d.ts.map +1 -1
  73. package/dist/host/v2-scope.js +14 -7
  74. package/dist/host/v2-scope.js.map +1 -1
  75. package/dist/host/v2-session-client.d.ts +62 -7
  76. package/dist/host/v2-session-client.d.ts.map +1 -1
  77. package/dist/host/v2-session-client.js +485 -26
  78. package/dist/host/v2-session-client.js.map +1 -1
  79. package/dist/host/v2-session.d.ts +72 -1
  80. package/dist/host/v2-session.d.ts.map +1 -1
  81. package/dist/host/v2-session.js +154 -10
  82. package/dist/host/v2-session.js.map +1 -1
  83. package/dist/host/v2-tool.d.ts +2 -3
  84. package/dist/host/v2-tool.d.ts.map +1 -1
  85. package/dist/host/v2-tool.js +2 -3
  86. package/dist/host/v2-tool.js.map +1 -1
  87. package/dist/host/v2-types.d.ts +25 -0
  88. package/dist/host/v2-types.d.ts.map +1 -1
  89. package/dist/host/v2.d.ts +14 -12
  90. package/dist/host/v2.d.ts.map +1 -1
  91. package/dist/host/v2.js +648 -74
  92. package/dist/host/v2.js.map +1 -1
  93. package/dist/identity.d.ts +27 -0
  94. package/dist/identity.d.ts.map +1 -0
  95. package/dist/identity.js +34 -0
  96. package/dist/identity.js.map +1 -0
  97. package/dist/index.d.ts +9 -24
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +17 -27
  100. package/dist/index.js.map +1 -1
  101. package/dist/prompts/lead.d.ts +1 -1
  102. package/dist/prompts/lead.d.ts.map +1 -1
  103. package/dist/prompts/lead.js +44 -27
  104. package/dist/prompts/lead.js.map +1 -1
  105. package/dist/prompts/shared.d.ts +2 -2
  106. package/dist/prompts/shared.d.ts.map +1 -1
  107. package/dist/prompts/shared.js +74 -57
  108. package/dist/prompts/shared.js.map +1 -1
  109. package/dist/prompts/specialists.d.ts +2 -2
  110. package/dist/prompts/specialists.d.ts.map +1 -1
  111. package/dist/prompts/specialists.js +53 -83
  112. package/dist/prompts/specialists.js.map +1 -1
  113. package/dist/tm/args-schema.d.ts +7 -32
  114. package/dist/tm/args-schema.d.ts.map +1 -1
  115. package/dist/tm/args-schema.js +12 -153
  116. package/dist/tm/args-schema.js.map +1 -1
  117. package/dist/tm/board.d.ts +10 -9
  118. package/dist/tm/board.d.ts.map +1 -1
  119. package/dist/tm/board.js +11 -10
  120. package/dist/tm/board.js.map +1 -1
  121. package/dist/tm/config-files.d.ts +81 -0
  122. package/dist/tm/config-files.d.ts.map +1 -0
  123. package/dist/tm/config-files.js +209 -0
  124. package/dist/tm/config-files.js.map +1 -0
  125. package/dist/tm/config-layers.d.ts +117 -0
  126. package/dist/tm/config-layers.d.ts.map +1 -0
  127. package/dist/tm/config-layers.js +320 -0
  128. package/dist/tm/config-layers.js.map +1 -0
  129. package/dist/tm/config.d.ts +10 -101
  130. package/dist/tm/config.d.ts.map +1 -1
  131. package/dist/tm/config.js +17 -71
  132. package/dist/tm/config.js.map +1 -1
  133. package/dist/tm/dispatch.d.ts +154 -22
  134. package/dist/tm/dispatch.d.ts.map +1 -1
  135. package/dist/tm/dispatch.js +559 -80
  136. package/dist/tm/dispatch.js.map +1 -1
  137. package/dist/tm/egress.d.ts +19 -7
  138. package/dist/tm/egress.d.ts.map +1 -1
  139. package/dist/tm/egress.js +21 -9
  140. package/dist/tm/egress.js.map +1 -1
  141. package/dist/tm/index.d.ts +23 -20
  142. package/dist/tm/index.d.ts.map +1 -1
  143. package/dist/tm/index.js +117 -116
  144. package/dist/tm/index.js.map +1 -1
  145. package/dist/tm/ledger.d.ts +9 -2
  146. package/dist/tm/ledger.d.ts.map +1 -1
  147. package/dist/tm/ledger.js +4 -1
  148. package/dist/tm/ledger.js.map +1 -1
  149. package/dist/tm/perm-ask.d.ts +3 -3
  150. package/dist/tm/perm-ask.d.ts.map +1 -1
  151. package/dist/tm/perm-ask.js +3 -3
  152. package/dist/tm/perm-ask.js.map +1 -1
  153. package/dist/tm/pipelines.d.ts +12 -11
  154. package/dist/tm/pipelines.d.ts.map +1 -1
  155. package/dist/tm/pipelines.js +40 -207
  156. package/dist/tm/pipelines.js.map +1 -1
  157. package/dist/tm/preview.d.ts +1 -1
  158. package/dist/tm/preview.js +3 -3
  159. package/dist/tm/preview.js.map +1 -1
  160. package/dist/tm/refs.d.ts +31 -4
  161. package/dist/tm/refs.d.ts.map +1 -1
  162. package/dist/tm/refs.js +48 -4
  163. package/dist/tm/refs.js.map +1 -1
  164. package/dist/tm/result.d.ts +1 -1
  165. package/dist/tm/result.d.ts.map +1 -1
  166. package/dist/tm/result.js +4 -3
  167. package/dist/tm/result.js.map +1 -1
  168. package/dist/tm/search.d.ts +2 -2
  169. package/dist/tm/search.js +7 -7
  170. package/dist/tm/search.js.map +1 -1
  171. package/dist/tm/serp-loop.d.ts +19 -7
  172. package/dist/tm/serp-loop.d.ts.map +1 -1
  173. package/dist/tm/serp-loop.js +24 -9
  174. package/dist/tm/serp-loop.js.map +1 -1
  175. package/dist/tm/stats.d.ts +31 -27
  176. package/dist/tm/stats.d.ts.map +1 -1
  177. package/dist/tm/stats.js +170 -58
  178. package/dist/tm/stats.js.map +1 -1
  179. package/dist/tm/store.d.ts +18 -5
  180. package/dist/tm/store.d.ts.map +1 -1
  181. package/dist/tm/store.js +39 -9
  182. package/dist/tm/store.js.map +1 -1
  183. package/dist/tm/tools.d.ts +7 -5
  184. package/dist/tm/tools.d.ts.map +1 -1
  185. package/dist/tm/tools.js +10 -41
  186. package/dist/tm/tools.js.map +1 -1
  187. package/dist/tm/webfetch.d.ts +1 -1
  188. package/dist/tm/webfetch.d.ts.map +1 -1
  189. package/dist/tm/webfetch.js +38 -23
  190. package/dist/tm/webfetch.js.map +1 -1
  191. package/dist/types.d.ts +40 -123
  192. package/dist/types.d.ts.map +1 -1
  193. package/dist/types.js +7 -21
  194. package/dist/types.js.map +1 -1
  195. package/docs/installation-v2.md +124 -39
  196. package/docs/installation.md +9 -0
  197. package/docs/research/browser-pane.md +5 -0
  198. package/docs/v2-port-notes.md +500 -315
  199. package/package.json +1 -1
  200. package/scripts/gen-v2-config.mjs +246 -94
  201. package/scripts/lib/fake-ctx.mjs +244 -146
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-probe-chain.js","sourceRoot":"","sources":["../../src/host/v2-probe-chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Context Pruning — the "pointer for volume" half of the context-budget work
3
+ * (plan 功能 1, item #49).
4
+ *
5
+ * `v2-offload.ts` governs a result AT THE MOMENT it is produced; it can do
6
+ * nothing about history that already entered the context window. This layer
7
+ * rides the SAME `session.hook("context")` seam the request layer uses and, once
8
+ * a session crosses a fraction of its model's window, replaces the BODY of
9
+ * settled messages with a one-line POINTER that keeps the recoverable address
10
+ * (a `tm_fetch` handle, a child session id, or an explicit "re-run to recover").
11
+ *
12
+ * The rules that make this safe rather than destructive:
13
+ *
14
+ * - **Only settled messages are touched.** The newest message is always kept,
15
+ * a message naming a still-running child (`state="running"`) is kept, and a
16
+ * message carrying a bare `ses_…` id with no settled marker is kept — an
17
+ * uncollected child is still-running work, not finished work.
18
+ * - **The hard-protection list is never cut** (`isProtectedMessage`): the reply
19
+ * skeleton, GOAL/ACCEPTANCE, provenance (`正文来源=`), system messages and the
20
+ * board note. A `tm_fetch` handle is NOT a protected message — it is a
21
+ * protected STRING: the message may be pruned, but the stub must carry the
22
+ * handle verbatim, which is exactly the "keep the pointer, drop the body"
23
+ * contract.
24
+ * - **The threshold is derived, never hard-coded.** `TM_PRUNE_AT_PERCENT`
25
+ * (default 70) of `limit.context`, the same denominator the desktop's own
26
+ * usage meter reads. No `limit.context` → no prune, counted `no_limit`.
27
+ * - **Never throws, never rewrites an untouched message.** A failure allows
28
+ * the request through and is counted; a message outside the plan is left
29
+ * byte-exact.
30
+ * - **Idempotent.** The stub carries a sentinel, so a host that replays the
31
+ * hook in-process cannot prune the same message twice.
32
+ */
33
+ import type { TeamScope } from "./v2-scope.js";
34
+ import type { V2Context, V2Registration } from "./v2-types.js";
35
+ /** The marker every stub carries. Its presence means "already pruned". */
36
+ export declare const PRUNE_SENTINEL = "[\u5DF2\u88C1\u526A \u00B7";
37
+ export interface PruneConfig {
38
+ enabled: boolean;
39
+ /** Percent of `limit.context` at which pruning starts (clamped 40–90). */
40
+ atPercent: number;
41
+ /** Percent of `limit.context` kept verbatim at the tail (clamped 0–90). */
42
+ keepTailPercent: number;
43
+ }
44
+ export declare function resolvePruneConfig(env?: Record<string, string | undefined>): PruneConfig;
45
+ interface TextSlot {
46
+ container: Record<string, unknown>;
47
+ key: string;
48
+ text: string;
49
+ }
50
+ /**
51
+ * Every text slot in a message, across the shapes the host has actually used:
52
+ * `{info:{role},parts:[{type:"text",text}]}` (v1-ish), `{role,parts:[…]}`,
53
+ * `{role,content:[{type:"text",text}]}` (2.0.20), the flat
54
+ * `{id,time,type,text}` (2.0.18), and the 2.0.24 set a live probe measured —
55
+ * `{role:"tool",content:[{type:"tool-result",result}]}` (the body is under
56
+ * `result`, there is no `text`), `{type:"reasoning",text}`, and
57
+ * `{type:"tool-call",input}` (the arguments are the body; #64). Recognition
58
+ * is BY SHAPE, never by host version (the same discipline
59
+ * `normaliseContextMessages` follows). An unrecognised shape yields no slots and
60
+ * is therefore never touched.
61
+ */
62
+ export declare function textSlots(msg: unknown): TextSlot[];
63
+ /**
64
+ * How many parts in a message carry a `type` we do not recognise. A non-zero
65
+ * count beside a small `used` is the signature of a renamed shape — the exact
66
+ * failure that made pruning a no-op on 2.0.24 while every counter read healthy.
67
+ * A KNOWN-but-textless part (`media`) is not counted: it is not a renamed
68
+ * shape, and counting it would drown the signal in attachment noise.
69
+ */
70
+ export declare function unknownPartCount(msg: unknown): number;
71
+ export declare function messagesUnknownParts(messages: unknown): number;
72
+ export declare function messageText(msg: unknown): string;
73
+ export declare function messageTokens(msg: unknown): number;
74
+ export declare function messagesTokens(messages: unknown): number;
75
+ /**
76
+ * A message that must never be pruned. The handle check is deliberately ABSENT:
77
+ * a handle-bearing message is prunable, and `renderPruneStub` carries the handle
78
+ * verbatim — the pointer survives even when the body does not.
79
+ */
80
+ export declare function isProtectedMessage(msg: unknown): boolean;
81
+ export interface PruneHandle {
82
+ ref: string;
83
+ accessToken: string;
84
+ expireAt: number;
85
+ }
86
+ export interface PruneStubInput {
87
+ kind: "handle" | "child" | "echo";
88
+ originalTokens: number;
89
+ handle?: PruneHandle;
90
+ childId?: string;
91
+ childState?: string;
92
+ }
93
+ /**
94
+ * The replacement text. It must be self-explanatory: a reader has to know what
95
+ * used to be here and where to get it back. The handle / child id is copied
96
+ * VERBATIM — a stub that dropped the address would be evidence destruction.
97
+ */
98
+ export declare function renderPruneStub(entry: PruneStubInput): string;
99
+ export interface PrunePlanEntry {
100
+ index: number;
101
+ originalTokens: number;
102
+ stub: string;
103
+ kind: PruneStubInput["kind"];
104
+ }
105
+ export interface PrunePlan {
106
+ prune: PrunePlanEntry[];
107
+ protectedCount: number;
108
+ keptCount: number;
109
+ savedTokens: number;
110
+ }
111
+ export interface PrunePlanOptions {
112
+ /** Target size: stop pruning once `used - saved <= budgetTokens`. */
113
+ budgetTokens?: number;
114
+ /** Newest messages whose cumulative tokens fit here are kept verbatim. */
115
+ keepTailTokens?: number;
116
+ }
117
+ /**
118
+ * Decide which messages to prune, oldest first, until the request fits the
119
+ * budget. Pure: it reads the messages and returns a plan; nothing is mutated.
120
+ */
121
+ export declare function prunePlan(messages: unknown, opts?: PrunePlanOptions): PrunePlan;
122
+ /** Apply a plan in place: the first text slot becomes the stub, the rest blank. */
123
+ export declare function applyPrunePlan(messages: unknown, plan: PrunePlan): void;
124
+ export interface PruneReport {
125
+ enabled: boolean;
126
+ atPercent: number;
127
+ keepTailPercent: number;
128
+ checked: number;
129
+ prunedMessages: number;
130
+ prunedTokens: number;
131
+ skippedProtected: number;
132
+ below: number;
133
+ noLimit: number;
134
+ noMessages: number;
135
+ foreignSkipped: number;
136
+ threw: number;
137
+ /** Parts whose `type` we did not recognise — a renamed shape shows up here. */
138
+ unknownParts: number;
139
+ lastPercent: number;
140
+ lastUsed: number;
141
+ lastLimit: number;
142
+ }
143
+ export interface PruneInput {
144
+ config?: PruneConfig;
145
+ scope?: TeamScope;
146
+ /** Resolve the context window for a session; null = no denominator → no prune. */
147
+ modelLimitOf?: (sessionID: string, messages: unknown[]) => Promise<number | null> | number | null;
148
+ /** Per-decision trajectory line (counters alone are invisible in a CLI run). */
149
+ onEvent?: (row: Record<string, unknown>) => void;
150
+ }
151
+ export interface PruneLayer {
152
+ registrations: V2Registration[];
153
+ report: PruneReport;
154
+ }
155
+ export declare function applyV2ContextPrune(ctx: V2Context, input?: PruneInput): Promise<PruneLayer>;
156
+ export {};
157
+ //# sourceMappingURL=v2-prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-prune.d.ts","sourceRoot":"","sources":["../../src/host/v2-prune.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,507 @@
1
+ /**
2
+ * Context Pruning — the "pointer for volume" half of the context-budget work
3
+ * (plan 功能 1, item #49).
4
+ *
5
+ * `v2-offload.ts` governs a result AT THE MOMENT it is produced; it can do
6
+ * nothing about history that already entered the context window. This layer
7
+ * rides the SAME `session.hook("context")` seam the request layer uses and, once
8
+ * a session crosses a fraction of its model's window, replaces the BODY of
9
+ * settled messages with a one-line POINTER that keeps the recoverable address
10
+ * (a `tm_fetch` handle, a child session id, or an explicit "re-run to recover").
11
+ *
12
+ * The rules that make this safe rather than destructive:
13
+ *
14
+ * - **Only settled messages are touched.** The newest message is always kept,
15
+ * a message naming a still-running child (`state="running"`) is kept, and a
16
+ * message carrying a bare `ses_…` id with no settled marker is kept — an
17
+ * uncollected child is still-running work, not finished work.
18
+ * - **The hard-protection list is never cut** (`isProtectedMessage`): the reply
19
+ * skeleton, GOAL/ACCEPTANCE, provenance (`正文来源=`), system messages and the
20
+ * board note. A `tm_fetch` handle is NOT a protected message — it is a
21
+ * protected STRING: the message may be pruned, but the stub must carry the
22
+ * handle verbatim, which is exactly the "keep the pointer, drop the body"
23
+ * contract.
24
+ * - **The threshold is derived, never hard-coded.** `TM_PRUNE_AT_PERCENT`
25
+ * (default 70) of `limit.context`, the same denominator the desktop's own
26
+ * usage meter reads. No `limit.context` → no prune, counted `no_limit`.
27
+ * - **Never throws, never rewrites an untouched message.** A failure allows
28
+ * the request through and is counted; a message outside the plan is left
29
+ * byte-exact.
30
+ * - **Idempotent.** The stub carries a sentinel, so a host that replays the
31
+ * hook in-process cannot prune the same message twice.
32
+ */
33
+ import { estimateTokens } from "../tm/config.js";
34
+ import { percentOf, readModelKey } from "./v2-compaction.js";
35
+ /** The marker every stub carries. Its presence means "already pruned". */
36
+ export const PRUNE_SENTINEL = "[已裁剪 ·";
37
+ /** Reply-skeleton keys. A message carrying any of them is a report, not noise. */
38
+ const SKELETON_KEYS = ["STATUS:", "CHANGES:", "FINDINGS:", "EVIDENCE:", "HANDOFF:"];
39
+ const CHILD_ID_RE = /ses_[A-Za-z0-9]+/;
40
+ const SETTLED_STATE_RE = /state\s*=\s*"(?:completed|error)"/i;
41
+ const RUNNING_STATE_RE = /state\s*=\s*"(?:running|pending|in[_-]?progress)"/i;
42
+ const HANDLE_RE = /ref\s*:\s*"([^"]+)"[\s\S]*?access_token\s*:\s*"([^"]+)"[\s\S]*?expire_at\s*:\s*(\d+)/;
43
+ const isOff = (v) => typeof v === "string" && ["off", "false", "0", "no"].includes(v.trim().toLowerCase());
44
+ function clampPercent(raw, fallback, min, max) {
45
+ const n = Number.parseInt(String(raw ?? ""), 10);
46
+ if (!Number.isFinite(n) || n <= 0)
47
+ return fallback;
48
+ return Math.min(max, Math.max(min, n));
49
+ }
50
+ export function resolvePruneConfig(env = process.env) {
51
+ return {
52
+ enabled: !isOff(env.TM_PRUNE),
53
+ // The floor is 40 ON PURPOSE: a value below it (e.g. 5) is clamped UP, so a
54
+ // reproduction that sets TM_PRUNE_AT_PERCENT=5 and sees no prune is looking
55
+ // at the clamp, not at a broken layer.
56
+ atPercent: clampPercent(env.TM_PRUNE_AT_PERCENT, 70, 40, 90),
57
+ keepTailPercent: clampPercent(env.TM_PRUNE_KEEP_TAIL_PERCENT, 40, 0, 90),
58
+ };
59
+ }
60
+ /**
61
+ * The part types whose text we know how to read. A part of any OTHER type is
62
+ * COUNTED (`unknownPartCount`), never guessed — the conservative half of the
63
+ * shape rule: an unrecognised part contributes nothing to `used`, so a shape the
64
+ * host renames can only make us UNDER-count (never prune evidence we misread), and
65
+ * the counter is what keeps that from being silent.
66
+ *
67
+ * `tool-call` joined the set from a live 2.0.24 probe (#64): the host's own
68
+ * `LLM.Content.ToolCall` carries the arguments under `input` (there is NO
69
+ * `text`), and it rides every request — leaving it out made `used` read low on
70
+ * exactly the messages a tool-heavy session is made of.
71
+ */
72
+ const TEXT_PART_TYPES = new Set(["text", "tool-result", "reasoning", "tool-call"]);
73
+ /**
74
+ * Part types we KNOW carry no text — a pure reference/metadata part. They are
75
+ * excluded from `unknownPartCount` (a media attachment is not a renamed shape, so
76
+ * it must not read as noise) but contribute nothing to `used` (there is no body
77
+ * to count). Measured on 2.0.24: a `media` part is
78
+ * `{type:"media", media:{…asset ref…}, filename}` — a reference, not text.
79
+ */
80
+ const TEXTLESS_PART_TYPES = new Set(["media"]);
81
+ /** Every part type we recognise, text-bearing or not. */
82
+ const KNOWN_PART_TYPES = new Set([...TEXT_PART_TYPES, ...TEXTLESS_PART_TYPES]);
83
+ /**
84
+ * Read the text out of a `result`/`content` value that may be a string, an
85
+ * array, or an object. A tool result is not always a string — 2.0.24 carries
86
+ * the body under `result`, and it can be a nested `{content:[{type:"text",
87
+ * text}]}` — so a bare `String(obj)` would count `[object Object]` and lose the
88
+ * payload. Known text-bearing keys are tried first; an object with none of them
89
+ * falls back to its JSON, which is a fair token estimate and never the literal
90
+ * `[object Object]`.
91
+ */
92
+ function extractText(value, depth = 0) {
93
+ if (typeof value === "string")
94
+ return value;
95
+ if (typeof value === "number" || typeof value === "boolean")
96
+ return String(value);
97
+ if (value == null)
98
+ return "";
99
+ if (depth > 6)
100
+ return "";
101
+ if (Array.isArray(value)) {
102
+ return value
103
+ .map((v) => extractText(v, depth + 1))
104
+ .filter((s) => s.length > 0)
105
+ .join("\n");
106
+ }
107
+ if (typeof value === "object") {
108
+ const o = value;
109
+ for (const key of ["text", "content", "output", "result", "value", "message", "body"]) {
110
+ if (key in o) {
111
+ const t = extractText(o[key], depth + 1);
112
+ if (t)
113
+ return t;
114
+ }
115
+ }
116
+ try {
117
+ return JSON.stringify(value);
118
+ }
119
+ catch {
120
+ return "";
121
+ }
122
+ }
123
+ return "";
124
+ }
125
+ /**
126
+ * Every text slot in a message, across the shapes the host has actually used:
127
+ * `{info:{role},parts:[{type:"text",text}]}` (v1-ish), `{role,parts:[…]}`,
128
+ * `{role,content:[{type:"text",text}]}` (2.0.20), the flat
129
+ * `{id,time,type,text}` (2.0.18), and the 2.0.24 set a live probe measured —
130
+ * `{role:"tool",content:[{type:"tool-result",result}]}` (the body is under
131
+ * `result`, there is no `text`), `{type:"reasoning",text}`, and
132
+ * `{type:"tool-call",input}` (the arguments are the body; #64). Recognition
133
+ * is BY SHAPE, never by host version (the same discipline
134
+ * `normaliseContextMessages` follows). An unrecognised shape yields no slots and
135
+ * is therefore never touched.
136
+ */
137
+ export function textSlots(msg) {
138
+ const m = msg;
139
+ if (!m || typeof m !== "object")
140
+ return [];
141
+ const slots = [];
142
+ const collect = (arr) => {
143
+ if (!Array.isArray(arr))
144
+ return;
145
+ for (const p of arr) {
146
+ const part = p;
147
+ if (!part || typeof part !== "object")
148
+ continue;
149
+ if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") {
150
+ slots.push({ container: part, key: "text", text: part.text });
151
+ }
152
+ else if (part.type === "tool-result" && "result" in part) {
153
+ const text = extractText(part.result);
154
+ if (text)
155
+ slots.push({ container: part, key: "result", text });
156
+ }
157
+ else if (part.type === "tool-call" && "input" in part) {
158
+ const text = extractText(part.input);
159
+ if (text)
160
+ slots.push({ container: part, key: "input", text });
161
+ }
162
+ }
163
+ };
164
+ if (Array.isArray(m.parts))
165
+ collect(m.parts);
166
+ else if (Array.isArray(m.content))
167
+ collect(m.content);
168
+ else if (typeof m.text === "string")
169
+ slots.push({ container: m, key: "text", text: m.text });
170
+ return slots;
171
+ }
172
+ /**
173
+ * How many parts in a message carry a `type` we do not recognise. A non-zero
174
+ * count beside a small `used` is the signature of a renamed shape — the exact
175
+ * failure that made pruning a no-op on 2.0.24 while every counter read healthy.
176
+ * A KNOWN-but-textless part (`media`) is not counted: it is not a renamed
177
+ * shape, and counting it would drown the signal in attachment noise.
178
+ */
179
+ export function unknownPartCount(msg) {
180
+ const m = msg;
181
+ if (!m || typeof m !== "object")
182
+ return 0;
183
+ const arr = Array.isArray(m.parts) ? m.parts : Array.isArray(m.content) ? m.content : null;
184
+ if (!arr)
185
+ return 0;
186
+ let n = 0;
187
+ for (const p of arr) {
188
+ const part = p;
189
+ if (part && typeof part === "object" && typeof part.type === "string" && !KNOWN_PART_TYPES.has(part.type)) {
190
+ n++;
191
+ }
192
+ }
193
+ return n;
194
+ }
195
+ export function messagesUnknownParts(messages) {
196
+ if (!Array.isArray(messages))
197
+ return 0;
198
+ let n = 0;
199
+ for (const m of messages)
200
+ n += unknownPartCount(m);
201
+ return n;
202
+ }
203
+ export function messageText(msg) {
204
+ return textSlots(msg).map((s) => s.text).join("\n");
205
+ }
206
+ export function messageTokens(msg) {
207
+ return estimateTokens(messageText(msg));
208
+ }
209
+ export function messagesTokens(messages) {
210
+ if (!Array.isArray(messages))
211
+ return 0;
212
+ let sum = 0;
213
+ for (const m of messages)
214
+ sum += messageTokens(m);
215
+ return sum;
216
+ }
217
+ function messageRole(msg) {
218
+ const m = msg;
219
+ if (!m || typeof m !== "object")
220
+ return "";
221
+ const info = m.info;
222
+ const role = m.role ?? info?.role;
223
+ return typeof role === "string" ? role.toLowerCase() : "";
224
+ }
225
+ // ---------- the hard-protection list ----------
226
+ /**
227
+ * A message that must never be pruned. The handle check is deliberately ABSENT:
228
+ * a handle-bearing message is prunable, and `renderPruneStub` carries the handle
229
+ * verbatim — the pointer survives even when the body does not.
230
+ */
231
+ export function isProtectedMessage(msg) {
232
+ const text = messageText(msg);
233
+ if (!text)
234
+ return false;
235
+ if (messageRole(msg) === "system")
236
+ return true;
237
+ if (text.includes("## Team Blackboard"))
238
+ return true;
239
+ if (SKELETON_KEYS.some((k) => text.includes(k)))
240
+ return true;
241
+ if (/\bGOAL\s*:/.test(text) || /\bACCEPTANCE\s*:/.test(text))
242
+ return true;
243
+ if (text.includes("正文来源="))
244
+ return true;
245
+ if (RUNNING_STATE_RE.test(text))
246
+ return true;
247
+ // A child id with no settled marker is an uncollected child — still-running work.
248
+ if (CHILD_ID_RE.test(text) && !SETTLED_STATE_RE.test(text))
249
+ return true;
250
+ return false;
251
+ }
252
+ const withCommas = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
253
+ /**
254
+ * The replacement text. It must be self-explanatory: a reader has to know what
255
+ * used to be here and where to get it back. The handle / child id is copied
256
+ * VERBATIM — a stub that dropped the address would be evidence destruction.
257
+ */
258
+ export function renderPruneStub(entry) {
259
+ const head = `${PRUNE_SENTINEL} 原 ${withCommas(entry.originalTokens)} token]`;
260
+ if (entry.kind === "handle" && entry.handle) {
261
+ return `${head} 工具结果已卸载:tm_fetch { ref:"${entry.handle.ref}", access_token:"${entry.handle.accessToken}", expire_at:${entry.handle.expireAt} }。需要原文用 tm_fetch 取回,不要重跑命令。`;
262
+ }
263
+ if (entry.kind === "child" && entry.childId) {
264
+ return `${head} 子代理 ${entry.childId} 的报告已结算(state=${entry.childState ?? "completed"});需要原文用 tm_join { ids:["${entry.childId}"] } 取回。`;
265
+ }
266
+ return `${head} 已结算的工具输出/命令回显,正文已裁剪;如需重看请重跑原命令,或从轨迹(tm_stats { recent: N })取回。`;
267
+ }
268
+ function extractHandle(text) {
269
+ const m = HANDLE_RE.exec(text);
270
+ if (!m)
271
+ return null;
272
+ return { ref: m[1], accessToken: m[2], expireAt: Number(m[3]) };
273
+ }
274
+ function extractChild(text) {
275
+ const id = CHILD_ID_RE.exec(text)?.[0];
276
+ if (!id)
277
+ return null;
278
+ const state = SETTLED_STATE_RE.exec(text)?.[1] ?? "completed";
279
+ return { id, state };
280
+ }
281
+ function buildStub(text, tokens) {
282
+ const handle = extractHandle(text);
283
+ if (handle)
284
+ return { kind: "handle", stub: renderPruneStub({ kind: "handle", originalTokens: tokens, handle }) };
285
+ const child = extractChild(text);
286
+ if (child)
287
+ return { kind: "child", stub: renderPruneStub({ kind: "child", originalTokens: tokens, childId: child.id, childState: child.state }) };
288
+ return { kind: "echo", stub: renderPruneStub({ kind: "echo", originalTokens: tokens }) };
289
+ }
290
+ /**
291
+ * Decide which messages to prune, oldest first, until the request fits the
292
+ * budget. Pure: it reads the messages and returns a plan; nothing is mutated.
293
+ */
294
+ export function prunePlan(messages, opts = {}) {
295
+ const plan = { prune: [], protectedCount: 0, keptCount: 0, savedTokens: 0 };
296
+ if (!Array.isArray(messages) || messages.length === 0)
297
+ return plan;
298
+ const keepTailTokens = Math.max(0, opts.keepTailTokens ?? 0);
299
+ const budgetTokens = opts.budgetTokens ?? Number.POSITIVE_INFINITY;
300
+ // The tail boundary: walk from the newest message until the cumulative token
301
+ // count would exceed the tail budget. The newest message is ALWAYS protected.
302
+ let acc = 0;
303
+ let tailStart = messages.length;
304
+ for (let i = messages.length - 1; i >= 0; i--) {
305
+ const t = messageTokens(messages[i]);
306
+ if (acc + t > keepTailTokens)
307
+ break;
308
+ acc += t;
309
+ tailStart = i;
310
+ }
311
+ tailStart = Math.min(tailStart, messages.length - 1);
312
+ const total = messagesTokens(messages);
313
+ let saved = 0;
314
+ for (let i = 0; i < tailStart; i++) {
315
+ const msg = messages[i];
316
+ if (isProtectedMessage(msg)) {
317
+ plan.protectedCount++;
318
+ continue;
319
+ }
320
+ const text = messageText(msg);
321
+ if (!text || text.includes(PRUNE_SENTINEL)) {
322
+ plan.keptCount++;
323
+ continue;
324
+ }
325
+ if (total - saved <= budgetTokens) {
326
+ plan.keptCount++;
327
+ continue;
328
+ }
329
+ const originalTokens = estimateTokens(text);
330
+ const { kind, stub } = buildStub(text, originalTokens);
331
+ const delta = Math.max(0, originalTokens - estimateTokens(stub));
332
+ plan.prune.push({ index: i, originalTokens, stub, kind });
333
+ saved += delta;
334
+ }
335
+ plan.savedTokens = saved;
336
+ return plan;
337
+ }
338
+ /** Apply a plan in place: the first text slot becomes the stub, the rest blank. */
339
+ export function applyPrunePlan(messages, plan) {
340
+ if (!Array.isArray(messages))
341
+ return;
342
+ for (const entry of plan.prune) {
343
+ const slots = textSlots(messages[entry.index]);
344
+ if (!slots.length)
345
+ continue;
346
+ slots[0].container[slots[0].key] = entry.stub;
347
+ for (let i = 1; i < slots.length; i++)
348
+ slots[i].container[slots[i].key] = "";
349
+ }
350
+ }
351
+ /** The default denominator: `ctx.model.list()` keyed by the session's model,
352
+ * the same lookup the desktop's usage meter and the compaction layer use. */
353
+ function defaultModelLimitOf(ctx) {
354
+ let limits = null;
355
+ const askedUnknown = new Set();
356
+ const modelBySession = new Map();
357
+ async function loadLimits(force = false) {
358
+ if (!limits || force) {
359
+ const next = new Map();
360
+ try {
361
+ const res = (await ctx.model?.list?.({}));
362
+ const data = Array.isArray(res?.data) ? res.data : [];
363
+ for (const m of data) {
364
+ const any = m;
365
+ const providerID = typeof any?.providerID === "string" ? any.providerID : typeof any?.id === "string" ? any.id : "";
366
+ const id = typeof any?.id === "string" ? any.id : typeof any?.modelID === "string" ? any.modelID : "";
367
+ const limit = any?.limit?.context;
368
+ if (providerID && id && typeof limit === "number" && limit > 0)
369
+ next.set(`${providerID}/${id}`, limit);
370
+ }
371
+ }
372
+ catch {
373
+ /* no catalog is a counted no_limit, never a guess */
374
+ }
375
+ limits = next;
376
+ }
377
+ return limits;
378
+ }
379
+ function modelFromMessages(messages) {
380
+ for (let i = messages.length - 1; i >= 0; i--) {
381
+ const found = readModelKey(messages[i]);
382
+ if (found)
383
+ return found;
384
+ }
385
+ return null;
386
+ }
387
+ async function sessionModel(sessionID) {
388
+ const known = modelBySession.get(sessionID);
389
+ if (known)
390
+ return known;
391
+ try {
392
+ const get = ctx.session?.get;
393
+ const rec = get ? (await get.call(ctx.session, { sessionID })) : undefined;
394
+ const found = readModelKey(rec);
395
+ if (found)
396
+ modelBySession.set(sessionID, found);
397
+ return found;
398
+ }
399
+ catch {
400
+ return null;
401
+ }
402
+ }
403
+ return async (sessionID, messages) => {
404
+ const model = modelFromMessages(messages) ?? (await sessionModel(sessionID));
405
+ if (!model)
406
+ return null;
407
+ const key = `${model.providerID}/${model.id}`;
408
+ let limit = (await loadLimits()).get(key);
409
+ if (!limit && !askedUnknown.has(key)) {
410
+ askedUnknown.add(key);
411
+ limit = (await loadLimits(true)).get(key);
412
+ }
413
+ return limit ?? null;
414
+ };
415
+ }
416
+ export async function applyV2ContextPrune(ctx, input = {}) {
417
+ const config = input.config ?? resolvePruneConfig();
418
+ const report = {
419
+ enabled: config.enabled,
420
+ atPercent: config.atPercent,
421
+ keepTailPercent: config.keepTailPercent,
422
+ checked: 0,
423
+ prunedMessages: 0,
424
+ prunedTokens: 0,
425
+ skippedProtected: 0,
426
+ below: 0,
427
+ noLimit: 0,
428
+ noMessages: 0,
429
+ foreignSkipped: 0,
430
+ threw: 0,
431
+ unknownParts: 0,
432
+ lastPercent: 0,
433
+ lastUsed: 0,
434
+ lastLimit: 0,
435
+ };
436
+ const registrations = [];
437
+ const session = ctx.session;
438
+ if (!config.enabled || !session || typeof session.hook !== "function") {
439
+ return { registrations, report };
440
+ }
441
+ const modelLimitOf = input.modelLimitOf ?? defaultModelLimitOf(ctx);
442
+ const emit = (kind, fields) => {
443
+ try {
444
+ input.onEvent?.({ kind, ...fields });
445
+ }
446
+ catch {
447
+ /* a diagnostic that cannot be written never breaks the thing it describes */
448
+ }
449
+ };
450
+ registrations.push(await session.hook("context", async (event) => {
451
+ try {
452
+ // Team-scope isolation: `session.context` fires for EVERY agent on the
453
+ // host, so an unguarded write here would prune somebody else's request.
454
+ if (input.scope && input.scope.count(input.scope.decide(event)) !== "ours") {
455
+ report.foreignSkipped++;
456
+ return;
457
+ }
458
+ input.scope?.learn(event?.agent, event?.sessionID);
459
+ const messages = event?.messages;
460
+ if (!Array.isArray(messages) || messages.length === 0) {
461
+ report.noMessages++;
462
+ return;
463
+ }
464
+ report.checked++;
465
+ report.unknownParts += messagesUnknownParts(messages);
466
+ const sessionID = String(event?.sessionID ?? "");
467
+ const limit = await modelLimitOf(sessionID, messages);
468
+ if (!limit || !Number.isFinite(limit) || limit <= 0) {
469
+ report.noLimit++;
470
+ emit("no_limit", { sessionID });
471
+ return;
472
+ }
473
+ const used = messagesTokens(messages);
474
+ const pct = percentOf(used, limit);
475
+ report.lastPercent = pct;
476
+ report.lastUsed = used;
477
+ report.lastLimit = limit;
478
+ if (pct < config.atPercent) {
479
+ report.below++;
480
+ return;
481
+ }
482
+ const budgetTokens = Math.floor((limit * config.atPercent) / 100);
483
+ const keepTailTokens = Math.floor((limit * config.keepTailPercent) / 100);
484
+ const plan = prunePlan(messages, { budgetTokens, keepTailTokens });
485
+ applyPrunePlan(messages, plan);
486
+ report.prunedMessages += plan.prune.length;
487
+ report.prunedTokens += plan.savedTokens;
488
+ report.skippedProtected += plan.protectedCount;
489
+ emit("prune", {
490
+ sessionID,
491
+ percent: pct,
492
+ used,
493
+ limit,
494
+ pruned: plan.prune.length,
495
+ saved: plan.savedTokens,
496
+ protected: plan.protectedCount,
497
+ unknown: report.unknownParts,
498
+ });
499
+ }
500
+ catch (err) {
501
+ report.threw++;
502
+ emit("threw", { error: String(err?.message ?? err).slice(0, 120) });
503
+ }
504
+ }));
505
+ return { registrations, report };
506
+ }
507
+ //# sourceMappingURL=v2-prune.js.map