pi-ui-extend 1.0.41 → 1.0.45
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 +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { planProspectiveTestOutputDelivery, parseTestBuildOutput } from "../context-gateway/test-output-parser.js";
|
|
4
|
+
import { classifyShellCommand, shellCommandText } from "../shell-command-policy.js";
|
|
5
|
+
import type { DcpConfig } from "./config.js";
|
|
6
|
+
import {
|
|
7
|
+
isToolRecordPruningProtected,
|
|
8
|
+
isToolRecordProtectedByFilePattern,
|
|
9
|
+
} from "./pruner-tools.js";
|
|
10
|
+
import type { ToolRecord } from "./state.js";
|
|
11
|
+
|
|
12
|
+
export type ToolContinuityMode = "none" | "digest" | "verbatim";
|
|
13
|
+
|
|
14
|
+
export interface ToolContinuityDecision {
|
|
15
|
+
mode: ToolContinuityMode;
|
|
16
|
+
text?: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SHELL_TOOLS = new Set(["bash", "shell", "powershell", "exec", "execute"]);
|
|
21
|
+
const MAX_COMMAND_CHARS = 2_048;
|
|
22
|
+
const MAX_ERROR_EXCERPT_CHARS = 2_048;
|
|
23
|
+
const TEST_DIGEST_MAX_BYTES = 8_192;
|
|
24
|
+
|
|
25
|
+
function normalizeToolName(name: string): string {
|
|
26
|
+
return name.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stableDigest(text: string): string {
|
|
30
|
+
return createHash("sha256").update(text).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderedCommand(record: ToolRecord): string {
|
|
34
|
+
const command = shellCommandText(record.inputArgs);
|
|
35
|
+
if (!command) return "unavailable";
|
|
36
|
+
if (command.length <= MAX_COMMAND_CHARS) return command;
|
|
37
|
+
return `[command omitted: ${command.length} chars, sha256:${stableDigest(command)}]`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function outputIdentity(output: string): string {
|
|
41
|
+
return `${Buffer.byteLength(output, "utf8")} bytes, sha256:${stableDigest(output)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function boundedErrorExcerpt(output: string): string | undefined {
|
|
45
|
+
const lines = output.replace(/\r\n/g, "\n").split("\n").filter((line) => line.trim().length > 0);
|
|
46
|
+
const selected: string[] = [];
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
const add = (line: string) => {
|
|
49
|
+
if (seen.has(line)) return;
|
|
50
|
+
seen.add(line);
|
|
51
|
+
selected.push(line);
|
|
52
|
+
};
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (/\b(error|failed?|failure|exception|denied|not found|cannot|unable)\b/i.test(line)) add(line);
|
|
55
|
+
}
|
|
56
|
+
for (const line of lines.slice(-6)) add(line);
|
|
57
|
+
if (selected.length === 0) return undefined;
|
|
58
|
+
let text = selected.join("\n");
|
|
59
|
+
if (text.length > MAX_ERROR_EXCERPT_CHARS) text = text.slice(0, MAX_ERROR_EXCERPT_CHARS) + "\n[excerpt truncated]";
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function shellDigest(record: ToolRecord, kind: "inspection" | "test-build", body?: string): string {
|
|
64
|
+
const output = record.outputText ?? "";
|
|
65
|
+
const lines = [
|
|
66
|
+
`### Tool continuity: ${record.toolName}`,
|
|
67
|
+
`Command: ${renderedCommand(record)}`,
|
|
68
|
+
`Classification: ${kind}`,
|
|
69
|
+
`Outcome: ${record.isError ? "error" : "success"}`,
|
|
70
|
+
`Raw output identity: ${outputIdentity(output)}`,
|
|
71
|
+
];
|
|
72
|
+
if (body) lines.push(body);
|
|
73
|
+
else if (record.isError) {
|
|
74
|
+
const excerpt = boundedErrorExcerpt(output);
|
|
75
|
+
if (excerpt) lines.push("Actionable error excerpt:", excerpt);
|
|
76
|
+
}
|
|
77
|
+
lines.push("Exact raw output is not retained in protected continuity; rerun the command only if exact text is needed.");
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function verbatimToolText(record: ToolRecord): string | undefined {
|
|
82
|
+
const output = (record.outputText ?? "").trim();
|
|
83
|
+
return output ? `### Tool: ${record.toolName}\n${output}` : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function preferSmallerDigest(
|
|
87
|
+
record: ToolRecord,
|
|
88
|
+
reason: string,
|
|
89
|
+
digest: string,
|
|
90
|
+
): ToolContinuityDecision {
|
|
91
|
+
const verbatim = verbatimToolText(record);
|
|
92
|
+
if (verbatim && digest.length >= verbatim.length) {
|
|
93
|
+
return { mode: "verbatim", reason: `${reason}-not-smaller`, text: verbatim };
|
|
94
|
+
}
|
|
95
|
+
return { mode: "digest", reason, text: digest };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function gatewayCompactRepresentation(record: ToolRecord): string | undefined {
|
|
99
|
+
const details = record.outputDetails;
|
|
100
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
|
|
101
|
+
const marker = (details as Record<string, unknown>).contextGateway;
|
|
102
|
+
if (!marker || typeof marker !== "object" || Array.isArray(marker)) return undefined;
|
|
103
|
+
const value = marker as Record<string, unknown>;
|
|
104
|
+
if (value.version !== 1 || value.representation !== "test-build-compact") return undefined;
|
|
105
|
+
return record.outputText?.trim() || undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decide how much of a pruning-protected tool result must survive a compression
|
|
110
|
+
* roll-up. Pruning safety and continuity retention are deliberately separate:
|
|
111
|
+
* shell results remain protected from generic pruning, while proven read-only
|
|
112
|
+
* inspection and confidently parsed test/build output can keep only a bounded
|
|
113
|
+
* continuation digest.
|
|
114
|
+
*/
|
|
115
|
+
export function toolRecordContinuity(record: ToolRecord, config: DcpConfig): ToolContinuityDecision {
|
|
116
|
+
if (!isToolRecordPruningProtected(record, config)) return { mode: "none", reason: "not-pruning-protected" };
|
|
117
|
+
const normalized = normalizeToolName(record.toolName);
|
|
118
|
+
const output = record.outputText ?? "";
|
|
119
|
+
if (!SHELL_TOOLS.has(normalized)) {
|
|
120
|
+
return {
|
|
121
|
+
mode: "verbatim",
|
|
122
|
+
reason: "non-shell-protected-tool",
|
|
123
|
+
text: verbatimToolText(record),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (isToolRecordProtectedByFilePattern(record, config)) {
|
|
127
|
+
return {
|
|
128
|
+
mode: "verbatim",
|
|
129
|
+
reason: "protected-file-pattern",
|
|
130
|
+
text: verbatimToolText(record),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const gatewayCompact = gatewayCompactRepresentation(record);
|
|
134
|
+
if (gatewayCompact) {
|
|
135
|
+
return {
|
|
136
|
+
mode: "verbatim",
|
|
137
|
+
reason: "gateway-compacted",
|
|
138
|
+
text: `### Tool: ${record.toolName}\n${gatewayCompact}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const classification = classifyShellCommand(record.inputArgs);
|
|
143
|
+
if (classification.kind === "inspection") {
|
|
144
|
+
return preferSmallerDigest(record, "read-only-inspection", shellDigest(record, "inspection"));
|
|
145
|
+
}
|
|
146
|
+
if (classification.kind === "test-build" && classification.scope === "simple") {
|
|
147
|
+
const parsed = parseTestBuildOutput({
|
|
148
|
+
text: output,
|
|
149
|
+
hostOutcome: record.isError ? "error" : "success",
|
|
150
|
+
});
|
|
151
|
+
const compact = planProspectiveTestOutputDelivery(parsed, TEST_DIGEST_MAX_BYTES, { commandScope: "simple" });
|
|
152
|
+
if (compact.decision === "compact-candidate" && compact.text) {
|
|
153
|
+
return preferSmallerDigest(
|
|
154
|
+
record,
|
|
155
|
+
"recognised-test-build",
|
|
156
|
+
shellDigest(record, "test-build", compact.text),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
mode: "verbatim",
|
|
161
|
+
reason: `test-build-${compact.reason}`,
|
|
162
|
+
text: verbatimToolText(record),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
mode: "verbatim",
|
|
168
|
+
reason: classification.kind === "mutation"
|
|
169
|
+
? "shell-mutation"
|
|
170
|
+
: classification.scope === "compound"
|
|
171
|
+
? "shell-compound"
|
|
172
|
+
: "shell-unknown",
|
|
173
|
+
text: verbatimToolText(record),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -102,8 +102,17 @@ export interface CompressionCandidateSelectionOptions {
|
|
|
102
102
|
requiredSavingsTokens?: number;
|
|
103
103
|
/** Conservative estimator margin added to the required source size. */
|
|
104
104
|
estimatorMarginTokens?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Whether an automatic candidate may fold already-compressed block
|
|
107
|
+
* placeholders into a new block. Capacity-recovery plans disable this so an
|
|
108
|
+
* economical existing summary is not expanded again while rescuing raw tail
|
|
109
|
+
* growth. Manual/non-pressure consolidation keeps the historical default.
|
|
110
|
+
*/
|
|
111
|
+
allowCompressionBlocks?: boolean;
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
const MIN_AUTOMATIC_BLOCK_CONSOLIDATION = 8;
|
|
115
|
+
|
|
107
116
|
function selectOldestSafePrefix(
|
|
108
117
|
boundaries: CandidateBoundary[],
|
|
109
118
|
state: DcpState,
|
|
@@ -176,7 +185,9 @@ export function detectCompressionCandidate(
|
|
|
176
185
|
if (!settings.enabled) return null;
|
|
177
186
|
if (contextPercent < settings.minContextPercent) return null;
|
|
178
187
|
|
|
179
|
-
const boundaries = buildCandidateBoundaries(messages, _state, {
|
|
188
|
+
const boundaries = buildCandidateBoundaries(messages, _state, {
|
|
189
|
+
allowBlocks: options?.allowCompressionBlocks ?? true,
|
|
190
|
+
});
|
|
180
191
|
|
|
181
192
|
if (boundaries.length < settings.minMessages) return null;
|
|
182
193
|
|
|
@@ -224,6 +235,83 @@ export function detectCompressionCandidate(
|
|
|
224
235
|
};
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Periodically fold a contiguous prefix of already-compressed summaries into
|
|
240
|
+
* one summary. Raw-tail recovery deliberately excludes bN blocks so it cannot
|
|
241
|
+
* recreate the incident's b1 -> b2 -> ... ladder; without a separate bounded
|
|
242
|
+
* consolidation step, however, a marathon turn can accumulate dozens of small
|
|
243
|
+
* active summaries until summary overhead itself fills the provider window.
|
|
244
|
+
*
|
|
245
|
+
* This detector is intentionally block-only: every selected boundary must be a
|
|
246
|
+
* synthetic block message and physically adjacent in the provider projection.
|
|
247
|
+
* Requiring a batch of blocks amortizes the intentional prefix rewrite and
|
|
248
|
+
* prevents one-old-summary-plus-a-tiny-tail recompression on every turn.
|
|
249
|
+
*/
|
|
250
|
+
export function detectCompressionBlockConsolidationCandidate(
|
|
251
|
+
messages: any[],
|
|
252
|
+
state: DcpState,
|
|
253
|
+
config: DcpConfig,
|
|
254
|
+
options?: CompressionCandidateSelectionOptions,
|
|
255
|
+
): CompressionCandidate | null {
|
|
256
|
+
const settings = config.compress.autoCandidates;
|
|
257
|
+
const boundaries = buildCandidateBoundaries(messages, state, { allowBlocks: true });
|
|
258
|
+
const minimumBlocks = MIN_AUTOMATIC_BLOCK_CONSOLIDATION;
|
|
259
|
+
const requiredSavings = Math.max(0, Math.floor(options?.requiredSavingsTokens ?? 0));
|
|
260
|
+
const estimatorMargin = Math.max(0, Math.floor(options?.estimatorMarginTokens ?? 0));
|
|
261
|
+
const targetSourceTokens = Math.max(settings.minTokens, requiredSavings + estimatorMargin);
|
|
262
|
+
|
|
263
|
+
let run: CandidateBoundary[] = [];
|
|
264
|
+
const chooseRun = (candidateRun: CandidateBoundary[]): CandidateBoundary[] | null => {
|
|
265
|
+
if (candidateRun.length < minimumBlocks) return null;
|
|
266
|
+
let selectedTokens = 0;
|
|
267
|
+
let selectedEnd = minimumBlocks - 1;
|
|
268
|
+
for (let index = 0; index < candidateRun.length; index++) {
|
|
269
|
+
selectedTokens += candidateRun[index]!.tokenEstimate;
|
|
270
|
+
if (index + 1 >= minimumBlocks && selectedTokens >= targetSourceTokens) {
|
|
271
|
+
selectedEnd = index;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
selectedEnd = index;
|
|
275
|
+
}
|
|
276
|
+
return candidateRun.slice(0, Math.max(minimumBlocks, selectedEnd + 1));
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
for (const boundary of boundaries) {
|
|
280
|
+
const previous = run.at(-1);
|
|
281
|
+
const continuesBlockRun = boundary.blockId !== undefined &&
|
|
282
|
+
(!previous || boundary.messageIndex === previous.messageIndex + 1);
|
|
283
|
+
if (continuesBlockRun) {
|
|
284
|
+
run.push(boundary);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const selected = chooseRun(run);
|
|
288
|
+
if (selected) {
|
|
289
|
+
const estimatedTokens = selected.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
290
|
+
return {
|
|
291
|
+
startId: selected[0]!.id,
|
|
292
|
+
endId: selected[selected.length - 1]!.id,
|
|
293
|
+
messageCount: selected.length,
|
|
294
|
+
estimatedTokens,
|
|
295
|
+
includedBlockIds: selected.map((item) => item.blockId!),
|
|
296
|
+
reason: `automatic block-only consolidation of ${selected.length} adjacent summaries`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
run = boundary.blockId !== undefined ? [boundary] : [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const selected = chooseRun(run);
|
|
303
|
+
if (!selected) return null;
|
|
304
|
+
const estimatedTokens = selected.reduce((sum, item) => sum + item.tokenEstimate, 0);
|
|
305
|
+
return {
|
|
306
|
+
startId: selected[0]!.id,
|
|
307
|
+
endId: selected[selected.length - 1]!.id,
|
|
308
|
+
messageCount: selected.length,
|
|
309
|
+
estimatedTokens,
|
|
310
|
+
includedBlockIds: selected.map((item) => item.blockId!),
|
|
311
|
+
reason: `automatic block-only consolidation of ${selected.length} adjacent summaries`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
227
315
|
/**
|
|
228
316
|
* Emergency-only range candidate for marathon turns.
|
|
229
317
|
*
|
|
@@ -257,7 +345,9 @@ export function detectEmergencyCompressionCandidate(
|
|
|
257
345
|
if (!emergencySettings.enabled) return null;
|
|
258
346
|
if (contextPercent <= maxContextPercent) return null;
|
|
259
347
|
|
|
260
|
-
const boundaries = buildCandidateBoundaries(messages, state, {
|
|
348
|
+
const boundaries = buildCandidateBoundaries(messages, state, {
|
|
349
|
+
allowBlocks: options?.allowCompressionBlocks ?? true,
|
|
350
|
+
});
|
|
261
351
|
if (boundaries.length < settings.minMessages) return null;
|
|
262
352
|
|
|
263
353
|
let latestUserIndex = -1;
|
|
@@ -453,5 +543,5 @@ export function formatMessageCompressionCandidateHint(candidates: MessageCompres
|
|
|
453
543
|
const entries = candidates
|
|
454
544
|
.map((candidate) => `${candidate.messageId} (${candidate.priority}, ${candidate.role}, ~${candidate.estimatedTokens} tokens)`)
|
|
455
545
|
.join(", ");
|
|
456
|
-
return `\n\nSuggested individual message compression candidates: ${entries}. To compress individual messages, call \`compress\` with a \`messages\` array: { messageId, topic
|
|
546
|
+
return `\n\nSuggested individual message compression candidates: ${entries}. To compress individual messages, call \`compress\` with a \`messages\` array: { messageId, topic }. Omit \`summary\` to use the configured DCP summarizer; provide it only when parent-authored wording is required. Use message mode when a full range would be too broad.`;
|
|
457
547
|
}
|
|
@@ -113,9 +113,9 @@ function persistentMessageId(stableKey: string, state: DcpState): string {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function carrierSelfLabel(role: string): string {
|
|
116
|
-
if (role === "toolResult") return "
|
|
117
|
-
if (role === "bashExecution") return "
|
|
118
|
-
return "
|
|
116
|
+
if (role === "toolResult") return "t";
|
|
117
|
+
if (role === "bashExecution") return "x";
|
|
118
|
+
return "u";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function buildCarrierControlText(
|
|
@@ -125,15 +125,11 @@ function buildCarrierControlText(
|
|
|
125
125
|
blockId: number | undefined,
|
|
126
126
|
): string {
|
|
127
127
|
const assignments = [
|
|
128
|
-
...precedingAssistantIds.map((id) => `${id}=
|
|
128
|
+
...precedingAssistantIds.map((id) => `${id}=a`),
|
|
129
129
|
`${ownId}=${carrierSelfLabel(ownRole)}`,
|
|
130
130
|
];
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`Stable DCP IDs (use with compress; do not quote/output): ${assignments.join("; ")}`,
|
|
134
|
-
...(blockId === undefined ? [] : [`Active compressed block alias: b${blockId}`]),
|
|
135
|
-
"</dcp-message-ids>",
|
|
136
|
-
].join("\n");
|
|
131
|
+
if (blockId !== undefined) assignments.push(`b${blockId}=b`);
|
|
132
|
+
return `<dcp-message-ids>${assignments.join(";")}</dcp-message-ids>`;
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
function appendControlToCarrier(message: any, controlText: string): void {
|
|
@@ -32,31 +32,51 @@ function nudgeTypeLabel(type: DcpNudgeType): string {
|
|
|
32
32
|
|
|
33
33
|
function isRealAnchorCandidate(msg: any): boolean {
|
|
34
34
|
const role = msg?.role ?? "";
|
|
35
|
-
if (role !== "user" && role !== "
|
|
35
|
+
if (role !== "user" && role !== "toolResult") return false;
|
|
36
36
|
return msg?._dcpOrigin !== "block" && msg?._dcpOrigin !== "dcp-control";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function findAnchorMessage(messages: any[]): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
|
|
39
|
+
function findAnchorMessage(messages: any[], freshToolResultIds?: ReadonlySet<string>): { msg: any; index: number; stableId: string; timestamp: number; role: string } | null {
|
|
40
40
|
const stableKeys = stableMessageKeys(messages);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// Only append to a fresh tail; never edit an earlier user/assistant prefix.
|
|
42
|
+
// Tool-result freshness requires a positive local lifecycle grant, not the
|
|
43
|
+
// absence of successful provider evidence (which is unknown after restart).
|
|
44
44
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
45
45
|
const msg = messages[i];
|
|
46
46
|
if (msg?._dcpOrigin === "dcp-control") continue;
|
|
47
|
-
if (
|
|
47
|
+
if (!isRealAnchorCandidate(msg) || !Number.isFinite(msg.timestamp)) return null;
|
|
48
|
+
if (msg.role === "toolResult") {
|
|
49
|
+
if (!freshToolResultIds?.has(msg.toolCallId)) return null;
|
|
50
|
+
// A duplicate/reused call ID is not an unambiguous fresh carrier.
|
|
51
|
+
if (messages.filter((item) => item?.role === "toolResult" && item.toolCallId === msg.toolCallId).length !== 1) return null;
|
|
52
|
+
}
|
|
48
53
|
return { msg, index: i, stableId: stableKeys[i]!, timestamp: msg.timestamp, role: msg.role };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
return null;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
export function hasCacheSafeNudgeCarrier(
|
|
55
|
-
|
|
59
|
+
export function hasCacheSafeNudgeCarrier(
|
|
60
|
+
messages: any[],
|
|
61
|
+
freshToolResultIds?: ReadonlySet<string>,
|
|
62
|
+
anchors: readonly DcpNudgeAnchor[] = [],
|
|
63
|
+
): boolean {
|
|
64
|
+
if (findAnchorMessage(messages, freshToolResultIds) !== null) return true;
|
|
65
|
+
// A previously published reminder remains deliverable without editing its
|
|
66
|
+
// carrier. Losing the grant to create a NEW reminder on a provider attempt
|
|
67
|
+
// must not make retries bypass the completed-opportunity patience policy.
|
|
68
|
+
const stableKeys = stableMessageKeys(messages);
|
|
69
|
+
return anchors.some((anchor) => typeof anchor.renderedReminder === "string" &&
|
|
70
|
+
anchor.renderedReminder.length > 0 &&
|
|
71
|
+
messages.some((message, index) => anchorMatchesMessage(anchor, message, stableKeys[index]!)));
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
function anchorMatchesMessage(anchor: DcpNudgeAnchor, msg: any, stableKey: string): boolean {
|
|
59
|
-
if (
|
|
75
|
+
if (!isRealAnchorCandidate(msg) || msg.role !== anchor.anchorRole) return false;
|
|
76
|
+
if (anchor.anchorStableId) return stableKey === anchor.anchorStableId;
|
|
77
|
+
// Legacy user anchors may be timestamp-only. Tool anchors must never rebind
|
|
78
|
+
// to another parallel result that happens to share their timestamp.
|
|
79
|
+
if (msg.role === "toolResult") return false;
|
|
60
80
|
return msg?.timestamp === anchor.anchorTimestamp;
|
|
61
81
|
}
|
|
62
82
|
|
|
@@ -78,24 +98,8 @@ function appendTextToMessage(msg: any, text: string): void {
|
|
|
78
98
|
return;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
msg.content = [...msg.content, textBlock];
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Keep provider ordering constraints: assistant text must appear before
|
|
88
|
-
// toolCall blocks for models that enforce text/thinking/tool_use ordering.
|
|
89
|
-
const firstToolCallIdx = msg.content.findIndex((block: any) => block?.type === "toolCall");
|
|
90
|
-
if (firstToolCallIdx === -1) {
|
|
91
|
-
msg.content = [...msg.content, textBlock];
|
|
92
|
-
} else {
|
|
93
|
-
msg.content = [
|
|
94
|
-
...msg.content.slice(0, firstToolCallIdx),
|
|
95
|
-
textBlock,
|
|
96
|
-
...msg.content.slice(firstToolCallIdx),
|
|
97
|
-
];
|
|
98
|
-
}
|
|
101
|
+
// Only user/tool-result carriers reach here. Assistant items are immutable.
|
|
102
|
+
msg.content = [...msg.content, { type: "text", text: suffix }];
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
function insertBeforeReminderClose(reminder: string, detail: string): string {
|
|
@@ -174,7 +178,7 @@ export function upsertNudgeAnchor(
|
|
|
174
178
|
messages: any[],
|
|
175
179
|
state: DcpState,
|
|
176
180
|
type: DcpNudgeType,
|
|
177
|
-
options: { contextPercent?: number; renderedReminder?: string } = {},
|
|
181
|
+
options: { contextPercent?: number; renderedReminder?: string; freshToolResultIds?: ReadonlySet<string> } = {},
|
|
178
182
|
): { anchor: DcpNudgeAnchor | null; created: boolean; updated: boolean } {
|
|
179
183
|
const stableKeys = stableMessageKeys(messages);
|
|
180
184
|
let existing: DcpNudgeAnchor | null = null;
|
|
@@ -198,11 +202,10 @@ export function upsertNudgeAnchor(
|
|
|
198
202
|
state.lastNudge = undefined;
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
const target = findAnchorMessage(messages);
|
|
205
|
+
const target = findAnchorMessage(messages, options.freshToolResultIds);
|
|
202
206
|
if (!target) {
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// provider continuation and break prefix equality.
|
|
207
|
+
// No positively fresh carrier: do not synthesize a disappearing tail or
|
|
208
|
+
// reinterpret unknown historical exposure as permission to edit history.
|
|
206
209
|
return { anchor: null, created: false, updated: false };
|
|
207
210
|
}
|
|
208
211
|
|
|
@@ -263,7 +266,7 @@ export function applyAnchoredNudges(
|
|
|
263
266
|
const reminder = selected.anchor.renderedReminder ?? render(selected.anchor);
|
|
264
267
|
if (materializedReminder) selected.anchor.renderedReminder = reminder;
|
|
265
268
|
const anchorMessage = messages[selected.index];
|
|
266
|
-
if (anchorMessage
|
|
269
|
+
if (!isRealAnchorCandidate(anchorMessage)) {
|
|
267
270
|
state.nudgeAnchors = [];
|
|
268
271
|
state.lastNudge = undefined;
|
|
269
272
|
return { rendered: false, stateChanged: true };
|
|
@@ -83,7 +83,7 @@ function patternMatchesValue(pattern: string, value: string): boolean {
|
|
|
83
83
|
return globToRegExp(normalizedPattern).test(normalizedValue);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function
|
|
86
|
+
export function isToolRecordProtectedByFilePattern(record: ToolRecord | undefined, config: DcpConfig): boolean {
|
|
87
87
|
if (!record || config.protectedFilePatterns.length === 0) return false;
|
|
88
88
|
const values = collectStringValues(record.inputArgs);
|
|
89
89
|
return values.some((value) =>
|
|
@@ -91,16 +91,19 @@ function isProtectedByFilePattern(record: ToolRecord | undefined, config: DcpCon
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export function
|
|
94
|
+
export function isToolRecordPruningProtected(
|
|
95
95
|
record: ToolRecord,
|
|
96
96
|
config: DcpConfig,
|
|
97
97
|
extraProtectedTools: string[] = [],
|
|
98
98
|
): boolean {
|
|
99
99
|
const protectedTools = buildProtectedTools(config, extraProtectedTools);
|
|
100
100
|
if (protectedTools.has(normalizeToolName(record.toolName))) return true;
|
|
101
|
-
return
|
|
101
|
+
return isToolRecordProtectedByFilePattern(record, config);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Backward-compatible name for callers that mean pruning protection. */
|
|
105
|
+
export const isToolRecordProtected = isToolRecordPruningProtected;
|
|
106
|
+
|
|
104
107
|
export function markToolPruned(
|
|
105
108
|
state: DcpState,
|
|
106
109
|
toolCallId: string,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { canonicalMessageHash } from "./conversation-index.js";
|
|
2
|
+
import type { CompressionBlock } from "./state.js";
|
|
3
|
+
|
|
4
|
+
interface ProjectionFrame {
|
|
5
|
+
owner: string;
|
|
6
|
+
avoidedTokens: number;
|
|
7
|
+
blocks: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RoutinePressureEstimate {
|
|
11
|
+
projectedTokens: number;
|
|
12
|
+
adjustmentTokens: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function usableUsage(message: any): boolean {
|
|
16
|
+
if (message?.role !== "assistant" || ["error", "aborted"].includes(message.stopReason)) return false;
|
|
17
|
+
const usage = message.usage;
|
|
18
|
+
if (!usage) return false;
|
|
19
|
+
const tokens = usage.totalTokens || [usage.input, usage.output, usage.cacheRead, usage.cacheWrite]
|
|
20
|
+
.reduce((sum, value) => sum + (typeof value === "number" && Number.isFinite(value) ? value : 0), 0);
|
|
21
|
+
return typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function blockVersion(blocks: readonly CompressionBlock[]): string {
|
|
25
|
+
return blocks.filter((block) => block.active).map((block) => block.id).join(",");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runtime-only calibration for ROUTINE nudges, never a provider-send budget.
|
|
30
|
+
* The SDK's last assistant usage still includes history removed by a later DCP
|
|
31
|
+
* rewrite. Subtract only the additional locally measured raw/projected delta
|
|
32
|
+
* since that assistant's correlated request. This keeps provider overhead and
|
|
33
|
+
* tail growth, and avoids counting older compression twice. Uncorrelated usage
|
|
34
|
+
* and lifecycle changes fall back to the existing conservative estimate.
|
|
35
|
+
*/
|
|
36
|
+
export class RoutinePressureTracker {
|
|
37
|
+
private prepared: ProjectionFrame | undefined;
|
|
38
|
+
private pending: ProjectionFrame | undefined;
|
|
39
|
+
private sample: (ProjectionFrame & { hash: string; timestamp: unknown }) | undefined;
|
|
40
|
+
|
|
41
|
+
reset(): void {
|
|
42
|
+
this.prepared = undefined;
|
|
43
|
+
this.pending = undefined;
|
|
44
|
+
this.sample = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
prepare(owner: string, rawTokens: number, projectedTokens: number, blocks: readonly CompressionBlock[]): void {
|
|
48
|
+
this.prepared = { owner, avoidedTokens: rawTokens - projectedTokens, blocks: blockVersion(blocks) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeRequest(): void {
|
|
52
|
+
this.pending = this.prepared;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
complete(message: any, correlated: boolean): void {
|
|
56
|
+
const pending = this.pending;
|
|
57
|
+
this.pending = undefined;
|
|
58
|
+
if (!usableUsage(message)) return;
|
|
59
|
+
this.sample = correlated && pending && typeof message.timestamp === "number" && Number.isFinite(message.timestamp)
|
|
60
|
+
? { ...pending, hash: canonicalMessageHash(message), timestamp: message.timestamp }
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
estimate(owner: string, nativeTokens: number | null | undefined, raw: readonly any[],
|
|
65
|
+
rawTokens: number, projectedTokens: number, blocks: readonly CompressionBlock[]): RoutinePressureEstimate {
|
|
66
|
+
const fallback = Math.max(projectedTokens, nativeTokens ?? 0);
|
|
67
|
+
const unchanged = { projectedTokens: fallback, adjustmentTokens: 0 };
|
|
68
|
+
const sample = this.sample;
|
|
69
|
+
if (!sample || sample.owner !== owner || nativeTokens === null || nativeTokens === undefined) return unchanged;
|
|
70
|
+
const version = blockVersion(blocks);
|
|
71
|
+
if (!version || version === sample.blocks) return unchanged;
|
|
72
|
+
// Confirm the SDK is still based on this exact successful assistant, not
|
|
73
|
+
// an unrelated/new response, failed stream or a sample from another owner.
|
|
74
|
+
let latest: any;
|
|
75
|
+
for (let index = raw.length - 1; index >= 0; index--) {
|
|
76
|
+
if (usableUsage(raw[index])) {
|
|
77
|
+
latest = raw[index];
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!latest || latest.timestamp !== sample.timestamp || canonicalMessageHash(latest) !== sample.hash) return unchanged;
|
|
82
|
+
const newSavings = Math.max(0, rawTokens - projectedTokens - sample.avoidedTokens);
|
|
83
|
+
const adjusted = Math.max(projectedTokens, nativeTokens - newSavings);
|
|
84
|
+
return { projectedTokens: adjusted, adjustmentTokens: fallback - adjusted };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -153,6 +153,15 @@ export interface CompressionBlock {
|
|
|
153
153
|
active: boolean
|
|
154
154
|
/** Token estimate for the summary text itself */
|
|
155
155
|
summaryTokenEstimate: number
|
|
156
|
+
/** One record per successful transaction (on its first block), never on retries.
|
|
157
|
+
* Optional for older journal blocks; absence means unmeasured, not zero gain. */
|
|
158
|
+
commitMetrics?: {
|
|
159
|
+
operationId: string
|
|
160
|
+
kind: "manual" | "auto" | "consolidation"
|
|
161
|
+
beforeTokens: number
|
|
162
|
+
afterTokens: number
|
|
163
|
+
netGainTokens: number
|
|
164
|
+
}
|
|
156
165
|
/** Wall-clock time the block was created (Date.now()) */
|
|
157
166
|
createdAt: number
|
|
158
167
|
/** Older compression blocks subsumed by this block during roll-up. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface DcpStatisticsInput {
|
|
2
|
+
branch?: readonly any[];
|
|
3
|
+
model?: any;
|
|
4
|
+
usage?: any;
|
|
5
|
+
historyStatus?: "full" | "unavailable";
|
|
6
|
+
}
|
|
7
|
+
export function collectDcpStatistics(input: DcpStatisticsInput): any;
|
|
8
|
+
export function formatDcpStatistics(input: DcpStatisticsInput): string;
|