mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
refactor(pi-compaction): 改为压力触发的观察遮蔽与空闲阈值整段摘要
原实现对所有工具结果(含 read)在下一次请求即替换为约 4K 的首尾存根, 不看上下文压力,导致模型看不到大文件中段;后台逐条语义摘要又反复改写 历史、打碎 prompt 缓存。参照 Anthropic 上下文编辑、Manus 与 JetBrains 观察遮蔽研究重构为: - L2:上下文达到阈值时在 turn_end 批量提交 Pi 原生 context_edit,把最近 keepRecentTurns 轮之外的旧 toolResult 换成确定性占位符;trigger/target 回差与 minClearTokens 保证少触发、一次清够;原始条目保留,可经 context_tree_query 取回 - L3:空闲 agent_settled 且超过绝对阈值时调用一次 ctx.compact - 阈值按绝对 token 计并受窗口比例约束,默认针对 1M 窗口(DeepSeek V4.1 Flash):遮蔽 200K→100K、整段摘要 400K - 删除逐条存根、语义 worker、soft/hard/target 比例与自行取消 Pi 压缩的门控; Pi 原生阈值压缩保留为最终安全网 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
a254d4fd68
commit
a6d8416c76
@@ -1,7 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DEFAULT_CONFIG } from "../src/config.ts";
|
||||
import { SegmentIndex, deterministicSummary } from "../src/segment-index.ts";
|
||||
import { SegmentIndex } from "../src/segment-index.ts";
|
||||
|
||||
const assistant = {
|
||||
role: "assistant",
|
||||
@@ -17,66 +16,96 @@ const result = {
|
||||
timestamp: 11,
|
||||
};
|
||||
|
||||
test("deterministic summary is bounded and preserves head/tail evidence", () => {
|
||||
const summary = deterministicSummary({
|
||||
toolName: "bash",
|
||||
args: { command: "npm test" },
|
||||
rawText: result.content[0]!.text,
|
||||
isError: true,
|
||||
maxStubTokens: 256,
|
||||
// Regression: a ~14.5K-char read result projected under a 1M window must reach
|
||||
// the model byte-for-byte unchanged — masking only happens via context_edit
|
||||
// drafts under real pressure, never on the hot projection path.
|
||||
test("projection is byte-for-byte identity for a 300-line read result under a 1M window", () => {
|
||||
const readBody = Array.from({ length: 300 }, (_unused, index) => `line ${index + 1}: ${"y".repeat(38)}`).join("\n");
|
||||
const readResult = {
|
||||
role: "toolResult",
|
||||
toolCallId: "call-read",
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: readBody }],
|
||||
timestamp: 12,
|
||||
};
|
||||
assert.ok(readBody.length > 14_000 && readBody.length < 15_000, `fixture ${readBody.length} chars`);
|
||||
|
||||
const index = new SegmentIndex("session-1");
|
||||
index.hydrate();
|
||||
const snapshot = index.project([assistant, readResult], {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 100,
|
||||
contextWindow: 1_000_000,
|
||||
maskedResults: 0,
|
||||
reclaimedTokens: 0,
|
||||
});
|
||||
assert.match(summary, /tool=bash status=error/);
|
||||
assert.match(summary, /failure/);
|
||||
assert.match(summary, /stack tail/);
|
||||
assert.ok(summary.length <= 1_100);
|
||||
|
||||
assert.strictEqual(snapshot.messages[1], readResult);
|
||||
assert.strictEqual(snapshot.messages[1]!.content[0].text, readBody);
|
||||
assert.ok(snapshot.messages[1]!.content[0].text.includes("line 150:"));
|
||||
assert.equal(snapshot.reclaimedTokens, 0);
|
||||
assert.equal(snapshot.maskedResults, 0);
|
||||
});
|
||||
|
||||
test("projection incrementally replaces tool results and caches identical input", () => {
|
||||
const index = new SegmentIndex("session-1");
|
||||
index.hydrate([], DEFAULT_CONFIG);
|
||||
const records = index.captureTurn(assistant, [result], DEFAULT_CONFIG);
|
||||
assert.equal(records.length, 1);
|
||||
|
||||
test("projection caches identical input and handles append-only deltas", () => {
|
||||
const index = new SegmentIndex("session-2");
|
||||
index.hydrate();
|
||||
const messages = [assistant, result];
|
||||
const first = index.project(messages, {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 100,
|
||||
contextWindow: 300_000,
|
||||
});
|
||||
const second = index.project(messages, {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 100,
|
||||
contextWindow: 300_000,
|
||||
});
|
||||
|
||||
const params = { environmentHash: "env", environmentTokens: 100, contextWindow: 300_000, maskedResults: 0, reclaimedTokens: 0 };
|
||||
const first = index.project(messages, params);
|
||||
const second = index.project(messages, params);
|
||||
assert.strictEqual(first, second);
|
||||
assert.ok(first.effectiveTokens < first.rawTokens);
|
||||
assert.match(first.messages[1]!.content[0].text, /context_tree_query/);
|
||||
assert.ok(first.rawTokens > 0);
|
||||
|
||||
const record = records[0]!;
|
||||
assert.ok(index.applySemantic(record.sourceKey, record.sourceHash, "- npm test failed\n- stack tail"));
|
||||
const semantic = index.project(messages, {
|
||||
const appended = index.project([...messages, { role: "user", content: "next", timestamp: 13 }], params);
|
||||
assert.notStrictEqual(appended, first);
|
||||
assert.equal(appended.messages.length, 3);
|
||||
assert.strictEqual(appended.messages[1], result);
|
||||
});
|
||||
|
||||
test("changed masking stats publish a new snapshot without mutating the old one", () => {
|
||||
const index = new SegmentIndex("session-stats");
|
||||
index.hydrate();
|
||||
const params = {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 100,
|
||||
contextWindow: 300_000,
|
||||
});
|
||||
assert.notStrictEqual(semantic, first);
|
||||
assert.match(semantic.messages[1]!.content[0].text, /npm test failed/);
|
||||
maskedResults: 0,
|
||||
reclaimedTokens: 0,
|
||||
};
|
||||
const first = index.project([assistant, result], params);
|
||||
const same = index.project([assistant, result], params);
|
||||
assert.strictEqual(same, first);
|
||||
|
||||
const updated = index.project([assistant, result], { ...params, maskedResults: 1, reclaimedTokens: 500 });
|
||||
assert.notStrictEqual(updated, first);
|
||||
assert.equal(updated.maskedResults, 1);
|
||||
assert.equal(updated.reclaimedTokens, 500);
|
||||
assert.equal(updated.rawTokens, updated.effectiveTokens + 500);
|
||||
// The previously published object stays immutable.
|
||||
assert.equal(first.maskedResults, 0);
|
||||
assert.equal(first.reclaimedTokens, 0);
|
||||
// Unchanged stats keep returning the same cached object.
|
||||
assert.strictEqual(index.project([assistant, result], { ...params, maskedResults: 1, reclaimedTokens: 500 }), updated);
|
||||
});
|
||||
|
||||
test("usage-backed baseline is adjusted only by projected delta", () => {
|
||||
const index = new SegmentIndex("session-2");
|
||||
index.hydrate([], DEFAULT_CONFIG);
|
||||
const index = new SegmentIndex("session-3");
|
||||
index.hydrate();
|
||||
const initial = index.project([assistant], {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 50,
|
||||
contextWindow: 100_000,
|
||||
maskedResults: 0,
|
||||
reclaimedTokens: 0,
|
||||
});
|
||||
index.recordUsage(1_000, initial.messageEstimate, "env");
|
||||
const next = index.project([assistant, { role: "user", content: "next", timestamp: 12 }], {
|
||||
environmentHash: "env",
|
||||
environmentTokens: 50,
|
||||
contextWindow: 100_000,
|
||||
maskedResults: 0,
|
||||
reclaimedTokens: 0,
|
||||
});
|
||||
assert.equal(next.confidence, "usage-backed");
|
||||
assert.ok(next.effectiveTokens >= 1_000);
|
||||
|
||||
Reference in New Issue
Block a user