@te-river/opencode-team-mode 1.8.2 → 1.8.4
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 +499 -1012
- package/README.zh-CN.md +493 -909
- package/dist/host/v2-prune.d.ts +81 -1
- package/dist/host/v2-prune.d.ts.map +1 -1
- package/dist/host/v2-prune.js +303 -18
- package/dist/host/v2-prune.js.map +1 -1
- package/dist/host/v2-retry.d.ts +8 -0
- package/dist/host/v2-retry.d.ts.map +1 -1
- package/dist/host/v2-retry.js +8 -2
- package/dist/host/v2-retry.js.map +1 -1
- package/dist/host/v2-session.d.ts +8 -0
- package/dist/host/v2-session.d.ts.map +1 -1
- package/dist/host/v2-session.js +18 -5
- package/dist/host/v2-session.js.map +1 -1
- package/dist/host/v2-state.d.ts +165 -0
- package/dist/host/v2-state.d.ts.map +1 -0
- package/dist/host/v2-state.js +448 -0
- package/dist/host/v2-state.js.map +1 -0
- package/dist/host/v2-tail.d.ts +102 -0
- package/dist/host/v2-tail.d.ts.map +1 -0
- package/dist/host/v2-tail.js +198 -0
- package/dist/host/v2-tail.js.map +1 -0
- package/dist/host/v2-types.d.ts +2 -0
- package/dist/host/v2-types.d.ts.map +1 -1
- package/dist/host/v2-usage.d.ts +72 -2
- package/dist/host/v2-usage.d.ts.map +1 -1
- package/dist/host/v2-usage.js +142 -9
- package/dist/host/v2-usage.js.map +1 -1
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +187 -3
- package/dist/host/v2.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 +12 -0
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/shared.d.ts +1 -1
- package/dist/prompts/shared.d.ts.map +1 -1
- package/dist/prompts/shared.js +5 -0
- package/dist/prompts/shared.js.map +1 -1
- package/dist/tm/config-layers.d.ts.map +1 -1
- package/dist/tm/config-layers.js +4 -0
- package/dist/tm/config-layers.js.map +1 -1
- package/dist/tm/config.d.ts +8 -0
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/dispatch.d.ts +17 -0
- package/dist/tm/dispatch.d.ts.map +1 -1
- package/dist/tm/dispatch.js +52 -7
- package/dist/tm/dispatch.js.map +1 -1
- package/dist/tm/pipelines.d.ts +9 -0
- package/dist/tm/pipelines.d.ts.map +1 -1
- package/dist/tm/pipelines.js +1 -1
- package/dist/tm/pipelines.js.map +1 -1
- package/dist/tm/stats.d.ts +11 -0
- package/dist/tm/stats.d.ts.map +1 -1
- package/dist/tm/stats.js +34 -1
- package/dist/tm/stats.js.map +1 -1
- package/docs/v2-port-notes.md +7 -3
- package/package.json +1 -1
package/dist/host/v2-prune.d.ts
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* protected STRING: the message may be pruned, but the stub must carry the
|
|
22
22
|
* handle verbatim, which is exactly the "keep the pointer, drop the body"
|
|
23
23
|
* contract.
|
|
24
|
+
* - **Nothing leaves that cannot be recovered.** A message carrying no handle
|
|
25
|
+
* of its own is offloaded to the run store BEFORE its body is replaced, so
|
|
26
|
+
* the stub carries a full `tm_fetch` handle (`pruneOffload`, default on). A
|
|
27
|
+
* failed offload falls back to today's echo note, one message at a time —
|
|
28
|
+
* losing a body is data loss, losing an offload is a counter. A stub with no
|
|
29
|
+
* handle to offer still carries the ADDRESSES the body named (board paths,
|
|
30
|
+
* `file:line`, URLs; at most three, deduplicated, in document order), because a
|
|
31
|
+
* pointer the reader can act on is the whole point of keeping one — and that
|
|
32
|
+
* scan is SINGLE-PASS and linear in the body length, not three global regexes
|
|
33
|
+
* over the whole text (which went quadratic on a long word run).
|
|
34
|
+
* - **A user pattern outranks the budget** (`pruneProtectPatterns`): a message
|
|
35
|
+
* whose text matches any compiled entry is never pruned, and the matches are
|
|
36
|
+
* counted (`protectMatched`) instead of silently changing the saving figure.
|
|
24
37
|
* - **The threshold is derived, never hard-coded.** `TM_PRUNE_AT_PERCENT`
|
|
25
38
|
* (default 70) of `limit.context`, the same denominator the desktop's own
|
|
26
39
|
* usage meter reads. No `limit.context` → no prune, counted `no_limit`.
|
|
@@ -40,11 +53,24 @@ export interface PruneConfig {
|
|
|
40
53
|
atPercent: number;
|
|
41
54
|
/** Percent of `limit.context` kept verbatim at the tail (clamped 0–90). */
|
|
42
55
|
keepTailPercent: number;
|
|
56
|
+
/**
|
|
57
|
+
* User-supplied protection regexes (`pruneProtectPatterns`, comma separated).
|
|
58
|
+
* A message whose text matches ANY of them is never pruned. Empty = off, and
|
|
59
|
+
* that is the byte-identical path: the list is consulted only when non-empty.
|
|
60
|
+
*/
|
|
61
|
+
protectPatterns: RegExp[];
|
|
43
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Compile the comma-separated pattern list. One invalid pattern loses only
|
|
65
|
+
* itself (a typo must not disable the ones next to it, and must never throw into
|
|
66
|
+
* the request path); an empty / absent value compiles to `[]` = off.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseProtectPatterns(raw: unknown): RegExp[];
|
|
44
69
|
export declare function resolvePruneConfig(config?: {
|
|
45
70
|
prune: string;
|
|
46
71
|
pruneAtPercent: number;
|
|
47
72
|
pruneKeepTailPercent: number;
|
|
73
|
+
pruneProtectPatterns?: string;
|
|
48
74
|
}): PruneConfig;
|
|
49
75
|
interface TextSlot {
|
|
50
76
|
container: Record<string, unknown>;
|
|
@@ -86,13 +112,43 @@ export interface PruneHandle {
|
|
|
86
112
|
ref: string;
|
|
87
113
|
accessToken: string;
|
|
88
114
|
expireAt: number;
|
|
115
|
+
/** Set when the wiring recognised this body and handed back an EXISTING handle. */
|
|
116
|
+
reused?: boolean;
|
|
89
117
|
}
|
|
118
|
+
/** 地址行最多留几条——再多就不是「地址」,而是正文摘抄。 */
|
|
119
|
+
export declare const PRUNE_ADDRESS_MAX = 3;
|
|
120
|
+
/** 地址行的前缀;整行只在真的抽到地址时出现。 */
|
|
121
|
+
export declare const PRUNE_ADDRESS_PREFIX = "\u5730\u5740\uFF1A";
|
|
122
|
+
/**
|
|
123
|
+
* 单个 token 的字符上限。 超过它的不是地址,是正文——这条上限也是性能闸门:
|
|
124
|
+
* 长词串整段跳过,永远不会进到正则里。
|
|
125
|
+
*/
|
|
126
|
+
export declare const PRUNE_ADDRESS_TOKEN_MAX = 2048;
|
|
127
|
+
/**
|
|
128
|
+
* The addresses a pruned body carried, in DOCUMENT ORDER, deduplicated, at most
|
|
129
|
+
* `PRUNE_ADDRESS_MAX`. Pure, and it only ever yields the address itself — never a
|
|
130
|
+
* fragment of the body around it, because the stub is a pointer, not a summary.
|
|
131
|
+
*
|
|
132
|
+
* LINEAR BY CONSTRUCTION (this is a performance contract, not a style note): the
|
|
133
|
+
* text is walked ONCE, token by token, and every pattern is anchored to a single
|
|
134
|
+
* token. The previous version ran three GLOBAL regexes over the whole body and
|
|
135
|
+
* blanked each URL match by rebuilding the string — quadratic on a long word run
|
|
136
|
+
* (`"a".repeat(200000)` was hundreds of seconds) and O(k·n) with many URLs. A
|
|
137
|
+
* token longer than `PRUNE_ADDRESS_TOKEN_MAX` is skipped without touching a regex.
|
|
138
|
+
*
|
|
139
|
+
* A longer address that CONTAINS a shorter one wins: `…/v2-prune.ts:120` is the
|
|
140
|
+
* pointer a reader can use, while `…/v2-prune.ts` would send them hunting for the
|
|
141
|
+
* line the message was about.
|
|
142
|
+
*/
|
|
143
|
+
export declare function extractAddresses(text: string): string[];
|
|
90
144
|
export interface PruneStubInput {
|
|
91
|
-
kind: "handle" | "child" | "echo";
|
|
145
|
+
kind: "handle" | "child" | "echo" | "offload";
|
|
92
146
|
originalTokens: number;
|
|
93
147
|
handle?: PruneHandle;
|
|
94
148
|
childId?: string;
|
|
95
149
|
childState?: string;
|
|
150
|
+
/** Addresses found in the body — rendered as one `地址:` line, or omitted whole. */
|
|
151
|
+
addresses?: string[];
|
|
96
152
|
}
|
|
97
153
|
/**
|
|
98
154
|
* The replacement text. It must be self-explanatory: a reader has to know what
|
|
@@ -109,6 +165,8 @@ export interface PrunePlanEntry {
|
|
|
109
165
|
export interface PrunePlan {
|
|
110
166
|
prune: PrunePlanEntry[];
|
|
111
167
|
protectedCount: number;
|
|
168
|
+
/** Messages a user-supplied `pruneProtectPatterns` entry kept (a subset of the kept ones). */
|
|
169
|
+
protectMatched: number;
|
|
112
170
|
keptCount: number;
|
|
113
171
|
savedTokens: number;
|
|
114
172
|
}
|
|
@@ -117,6 +175,8 @@ export interface PrunePlanOptions {
|
|
|
117
175
|
budgetTokens?: number;
|
|
118
176
|
/** Newest messages whose cumulative tokens fit here are kept verbatim. */
|
|
119
177
|
keepTailTokens?: number;
|
|
178
|
+
/** User patterns: any hit keeps that message, whatever the budget says. */
|
|
179
|
+
protectPatterns?: RegExp[];
|
|
120
180
|
}
|
|
121
181
|
/**
|
|
122
182
|
* Decide which messages to prune, oldest first, until the request fits the
|
|
@@ -138,8 +198,22 @@ export interface PruneReport {
|
|
|
138
198
|
noMessages: number;
|
|
139
199
|
foreignSkipped: number;
|
|
140
200
|
threw: number;
|
|
201
|
+
/** Bodies stored in the run store before their message was replaced. */
|
|
202
|
+
offloaded: number;
|
|
203
|
+
/** Handle reused for a body an earlier request already stored (no second write). */
|
|
204
|
+
reused: number;
|
|
205
|
+
/** Messages whose offload failed (or returned no handle) → echo stub. */
|
|
206
|
+
offloadFailed: number;
|
|
207
|
+
/** Requests whose post-prune size still exceeds the budget (counted, never re-pruned). */
|
|
208
|
+
overBudget: number;
|
|
209
|
+
/** Bodies stored for a request whose plan never landed — payloads with no pointer. */
|
|
210
|
+
orphans: number;
|
|
141
211
|
/** Parts whose `type` we did not recognise — a renamed shape shows up here. */
|
|
142
212
|
unknownParts: number;
|
|
213
|
+
/** Compiled `pruneProtectPatterns` entries (a value that failed to parse is not counted here). */
|
|
214
|
+
protectPatterns: number;
|
|
215
|
+
/** Messages a protect pattern kept out of the plan this process. */
|
|
216
|
+
protectMatched: number;
|
|
143
217
|
lastPercent: number;
|
|
144
218
|
lastUsed: number;
|
|
145
219
|
lastLimit: number;
|
|
@@ -151,6 +225,12 @@ export interface PruneInput {
|
|
|
151
225
|
modelLimitOf?: (sessionID: string, messages: unknown[]) => Promise<number | null> | number | null;
|
|
152
226
|
/** Per-decision trajectory line (counters alone are invisible in a CLI run). */
|
|
153
227
|
onEvent?: (row: Record<string, unknown>) => void;
|
|
228
|
+
/**
|
|
229
|
+
* Store one message body and hand back its handle. Absent = today's
|
|
230
|
+
* behaviour (echo stub, body not kept) — the wiring passes it only while
|
|
231
|
+
* `pruneOffload` is on.
|
|
232
|
+
*/
|
|
233
|
+
offload?: (text: string) => Promise<PruneHandle | null>;
|
|
154
234
|
}
|
|
155
235
|
export interface PruneLayer {
|
|
156
236
|
registrations: V2Registration[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v2-prune.d.ts","sourceRoot":"","sources":["../../src/host/v2-prune.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"v2-prune.d.ts","sourceRoot":"","sources":["../../src/host/v2-prune.ts"],"names":[],"mappings":"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"}
|
package/dist/host/v2-prune.js
CHANGED
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* protected STRING: the message may be pruned, but the stub must carry the
|
|
22
22
|
* handle verbatim, which is exactly the "keep the pointer, drop the body"
|
|
23
23
|
* contract.
|
|
24
|
+
* - **Nothing leaves that cannot be recovered.** A message carrying no handle
|
|
25
|
+
* of its own is offloaded to the run store BEFORE its body is replaced, so
|
|
26
|
+
* the stub carries a full `tm_fetch` handle (`pruneOffload`, default on). A
|
|
27
|
+
* failed offload falls back to today's echo note, one message at a time —
|
|
28
|
+
* losing a body is data loss, losing an offload is a counter. A stub with no
|
|
29
|
+
* handle to offer still carries the ADDRESSES the body named (board paths,
|
|
30
|
+
* `file:line`, URLs; at most three, deduplicated, in document order), because a
|
|
31
|
+
* pointer the reader can act on is the whole point of keeping one — and that
|
|
32
|
+
* scan is SINGLE-PASS and linear in the body length, not three global regexes
|
|
33
|
+
* over the whole text (which went quadratic on a long word run).
|
|
34
|
+
* - **A user pattern outranks the budget** (`pruneProtectPatterns`): a message
|
|
35
|
+
* whose text matches any compiled entry is never pruned, and the matches are
|
|
36
|
+
* counted (`protectMatched`) instead of silently changing the saving figure.
|
|
24
37
|
* - **The threshold is derived, never hard-coded.** `TM_PRUNE_AT_PERCENT`
|
|
25
38
|
* (default 70) of `limit.context`, the same denominator the desktop's own
|
|
26
39
|
* usage meter reads. No `limit.context` → no prune, counted `no_limit`.
|
|
@@ -40,6 +53,28 @@ const CHILD_ID_RE = /ses_[A-Za-z0-9]+/;
|
|
|
40
53
|
const SETTLED_STATE_RE = /state\s*=\s*"(?:completed|error)"/i;
|
|
41
54
|
const RUNNING_STATE_RE = /state\s*=\s*"(?:running|pending|in[_-]?progress)"/i;
|
|
42
55
|
const HANDLE_RE = /ref\s*:\s*"([^"]+)"[\s\S]*?access_token\s*:\s*"([^"]+)"[\s\S]*?expire_at\s*:\s*(\d+)/;
|
|
56
|
+
/**
|
|
57
|
+
* Compile the comma-separated pattern list. One invalid pattern loses only
|
|
58
|
+
* itself (a typo must not disable the ones next to it, and must never throw into
|
|
59
|
+
* the request path); an empty / absent value compiles to `[]` = off.
|
|
60
|
+
*/
|
|
61
|
+
export function parseProtectPatterns(raw) {
|
|
62
|
+
if (typeof raw !== "string" || !raw.trim())
|
|
63
|
+
return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const part of raw.split(",")) {
|
|
66
|
+
const src = part.trim();
|
|
67
|
+
if (!src)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
out.push(new RegExp(src));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* an invalid regex is ignored, never fatal */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
43
78
|
const isOff = (v) => typeof v === "string" && ["off", "false", "0", "no"].includes(v.trim().toLowerCase());
|
|
44
79
|
export function resolvePruneConfig(config = resolveConfig()) {
|
|
45
80
|
return {
|
|
@@ -49,6 +84,7 @@ export function resolvePruneConfig(config = resolveConfig()) {
|
|
|
49
84
|
// prune is looking at the clamp, not at a broken layer.
|
|
50
85
|
atPercent: config.pruneAtPercent,
|
|
51
86
|
keepTailPercent: config.pruneKeepTailPercent,
|
|
87
|
+
protectPatterns: parseProtectPatterns(config.pruneProtectPatterns),
|
|
52
88
|
};
|
|
53
89
|
}
|
|
54
90
|
/**
|
|
@@ -243,7 +279,142 @@ export function isProtectedMessage(msg) {
|
|
|
243
279
|
return true;
|
|
244
280
|
return false;
|
|
245
281
|
}
|
|
282
|
+
/** 地址行最多留几条——再多就不是「地址」,而是正文摘抄。 */
|
|
283
|
+
export const PRUNE_ADDRESS_MAX = 3;
|
|
284
|
+
/** 地址行的前缀;整行只在真的抽到地址时出现。 */
|
|
285
|
+
export const PRUNE_ADDRESS_PREFIX = "地址:";
|
|
286
|
+
/**
|
|
287
|
+
* 单个 token 的字符上限。 超过它的不是地址,是正文——这条上限也是性能闸门:
|
|
288
|
+
* 长词串整段跳过,永远不会进到正则里。
|
|
289
|
+
*/
|
|
290
|
+
export const PRUNE_ADDRESS_TOKEN_MAX = 2048;
|
|
291
|
+
/**
|
|
292
|
+
* Token separators. `<` / `>` are deliberately NOT separators: the board note
|
|
293
|
+
* publishes `<board-root>/…md`, and splitting on the angle brackets would strip
|
|
294
|
+
* the placeholder off the address it belongs to.
|
|
295
|
+
*/
|
|
296
|
+
const ADDRESS_SEPARATORS = new Set([" ", "\t", "\n", "\r", "\f", "\v", '"', "'", "|", "`", ";", ",", "(", ")"]);
|
|
297
|
+
/** A cheap anchored pre-filter: only these suffixes can be a path address. */
|
|
298
|
+
const PATH_EXT_RE = /\.(?:md|json|jsonl|txt|log|ts|tsx|js|py|yml|yaml)$/;
|
|
299
|
+
/** `file:line` / `file:line:col`, ANCHORED inside one token (no per-start retry). */
|
|
300
|
+
const FILE_LINE_TOKEN_RE = /^(?:[A-Za-z]:)?[\w./\\-]+:\d+(?::\d+)?$/;
|
|
301
|
+
/**
|
|
302
|
+
* A path-shaped token with a KNOWN extension: `C:\…\01-team-report.md`,
|
|
303
|
+
* `/x/y/z.json`, `<board-root>/…md`, `src/host/v2-prune.ts`. The leading run is
|
|
304
|
+
* optional (drive letter, an angle-bracket placeholder, a relative first
|
|
305
|
+
* segment); every further segment must start with a separator, which is what
|
|
306
|
+
* keeps a bare `v2-prune.ts` from being called an address. BOTH ENDS ANCHORED:
|
|
307
|
+
* the engine never re-tries every start position, which is the whole point of
|
|
308
|
+
* the token pass.
|
|
309
|
+
*/
|
|
310
|
+
const PATH_TOKEN_RE = /^(?:[A-Za-z]:|<[^>\s]+>|[^\s"'<>|`;,():\/\\]+)?(?:[\/\\][^\s"'<>|`;,():\/\\]+)+\.(?:md|json|jsonl|txt|log|ts|tsx|js|py|yml|yaml)$/;
|
|
311
|
+
/** Strip the `<`/`>` a prose writer may have wrapped an address in. */
|
|
312
|
+
const trimAngles = (s) => s.replace(/^</, "").replace(/>+$/, "");
|
|
313
|
+
/**
|
|
314
|
+
* The first `http(s)://` inside a token, or -1. `indexOf` is linear, and it is
|
|
315
|
+
* how an address glued to Chinese text (`看https://…`) is still recovered —
|
|
316
|
+
* without a cross-text regex that would try every start position.
|
|
317
|
+
*/
|
|
318
|
+
function urlAt(token) {
|
|
319
|
+
const a = token.indexOf("http://");
|
|
320
|
+
const b = token.indexOf("https://");
|
|
321
|
+
if (a < 0)
|
|
322
|
+
return b;
|
|
323
|
+
if (b < 0)
|
|
324
|
+
return a;
|
|
325
|
+
return Math.min(a, b);
|
|
326
|
+
}
|
|
327
|
+
/** Retry a token from its first path separator: `见:/a/one.md` → `/a/one.md`. */
|
|
328
|
+
function tailFromSeparator(token) {
|
|
329
|
+
const a = token.indexOf("/");
|
|
330
|
+
const b = token.indexOf("\\");
|
|
331
|
+
const cut = a < 0 ? b : b < 0 ? a : Math.min(a, b);
|
|
332
|
+
return cut > 0 ? token.slice(cut) : null;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The addresses a pruned body carried, in DOCUMENT ORDER, deduplicated, at most
|
|
336
|
+
* `PRUNE_ADDRESS_MAX`. Pure, and it only ever yields the address itself — never a
|
|
337
|
+
* fragment of the body around it, because the stub is a pointer, not a summary.
|
|
338
|
+
*
|
|
339
|
+
* LINEAR BY CONSTRUCTION (this is a performance contract, not a style note): the
|
|
340
|
+
* text is walked ONCE, token by token, and every pattern is anchored to a single
|
|
341
|
+
* token. The previous version ran three GLOBAL regexes over the whole body and
|
|
342
|
+
* blanked each URL match by rebuilding the string — quadratic on a long word run
|
|
343
|
+
* (`"a".repeat(200000)` was hundreds of seconds) and O(k·n) with many URLs. A
|
|
344
|
+
* token longer than `PRUNE_ADDRESS_TOKEN_MAX` is skipped without touching a regex.
|
|
345
|
+
*
|
|
346
|
+
* A longer address that CONTAINS a shorter one wins: `…/v2-prune.ts:120` is the
|
|
347
|
+
* pointer a reader can use, while `…/v2-prune.ts` would send them hunting for the
|
|
348
|
+
* line the message was about.
|
|
349
|
+
*/
|
|
350
|
+
export function extractAddresses(text) {
|
|
351
|
+
if (!text)
|
|
352
|
+
return [];
|
|
353
|
+
const found = [];
|
|
354
|
+
const n = text.length;
|
|
355
|
+
let i = 0;
|
|
356
|
+
while (i < n) {
|
|
357
|
+
while (i < n && ADDRESS_SEPARATORS.has(text[i]))
|
|
358
|
+
i++;
|
|
359
|
+
if (i >= n)
|
|
360
|
+
break;
|
|
361
|
+
const start = i;
|
|
362
|
+
while (i < n && !ADDRESS_SEPARATORS.has(text[i]))
|
|
363
|
+
i++;
|
|
364
|
+
const token = text.slice(start, i);
|
|
365
|
+
if (token.length > PRUNE_ADDRESS_TOKEN_MAX)
|
|
366
|
+
continue;
|
|
367
|
+
// URL first: its own path must never be counted as a second address.
|
|
368
|
+
const scheme = urlAt(token);
|
|
369
|
+
if (scheme >= 0) {
|
|
370
|
+
const address = trimAngles(token.slice(scheme));
|
|
371
|
+
if (address.length > 8)
|
|
372
|
+
found.push({ at: start + scheme, address });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
// `file:line` — a prefix with no separator is a word (`expire_at:9`), not a path.
|
|
376
|
+
if (token.includes(":") && /\d$/.test(token)) {
|
|
377
|
+
const direct = FILE_LINE_TOKEN_RE.test(token) ? token : null;
|
|
378
|
+
const candidate = direct ?? tailFromSeparator(token);
|
|
379
|
+
if (candidate && FILE_LINE_TOKEN_RE.test(candidate)) {
|
|
380
|
+
const cut = candidate.lastIndexOf(":");
|
|
381
|
+
if (/[\/\\]/.test(candidate.slice(0, cut))) {
|
|
382
|
+
found.push({ at: start, address: candidate });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (PATH_EXT_RE.test(token)) {
|
|
388
|
+
const direct = PATH_TOKEN_RE.test(token) ? token : null;
|
|
389
|
+
const tail = direct ? null : tailFromSeparator(token);
|
|
390
|
+
const candidate = direct ?? (tail && PATH_EXT_RE.test(tail) && PATH_TOKEN_RE.test(tail) ? tail : null);
|
|
391
|
+
if (candidate)
|
|
392
|
+
found.push({ at: start, address: candidate });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Tokens are already in document order; the sort stays because an embedded URL
|
|
396
|
+
// reports an offset inside its token, and "document order" is what the reader
|
|
397
|
+
// expects either way.
|
|
398
|
+
found.sort((a, b) => a.at - b.at);
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const { address } of found) {
|
|
401
|
+
if (out.some((kept) => kept.includes(address)))
|
|
402
|
+
continue;
|
|
403
|
+
if (found.some((o) => o.address !== address && o.address.includes(address)))
|
|
404
|
+
continue;
|
|
405
|
+
out.push(address);
|
|
406
|
+
if (out.length >= PRUNE_ADDRESS_MAX)
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
246
411
|
const withCommas = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
412
|
+
/** One trailing line carrying the recoverable addresses; "" when none were found. */
|
|
413
|
+
function addressLine(addresses) {
|
|
414
|
+
if (!addresses || !addresses.length)
|
|
415
|
+
return "";
|
|
416
|
+
return `\n${PRUNE_ADDRESS_PREFIX}${addresses.join(" · ")}`;
|
|
417
|
+
}
|
|
247
418
|
/**
|
|
248
419
|
* The replacement text. It must be self-explanatory: a reader has to know what
|
|
249
420
|
* used to be here and where to get it back. The handle / child id is copied
|
|
@@ -257,7 +428,15 @@ export function renderPruneStub(entry) {
|
|
|
257
428
|
if (entry.kind === "child" && entry.childId) {
|
|
258
429
|
return `${head} 子代理 ${entry.childId} 的报告已结算(state=${entry.childState ?? "completed"});需要原文用 tm_join { ids:["${entry.childId}"] } 取回。`;
|
|
259
430
|
}
|
|
260
|
-
|
|
431
|
+
if (entry.kind === "offload" && entry.handle) {
|
|
432
|
+
// Deliberately different wording from the tool-result stub above: this body
|
|
433
|
+
// had NO handle until the prune created one, so "re-run the command" would
|
|
434
|
+
// be wrong advice — the bytes are in the store, not behind a command.
|
|
435
|
+
return `${head} 本条消息已卸载:tm_fetch { ref:"${entry.handle.ref}", access_token:"${entry.handle.accessToken}", expire_at:${entry.handle.expireAt} }。原文可回取(TTL 由 blackboardTtlDays 决定),不要重跑。${addressLine(entry.addresses)}`;
|
|
436
|
+
}
|
|
437
|
+
// The echo stub is the one that keeps NO copy of the body, so the addresses it
|
|
438
|
+
// reports are the only thing the next round can still act on.
|
|
439
|
+
return `${head} 已结算的工具输出/命令回显,正文已裁剪;如需重看请重跑原命令,或从轨迹(tm_stats { recent: N })取回。${addressLine(entry.addresses)}`;
|
|
261
440
|
}
|
|
262
441
|
function extractHandle(text) {
|
|
263
442
|
const m = HANDLE_RE.exec(text);
|
|
@@ -279,18 +458,21 @@ function buildStub(text, tokens) {
|
|
|
279
458
|
const child = extractChild(text);
|
|
280
459
|
if (child)
|
|
281
460
|
return { kind: "child", stub: renderPruneStub({ kind: "child", originalTokens: tokens, childId: child.id, childState: child.state }) };
|
|
282
|
-
|
|
461
|
+
// Only the branches that carry no recoverable pointer get the address line: a
|
|
462
|
+
// handle already says where the bytes are, and a child id where the report is.
|
|
463
|
+
return { kind: "echo", stub: renderPruneStub({ kind: "echo", originalTokens: tokens, addresses: extractAddresses(text) }) };
|
|
283
464
|
}
|
|
284
465
|
/**
|
|
285
466
|
* Decide which messages to prune, oldest first, until the request fits the
|
|
286
467
|
* budget. Pure: it reads the messages and returns a plan; nothing is mutated.
|
|
287
468
|
*/
|
|
288
469
|
export function prunePlan(messages, opts = {}) {
|
|
289
|
-
const plan = { prune: [], protectedCount: 0, keptCount: 0, savedTokens: 0 };
|
|
470
|
+
const plan = { prune: [], protectedCount: 0, protectMatched: 0, keptCount: 0, savedTokens: 0 };
|
|
290
471
|
if (!Array.isArray(messages) || messages.length === 0)
|
|
291
472
|
return plan;
|
|
292
473
|
const keepTailTokens = Math.max(0, opts.keepTailTokens ?? 0);
|
|
293
474
|
const budgetTokens = opts.budgetTokens ?? Number.POSITIVE_INFINITY;
|
|
475
|
+
const protectPatterns = opts.protectPatterns ?? [];
|
|
294
476
|
// The tail boundary: walk from the newest message until the cumulative token
|
|
295
477
|
// count would exceed the tail budget. The newest message is ALWAYS protected.
|
|
296
478
|
let acc = 0;
|
|
@@ -307,6 +489,13 @@ export function prunePlan(messages, opts = {}) {
|
|
|
307
489
|
let saved = 0;
|
|
308
490
|
for (let i = 0; i < tailStart; i++) {
|
|
309
491
|
const msg = messages[i];
|
|
492
|
+
if (protectPatterns.length) {
|
|
493
|
+
const body = messageText(msg);
|
|
494
|
+
if (body && protectPatterns.some((re) => re.test(body))) {
|
|
495
|
+
plan.protectMatched++;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
310
499
|
if (isProtectedMessage(msg)) {
|
|
311
500
|
plan.protectedCount++;
|
|
312
501
|
continue;
|
|
@@ -342,6 +531,63 @@ export function applyPrunePlan(messages, plan) {
|
|
|
342
531
|
slots[i].container[slots[i].key] = "";
|
|
343
532
|
}
|
|
344
533
|
}
|
|
534
|
+
/**
|
|
535
|
+
* Offload a planned body BEFORE its message is rewritten, so the stub can carry
|
|
536
|
+
* a real `tm_fetch` handle. Only `echo` entries are offloaded: a `handle` entry
|
|
537
|
+
* already points at a payload (offloading it again would duplicate the bytes) and
|
|
538
|
+
* a `child` entry points at a session (its body lives in the child, not here).
|
|
539
|
+
*
|
|
540
|
+
* Failure is BOUNDED and per message: one throw degrades that one stub to today's
|
|
541
|
+
* echo note and increments a counter — the remaining plan still runs, and the
|
|
542
|
+
* caller's hook never sees an exception. `savedTokens` is recomputed afterwards
|
|
543
|
+
* because a handle stub is longer than an echo note, and a reported saving that
|
|
544
|
+
* did not happen is exactly the kind of claim this product refuses.
|
|
545
|
+
*/
|
|
546
|
+
async function offloadPlannedBodies(messages, plan, offload, onFailure, report, sessionID) {
|
|
547
|
+
if (!offload || plan.prune.length === 0)
|
|
548
|
+
return;
|
|
549
|
+
let saved = 0;
|
|
550
|
+
for (const entry of plan.prune) {
|
|
551
|
+
const text = entry.kind === "echo" ? messageText(messages[entry.index]) : "";
|
|
552
|
+
if (entry.kind === "echo" && text) {
|
|
553
|
+
let handle = null;
|
|
554
|
+
let failure = null;
|
|
555
|
+
try {
|
|
556
|
+
handle = await offload(text);
|
|
557
|
+
if (!handle || !handle.ref || !handle.accessToken || !Number.isFinite(handle.expireAt)) {
|
|
558
|
+
failure = { reason: handle ? "invalid" : "empty" };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
failure = { reason: "threw", error: String(err?.message ?? err).slice(0, 120) };
|
|
563
|
+
}
|
|
564
|
+
if (handle && !failure) {
|
|
565
|
+
entry.kind = "offload";
|
|
566
|
+
entry.stub = renderPruneStub({
|
|
567
|
+
kind: "offload",
|
|
568
|
+
originalTokens: entry.originalTokens,
|
|
569
|
+
handle,
|
|
570
|
+
// The offload stub points at bytes a reader may not want to page
|
|
571
|
+
// through; the addresses are what they can act on in one round.
|
|
572
|
+
addresses: extractAddresses(text),
|
|
573
|
+
});
|
|
574
|
+
// A dedupe hit means these bytes are ALREADY in the store — counting it as
|
|
575
|
+
// a fresh offload would report a saving that was not made, and would make
|
|
576
|
+
// the card's "上轮修剪" line grow on a replayed request.
|
|
577
|
+
if (handle.reused)
|
|
578
|
+
report.reused++;
|
|
579
|
+
else
|
|
580
|
+
report.offloaded++;
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
report.offloadFailed++;
|
|
584
|
+
onFailure({ sessionID, index: entry.index, ...(failure ?? {}) });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
saved += Math.max(0, entry.originalTokens - estimateTokens(entry.stub));
|
|
588
|
+
}
|
|
589
|
+
plan.savedTokens = saved;
|
|
590
|
+
}
|
|
345
591
|
/** The default denominator: `ctx.model.list()` keyed by the session's model,
|
|
346
592
|
* the same lookup the desktop's usage meter and this package's usage readers use. */
|
|
347
593
|
function defaultModelLimitOf(ctx) {
|
|
@@ -409,6 +655,9 @@ function defaultModelLimitOf(ctx) {
|
|
|
409
655
|
}
|
|
410
656
|
export async function applyV2ContextPrune(ctx, input = {}) {
|
|
411
657
|
const config = input.config ?? resolvePruneConfig();
|
|
658
|
+
// A hand-built config (a test, an embedder) may predate the pattern list; `[]` is
|
|
659
|
+
// the "off" path, so tolerating it changes no behaviour and throws nothing.
|
|
660
|
+
const protectPatterns = config.protectPatterns ?? [];
|
|
412
661
|
const report = {
|
|
413
662
|
enabled: config.enabled,
|
|
414
663
|
atPercent: config.atPercent,
|
|
@@ -422,7 +671,14 @@ export async function applyV2ContextPrune(ctx, input = {}) {
|
|
|
422
671
|
noMessages: 0,
|
|
423
672
|
foreignSkipped: 0,
|
|
424
673
|
threw: 0,
|
|
674
|
+
offloaded: 0,
|
|
675
|
+
reused: 0,
|
|
676
|
+
offloadFailed: 0,
|
|
677
|
+
overBudget: 0,
|
|
678
|
+
orphans: 0,
|
|
425
679
|
unknownParts: 0,
|
|
680
|
+
protectPatterns: protectPatterns.length,
|
|
681
|
+
protectMatched: 0,
|
|
426
682
|
lastPercent: 0,
|
|
427
683
|
lastUsed: 0,
|
|
428
684
|
lastLimit: 0,
|
|
@@ -475,21 +731,50 @@ export async function applyV2ContextPrune(ctx, input = {}) {
|
|
|
475
731
|
}
|
|
476
732
|
const budgetTokens = Math.floor((limit * config.atPercent) / 100);
|
|
477
733
|
const keepTailTokens = Math.floor((limit * config.keepTailPercent) / 100);
|
|
478
|
-
const plan = prunePlan(messages, { budgetTokens, keepTailTokens });
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
734
|
+
const plan = prunePlan(messages, { budgetTokens, keepTailTokens, protectPatterns });
|
|
735
|
+
const freshBefore = report.offloaded;
|
|
736
|
+
await offloadPlannedBodies(messages, plan, input.offload, (row) => emit("prune_offload_failed", row), report, sessionID);
|
|
737
|
+
try {
|
|
738
|
+
applyPrunePlan(messages, plan);
|
|
739
|
+
// A handle stub is longer than the echo note it replaced, so the plan's
|
|
740
|
+
// own arithmetic can land ABOVE the budget. Counted, never silently
|
|
741
|
+
// re-pruned: a second pass would have to offload again to be lossless.
|
|
742
|
+
const overBudget = messagesTokens(messages) > budgetTokens;
|
|
743
|
+
if (overBudget)
|
|
744
|
+
report.overBudget++;
|
|
745
|
+
report.prunedMessages += plan.prune.length;
|
|
746
|
+
report.prunedTokens += plan.savedTokens;
|
|
747
|
+
report.skippedProtected += plan.protectedCount;
|
|
748
|
+
report.protectMatched += plan.protectMatched;
|
|
749
|
+
emit("prune", {
|
|
750
|
+
sessionID,
|
|
751
|
+
percent: pct,
|
|
752
|
+
used,
|
|
753
|
+
limit,
|
|
754
|
+
pruned: plan.prune.length,
|
|
755
|
+
saved: plan.savedTokens,
|
|
756
|
+
protected: plan.protectedCount,
|
|
757
|
+
protect_matched: plan.protectMatched,
|
|
758
|
+
protect_patterns: protectPatterns.length,
|
|
759
|
+
unknown: report.unknownParts,
|
|
760
|
+
over_budget: overBudget,
|
|
761
|
+
reused: report.reused,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
// Bodies stored for THIS request but never pointed at by a message are
|
|
766
|
+
// orphans: counted, and named on their own trajectory row — a payload
|
|
767
|
+
// nobody can reach is otherwise invisible until the TTL sweep.
|
|
768
|
+
// R4: only the FRESH writes of this request count. A reused handle points
|
|
769
|
+
// at a payload an earlier request already stored, so calling it an orphan
|
|
770
|
+
// would invent garbage — and deleting nothing would be the fix anyway.
|
|
771
|
+
const freshThisRequest = Math.max(0, report.offloaded - freshBefore);
|
|
772
|
+
if (freshThisRequest > 0) {
|
|
773
|
+
report.orphans += freshThisRequest;
|
|
774
|
+
emit("prune_offload_failed", { sessionID, reason: "orphaned", orphan: freshThisRequest });
|
|
775
|
+
}
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
493
778
|
}
|
|
494
779
|
catch (err) {
|
|
495
780
|
report.threw++;
|