gsd-pi 2.80.0-dev.96f4d89d5 → 2.80.0-dev.b2a374d5b
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/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/GSD-WORKFLOW.md +2 -2
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto/phases.js +37 -30
- package/dist/resources/extensions/gsd/auto-dispatch.js +10 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
- package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +154 -8
- package/dist/resources/extensions/gsd/auto-start.js +2 -3
- package/dist/resources/extensions/gsd/auto.js +16 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +129 -1
- package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
- package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
- package/dist/resources/extensions/gsd/commands-extract-learnings.js +17 -12
- package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
- package/dist/resources/extensions/gsd/db-base-schema.js +14 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +16 -0
- package/dist/resources/extensions/gsd/detection.js +106 -0
- package/dist/resources/extensions/gsd/gsd-db.js +102 -2
- package/dist/resources/extensions/gsd/guided-flow.js +63 -12
- package/dist/resources/extensions/gsd/migrate/command.js +40 -1
- package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
- package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
- package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/dist/resources/extensions/gsd/quick.js +34 -2
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
- package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
- package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +4 -2
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +76 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
- package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
- package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +17 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +99 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js +6 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +50 -19
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +77 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +68 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +36 -27
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +21 -0
- package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
- package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
- package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
- package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +110 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.ts +7 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +68 -19
- package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.ts +10 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +88 -13
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +91 -3
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +36 -27
- package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +25 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
- package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +17 -7
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/tui.ts +19 -7
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js +36 -27
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/src/resources/GSD-WORKFLOW.md +2 -2
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/loop.ts +50 -8
- package/src/resources/extensions/gsd/auto/phases.ts +42 -28
- package/src/resources/extensions/gsd/auto-dispatch.ts +17 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
- package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +153 -7
- package/src/resources/extensions/gsd/auto-start.ts +7 -6
- package/src/resources/extensions/gsd/auto.ts +23 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +135 -1
- package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
- package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
- package/src/resources/extensions/gsd/commands-extract-learnings.ts +17 -12
- package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
- package/src/resources/extensions/gsd/db-base-schema.ts +15 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +17 -0
- package/src/resources/extensions/gsd/detection.ts +128 -0
- package/src/resources/extensions/gsd/gsd-db.ts +119 -1
- package/src/resources/extensions/gsd/guided-flow.ts +66 -12
- package/src/resources/extensions/gsd/migrate/command.ts +47 -1
- package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
- package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
- package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
- package/src/resources/extensions/gsd/quick.ts +37 -2
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +170 -1
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
- package/src/resources/extensions/gsd/tests/commands-extract-learnings.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/db-schema-metadata.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/has-pending-deep-stage.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
- package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +63 -2
- package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
- package/src/resources/extensions/gsd/tests/worktree-write-gate.test.ts +179 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
- package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
- /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// pi-coding-agent / Regression tests for compaction threshold percent (#5475)
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
import { shouldCompact, type CompactionSettings } from "./compaction/compaction.js";
|
|
7
|
+
import { SettingsManager } from "./settings-manager.js";
|
|
8
|
+
|
|
9
|
+
const REGISTRY_DEFAULTS: CompactionSettings = {
|
|
10
|
+
enabled: true,
|
|
11
|
+
reserveTokens: 16_384,
|
|
12
|
+
keepRecentTokens: 20_000,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("shouldCompact — thresholdPercent (#5475)", () => {
|
|
16
|
+
it("uses absolute reserveTokens when thresholdPercent is unset (legacy behavior)", () => {
|
|
17
|
+
// 200K window, 16384 reserve → fires at 183_617 tokens
|
|
18
|
+
assert.equal(shouldCompact(183_616, 200_000, REGISTRY_DEFAULTS), false);
|
|
19
|
+
assert.equal(shouldCompact(183_617, 200_000, REGISTRY_DEFAULTS), true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("uses thresholdPercent when set, ignoring reserveTokens", () => {
|
|
23
|
+
const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: 0.7 };
|
|
24
|
+
// 200K * 0.7 = 140_000 → fires above that
|
|
25
|
+
assert.equal(shouldCompact(140_000, 200_000, settings), false);
|
|
26
|
+
assert.equal(shouldCompact(140_001, 200_000, settings), true);
|
|
27
|
+
// reserveTokens-based math would have said false at 183_616 — the percent override changes that
|
|
28
|
+
assert.equal(shouldCompact(150_000, 200_000, settings), true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("falls back to reserveTokens when thresholdPercent is out of range", () => {
|
|
32
|
+
// Defense in depth: reject 0, 1, negative, NaN, Infinity
|
|
33
|
+
for (const bad of [0, 1, -0.1, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
34
|
+
const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: bad };
|
|
35
|
+
assert.equal(
|
|
36
|
+
shouldCompact(183_616, 200_000, settings),
|
|
37
|
+
false,
|
|
38
|
+
`bad=${bad} should fall back to reserveTokens math`,
|
|
39
|
+
);
|
|
40
|
+
assert.equal(shouldCompact(183_617, 200_000, settings), true, `bad=${bad}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("respects enabled=false regardless of thresholdPercent", () => {
|
|
45
|
+
const settings: CompactionSettings = {
|
|
46
|
+
...REGISTRY_DEFAULTS,
|
|
47
|
+
enabled: false,
|
|
48
|
+
thresholdPercent: 0.5,
|
|
49
|
+
};
|
|
50
|
+
assert.equal(shouldCompact(199_999, 200_000, settings), false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("scales with contextWindow — same percent, different windows", () => {
|
|
54
|
+
const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: 0.8 };
|
|
55
|
+
// 100K window: fires above 80_000
|
|
56
|
+
assert.equal(shouldCompact(80_000, 100_000, settings), false);
|
|
57
|
+
assert.equal(shouldCompact(80_001, 100_000, settings), true);
|
|
58
|
+
// 1M window: fires above 800_000
|
|
59
|
+
assert.equal(shouldCompact(800_000, 1_000_000, settings), false);
|
|
60
|
+
assert.equal(shouldCompact(800_001, 1_000_000, settings), true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("SettingsManager — compaction threshold override (#5475)", () => {
|
|
65
|
+
it("getCompactionThresholdPercent returns undefined by default", () => {
|
|
66
|
+
const sm = SettingsManager.inMemory({});
|
|
67
|
+
assert.equal(sm.getCompactionThresholdPercent(), undefined);
|
|
68
|
+
assert.equal(sm.getCompactionSettings().thresholdPercent, undefined);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("setCompactionThresholdOverride applies in-memory and is exposed via getCompactionSettings", () => {
|
|
72
|
+
const sm = SettingsManager.inMemory({});
|
|
73
|
+
sm.setCompactionThresholdOverride(0.7);
|
|
74
|
+
assert.equal(sm.getCompactionThresholdPercent(), 0.7);
|
|
75
|
+
assert.equal(sm.getCompactionSettings().thresholdPercent, 0.7);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("setCompactionThresholdOverride(undefined) clears a prior override", () => {
|
|
79
|
+
const sm = SettingsManager.inMemory({});
|
|
80
|
+
sm.setCompactionThresholdOverride(0.7);
|
|
81
|
+
sm.setCompactionThresholdOverride(undefined);
|
|
82
|
+
assert.equal(sm.getCompactionThresholdPercent(), undefined);
|
|
83
|
+
assert.equal(sm.getCompactionSettings().thresholdPercent, undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("setCompactionThresholdOverride preserves other compaction fields (enabled, reserveTokens)", () => {
|
|
87
|
+
const sm = SettingsManager.inMemory({
|
|
88
|
+
compaction: { enabled: true, reserveTokens: 30_000, keepRecentTokens: 25_000 },
|
|
89
|
+
});
|
|
90
|
+
sm.setCompactionThresholdOverride(0.6);
|
|
91
|
+
const settings = sm.getCompactionSettings();
|
|
92
|
+
assert.equal(settings.enabled, true);
|
|
93
|
+
assert.equal(settings.reserveTokens, 30_000);
|
|
94
|
+
assert.equal(settings.keepRecentTokens, 25_000);
|
|
95
|
+
assert.equal(settings.thresholdPercent, 0.6);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("setCompactionThresholdOverride works when no compaction config exists yet", () => {
|
|
99
|
+
const sm = SettingsManager.inMemory({});
|
|
100
|
+
sm.setCompactionThresholdOverride(0.85);
|
|
101
|
+
assert.equal(sm.getCompactionThresholdPercent(), 0.85);
|
|
102
|
+
// Other compaction fields fall back to their defaults
|
|
103
|
+
const settings = sm.getCompactionSettings();
|
|
104
|
+
assert.equal(settings.enabled, true);
|
|
105
|
+
assert.equal(typeof settings.reserveTokens, "number");
|
|
106
|
+
assert.equal(typeof settings.keepRecentTokens, "number");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("end-to-end — getCompactionSettings + shouldCompact (#5475)", () => {
|
|
111
|
+
it("70% threshold on a 200K window fires at the documented bug-report value (140_001 not 183_617)", () => {
|
|
112
|
+
const sm = SettingsManager.inMemory({});
|
|
113
|
+
sm.setCompactionThresholdOverride(0.7);
|
|
114
|
+
const settings = sm.getCompactionSettings();
|
|
115
|
+
|
|
116
|
+
assert.equal(shouldCompact(140_000, 200_000, settings), false);
|
|
117
|
+
assert.equal(shouldCompact(140_001, 200_000, settings), true);
|
|
118
|
+
// Pre-fix behavior would have required 183_617 — verify we no longer wait that long
|
|
119
|
+
assert.equal(shouldCompact(150_000, 200_000, settings), true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it, afterEach } from "node:test";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { atomicWriteDbSnapshotSync } from "./db-snapshot.js";
|
|
8
|
+
|
|
9
|
+
describe("atomicWriteDbSnapshotSync", () => {
|
|
10
|
+
let dir: string;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (dir) {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("writes the full snapshot and leaves no temp file after success", () => {
|
|
19
|
+
dir = mkdtempSync(join(tmpdir(), "gsd-db-snapshot-test-"));
|
|
20
|
+
const dbPath = join(dir, "agent.db");
|
|
21
|
+
const snapshot = new Uint8Array([0x53, 0x51, 0x4c, 0x69, 0x74, 0x65]);
|
|
22
|
+
|
|
23
|
+
atomicWriteDbSnapshotSync(dbPath, snapshot);
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(readFileSync(dbPath), Buffer.from(snapshot));
|
|
26
|
+
assert.equal(existsSync(`${dbPath}.tmp`), false);
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
readdirSync(dir).filter((entry) => entry.includes("agent.db") && entry.includes(".tmp")),
|
|
29
|
+
[],
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, fsyncSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export type DbSnapshot = Buffer | Uint8Array;
|
|
6
|
+
|
|
7
|
+
function closeBestEffort(fd: number | null): void {
|
|
8
|
+
if (fd === null) return;
|
|
9
|
+
try {
|
|
10
|
+
closeSync(fd);
|
|
11
|
+
} catch {
|
|
12
|
+
// Preserve the original write/rename failure when cleaning up.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fsyncDirectoryBestEffort(dirPath: string): void {
|
|
17
|
+
let fd: number | null = null;
|
|
18
|
+
try {
|
|
19
|
+
fd = openSync(dirPath, "r");
|
|
20
|
+
fsyncSync(fd);
|
|
21
|
+
} catch {
|
|
22
|
+
// Directory fsync is unsupported on some platforms/filesystems.
|
|
23
|
+
} finally {
|
|
24
|
+
closeBestEffort(fd);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Persist a sql.js database export without exposing callers to a torn live file.
|
|
30
|
+
*
|
|
31
|
+
* The snapshot is written to a unique temp file in the same directory, flushed,
|
|
32
|
+
* then renamed over the target. A hard kill during the temp write leaves the
|
|
33
|
+
* previous target intact; after rename, readers see the complete new snapshot.
|
|
34
|
+
*
|
|
35
|
+
* The rename replaces the target inode: existing file mode/ownership is not
|
|
36
|
+
* preserved, and a symlink at dbPath is replaced rather than written through.
|
|
37
|
+
* Agent DB snapshots are owned runtime files, so this trade-off favors a
|
|
38
|
+
* private 0600 replacement over retaining prior target metadata.
|
|
39
|
+
*/
|
|
40
|
+
export function atomicWriteDbSnapshotSync(dbPath: string, snapshot: DbSnapshot): void {
|
|
41
|
+
const dirPath = dirname(dbPath);
|
|
42
|
+
const tmpPath = join(dirPath, `.${basename(dbPath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
|
|
43
|
+
let fd: number | null = null;
|
|
44
|
+
let renamed = false;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
fd = openSync(tmpPath, "wx", 0o600);
|
|
48
|
+
writeFileSync(fd, Buffer.from(snapshot));
|
|
49
|
+
fsyncSync(fd);
|
|
50
|
+
closeSync(fd);
|
|
51
|
+
fd = null;
|
|
52
|
+
|
|
53
|
+
renameSync(tmpPath, dbPath);
|
|
54
|
+
renamed = true;
|
|
55
|
+
fsyncDirectoryBestEffort(dirPath);
|
|
56
|
+
} finally {
|
|
57
|
+
closeBestEffort(fd);
|
|
58
|
+
if (!renamed) {
|
|
59
|
+
try {
|
|
60
|
+
unlinkSync(tmpPath);
|
|
61
|
+
} catch {
|
|
62
|
+
// The temp file may not have been created yet.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -102,6 +102,61 @@ describe("ExtensionRunner.emitToolCall", () => {
|
|
|
102
102
|
assert.equal(errors[0].event, "tool_call");
|
|
103
103
|
assert.equal(errors[0].extensionPath, "/test/throwing-ext");
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it("preserves shutdown in tool_call handler context", async (t) => {
|
|
107
|
+
const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
|
|
108
|
+
t.after(() => {
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const sessionManager = SessionManager.create(dir, dir);
|
|
113
|
+
const authStorage = AuthStorage.create();
|
|
114
|
+
const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
|
|
115
|
+
const runtime = makeMinimalRuntime();
|
|
116
|
+
let shutdownCount = 0;
|
|
117
|
+
const handlers = new Map();
|
|
118
|
+
handlers.set("tool_call", [
|
|
119
|
+
async (_event: unknown, ctx: { shutdown: () => void }) => {
|
|
120
|
+
ctx.shutdown();
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
const extension = {
|
|
124
|
+
path: "/test/shutdown-on-tool-call",
|
|
125
|
+
handlers,
|
|
126
|
+
commands: new Map(),
|
|
127
|
+
shortcuts: new Map(),
|
|
128
|
+
tools: new Map(),
|
|
129
|
+
flags: new Map(),
|
|
130
|
+
diagnostics: [],
|
|
131
|
+
} as unknown as Extension;
|
|
132
|
+
const runner = new ExtensionRunner([extension], runtime, dir, sessionManager, modelRegistry);
|
|
133
|
+
runner.bindCore({} as any, {
|
|
134
|
+
getModel: () => undefined,
|
|
135
|
+
isIdle: () => true,
|
|
136
|
+
abort: () => {},
|
|
137
|
+
hasPendingMessages: () => false,
|
|
138
|
+
shutdown: () => {
|
|
139
|
+
shutdownCount += 1;
|
|
140
|
+
},
|
|
141
|
+
getContextUsage: () => undefined,
|
|
142
|
+
compact: () => {},
|
|
143
|
+
getSystemPrompt: () => "",
|
|
144
|
+
setCompactionThresholdOverride: () => {},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const errors: any[] = [];
|
|
148
|
+
runner.onError((err) => errors.push(err));
|
|
149
|
+
|
|
150
|
+
await runner.emitToolCall({
|
|
151
|
+
type: "tool_call",
|
|
152
|
+
toolCallId: "test-123",
|
|
153
|
+
toolName: "test_tool",
|
|
154
|
+
input: {},
|
|
155
|
+
} as ToolCallEvent);
|
|
156
|
+
|
|
157
|
+
assert.equal(shutdownCount, 1);
|
|
158
|
+
assert.equal(errors.length, 0);
|
|
159
|
+
});
|
|
105
160
|
});
|
|
106
161
|
|
|
107
162
|
describe("ExtensionRunner.createContext", () => {
|
|
@@ -127,6 +182,61 @@ describe("ExtensionRunner.createContext", () => {
|
|
|
127
182
|
assert.equal(runner.createContext().cwd, realProjectDir);
|
|
128
183
|
assert.equal(runner.createCommandContext().cwd, realProjectDir);
|
|
129
184
|
});
|
|
185
|
+
|
|
186
|
+
it("does not let lifecycle event handlers close the TUI", async (t) => {
|
|
187
|
+
const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
|
|
188
|
+
t.after(() => {
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const sessionManager = SessionManager.create(dir, dir);
|
|
193
|
+
const authStorage = AuthStorage.create();
|
|
194
|
+
const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
|
|
195
|
+
const runtime = makeMinimalRuntime();
|
|
196
|
+
let shutdownCount = 0;
|
|
197
|
+
const handlers = new Map();
|
|
198
|
+
handlers.set("agent_end", [
|
|
199
|
+
async (_event: unknown, ctx: { shutdown: () => void }) => {
|
|
200
|
+
ctx.shutdown();
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
const extension = {
|
|
204
|
+
path: "/test/shutdown-on-agent-end",
|
|
205
|
+
handlers,
|
|
206
|
+
commands: new Map(),
|
|
207
|
+
shortcuts: new Map(),
|
|
208
|
+
tools: new Map(),
|
|
209
|
+
flags: new Map(),
|
|
210
|
+
diagnostics: [],
|
|
211
|
+
} as unknown as Extension;
|
|
212
|
+
const runner = new ExtensionRunner([extension], runtime, dir, sessionManager, modelRegistry);
|
|
213
|
+
runner.bindCore({} as any, {
|
|
214
|
+
getModel: () => undefined,
|
|
215
|
+
isIdle: () => true,
|
|
216
|
+
abort: () => {},
|
|
217
|
+
hasPendingMessages: () => false,
|
|
218
|
+
shutdown: () => {
|
|
219
|
+
shutdownCount += 1;
|
|
220
|
+
},
|
|
221
|
+
getContextUsage: () => undefined,
|
|
222
|
+
compact: () => {},
|
|
223
|
+
getSystemPrompt: () => "",
|
|
224
|
+
setCompactionThresholdOverride: () => {},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const errors: any[] = [];
|
|
228
|
+
runner.onError((err) => errors.push(err));
|
|
229
|
+
|
|
230
|
+
await runner.emit({ type: "agent_end", messages: [] } as any);
|
|
231
|
+
|
|
232
|
+
assert.equal(shutdownCount, 0);
|
|
233
|
+
assert.equal(errors.length, 1);
|
|
234
|
+
assert.equal(errors[0].event, "agent_end");
|
|
235
|
+
assert.match(errors[0].error, /cannot request TUI shutdown/);
|
|
236
|
+
|
|
237
|
+
runner.createCommandContext().shutdown();
|
|
238
|
+
assert.equal(shutdownCount, 1);
|
|
239
|
+
});
|
|
130
240
|
});
|
|
131
241
|
|
|
132
242
|
describe("ExtensionRunner protected commands", () => {
|
|
@@ -235,6 +235,7 @@ export class ExtensionRunner {
|
|
|
235
235
|
private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
236
236
|
private compactFn: (options?: CompactOptions) => void = () => {};
|
|
237
237
|
private getSystemPromptFn: () => string = () => "";
|
|
238
|
+
private setCompactionThresholdOverrideFn: (percent: number | undefined) => void = () => {};
|
|
238
239
|
private newSessionHandler: NewSessionHandler = async () => {
|
|
239
240
|
throw new Error("Command context not yet bound: newSession is unavailable during early lifecycle");
|
|
240
241
|
};
|
|
@@ -428,6 +429,7 @@ export class ExtensionRunner {
|
|
|
428
429
|
this.getContextUsageFn = contextActions.getContextUsage;
|
|
429
430
|
this.compactFn = contextActions.compact;
|
|
430
431
|
this.getSystemPromptFn = contextActions.getSystemPrompt;
|
|
432
|
+
this.setCompactionThresholdOverrideFn = contextActions.setCompactionThresholdOverride;
|
|
431
433
|
|
|
432
434
|
// Flush provider registrations queued during extension loading
|
|
433
435
|
for (const { name, config } of this.runtime.pendingProviderRegistrations) {
|
|
@@ -714,9 +716,23 @@ export class ExtensionRunner {
|
|
|
714
716
|
getContextUsage: () => this.getContextUsageFn(),
|
|
715
717
|
compact: (options) => this.compactFn(options),
|
|
716
718
|
getSystemPrompt: () => this.getSystemPromptFn(),
|
|
719
|
+
setCompactionThresholdOverride: (percent) => this.setCompactionThresholdOverrideFn(percent),
|
|
717
720
|
};
|
|
718
721
|
}
|
|
719
722
|
|
|
723
|
+
private createEventContext(eventType: string): ExtensionContext {
|
|
724
|
+
return {
|
|
725
|
+
...this.createContext(),
|
|
726
|
+
shutdown: () => {
|
|
727
|
+
throw new Error(`Extension event '${eventType}' cannot request TUI shutdown`);
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private isShutdownGuardedEvent(eventType: string): boolean {
|
|
733
|
+
return eventType === "agent_end" || eventType === "stop" || eventType === "session_end";
|
|
734
|
+
}
|
|
735
|
+
|
|
720
736
|
createCommandContext(): ExtensionCommandContext {
|
|
721
737
|
return {
|
|
722
738
|
...this.createContext(),
|
|
@@ -757,7 +773,9 @@ export class ExtensionRunner {
|
|
|
757
773
|
getEvent: () => unknown,
|
|
758
774
|
processResult: (handlerResult: unknown, extensionPath: string) => { done: boolean },
|
|
759
775
|
): Promise<void> {
|
|
760
|
-
const ctx = this.
|
|
776
|
+
const ctx = this.isShutdownGuardedEvent(eventType)
|
|
777
|
+
? this.createEventContext(eventType)
|
|
778
|
+
: this.createContext();
|
|
761
779
|
|
|
762
780
|
for (const ext of this.extensions) {
|
|
763
781
|
const handlers = ext.handlers.get(eventType);
|
|
@@ -289,6 +289,12 @@ export interface ExtensionContext {
|
|
|
289
289
|
compact(options?: CompactOptions): void;
|
|
290
290
|
/** Get the current effective system prompt. */
|
|
291
291
|
getSystemPrompt(): string;
|
|
292
|
+
/**
|
|
293
|
+
* Set or clear an in-memory compaction threshold-percent override (0 < value < 1).
|
|
294
|
+
* Pass `undefined` to clear. The override is not persisted; host integrations
|
|
295
|
+
* are expected to re-apply on each session_start.
|
|
296
|
+
*/
|
|
297
|
+
setCompactionThresholdOverride(percent: number | undefined): void;
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
/**
|
|
@@ -1741,6 +1747,7 @@ export interface ExtensionContextActions {
|
|
|
1741
1747
|
getContextUsage: () => ContextUsage | undefined;
|
|
1742
1748
|
compact: (options?: CompactOptions) => void;
|
|
1743
1749
|
getSystemPrompt: () => string;
|
|
1750
|
+
setCompactionThresholdOverride: (percent: number | undefined) => void;
|
|
1744
1751
|
}
|
|
1745
1752
|
|
|
1746
1753
|
/**
|
|
@@ -15,6 +15,13 @@ export interface CompactionSettings {
|
|
|
15
15
|
enabled?: boolean; // default: true
|
|
16
16
|
reserveTokens?: number; // default: 16384
|
|
17
17
|
keepRecentTokens?: number; // default: 20000
|
|
18
|
+
/**
|
|
19
|
+
* Optional percent-of-context-window trigger (0 < value < 1). When set,
|
|
20
|
+
* compaction fires at `contextWindow * thresholdPercent` and overrides
|
|
21
|
+
* `reserveTokens`. Typically set as a runtime override by host integrations
|
|
22
|
+
* (see `setCompactionThresholdOverride`) and not persisted by users directly.
|
|
23
|
+
*/
|
|
24
|
+
thresholdPercent?: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export interface BranchSummarySettings {
|
|
@@ -812,11 +819,42 @@ export class SettingsManager {
|
|
|
812
819
|
return this.settings.compaction?.keepRecentTokens ?? COMPACTION_KEEP_RECENT_TOKENS;
|
|
813
820
|
}
|
|
814
821
|
|
|
815
|
-
|
|
822
|
+
getCompactionThresholdPercent(): number | undefined {
|
|
823
|
+
return this.settings.compaction?.thresholdPercent;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Set or clear an in-memory compaction threshold-percent override.
|
|
828
|
+
*
|
|
829
|
+
* Applied to `this.settings` only; never persisted to disk. Pass `undefined`
|
|
830
|
+
* to clear a previously set override (necessary for idempotent re-sync from
|
|
831
|
+
* host integrations whose preference may have been removed).
|
|
832
|
+
*
|
|
833
|
+
* Direct mutation is used instead of `applyOverrides()` because deep-merge
|
|
834
|
+
* semantics skip `undefined` values, which would prevent clearing.
|
|
835
|
+
*/
|
|
836
|
+
setCompactionThresholdOverride(percent: number | undefined): void {
|
|
837
|
+
if (!this.settings.compaction) {
|
|
838
|
+
this.settings.compaction = {};
|
|
839
|
+
}
|
|
840
|
+
if (percent === undefined) {
|
|
841
|
+
delete this.settings.compaction.thresholdPercent;
|
|
842
|
+
} else {
|
|
843
|
+
this.settings.compaction.thresholdPercent = percent;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
getCompactionSettings(): {
|
|
848
|
+
enabled: boolean;
|
|
849
|
+
reserveTokens: number;
|
|
850
|
+
keepRecentTokens: number;
|
|
851
|
+
thresholdPercent?: number;
|
|
852
|
+
} {
|
|
816
853
|
return {
|
|
817
854
|
enabled: this.getCompactionEnabled(),
|
|
818
855
|
reserveTokens: this.getCompactionReserveTokens(),
|
|
819
856
|
keepRecentTokens: this.getCompactionKeepRecentTokens(),
|
|
857
|
+
thresholdPercent: this.getCompactionThresholdPercent(),
|
|
820
858
|
};
|
|
821
859
|
}
|
|
822
860
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 TUI Tests - Chat frame card visual contract coverage.
|
|
1
2
|
import { test, describe } from "node:test";
|
|
2
3
|
import assert from "node:assert/strict";
|
|
3
4
|
import stripAnsi from "strip-ansi";
|
|
@@ -8,12 +9,12 @@ initTheme("dark", false);
|
|
|
8
9
|
|
|
9
10
|
// Regression tests for the "compaction" tone added to renderChatFrame.
|
|
10
11
|
// The compaction notice shares the same visual frame as user / assistant
|
|
11
|
-
// messages (top rule,
|
|
12
|
+
// messages (top rule, label header, `│ ` body prefix) but uses the
|
|
12
13
|
// purple `customMessageLabel` color key so it is visually distinct from
|
|
13
14
|
// conversation turns.
|
|
14
15
|
|
|
15
16
|
describe("renderChatFrame — compaction tone", () => {
|
|
16
|
-
test("produces a top rule,
|
|
17
|
+
test("produces a top rule, compaction header row, and a │ body margin", () => {
|
|
17
18
|
const lines = renderChatFrame(
|
|
18
19
|
["Compacted from 1,224,262 tokens (ctrl+o to expand)"],
|
|
19
20
|
60,
|
|
@@ -33,11 +34,12 @@ describe("renderChatFrame — compaction tone", () => {
|
|
|
33
34
|
// Top rule is a solid horizontal bar
|
|
34
35
|
assert.match(plain[0], /^─+$/, "first line should be the solid top rule");
|
|
35
36
|
|
|
36
|
-
// Header row contains
|
|
37
|
+
// Header row contains `compaction`
|
|
37
38
|
assert.ok(
|
|
38
|
-
plain[1].includes("
|
|
39
|
-
`expected header to contain "
|
|
39
|
+
plain[1].includes("compaction"),
|
|
40
|
+
`expected header to contain "compaction", got ${JSON.stringify(plain[1])}`,
|
|
40
41
|
);
|
|
42
|
+
assert.ok(!plain[1].includes("•"), `header should not render a bullet prefix, got ${JSON.stringify(plain[1])}`);
|
|
41
43
|
|
|
42
44
|
// Body line(s) start with `│ `
|
|
43
45
|
assert.ok(
|
package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// GSD-2 Interactive Tool Execution Rendering Tests
|
|
1
2
|
import { describe, test } from "node:test";
|
|
2
3
|
import assert from "node:assert/strict";
|
|
3
4
|
import stripAnsi from "strip-ansi";
|
|
4
|
-
import { ToolExecutionComponent } from "../tool-execution.js";
|
|
5
|
+
import { ToolExecutionComponent, ToolPhaseSummaryComponent, type ToolExecutionPhase } from "../tool-execution.js";
|
|
5
6
|
import { initTheme } from "../../theme/theme.js";
|
|
6
7
|
|
|
7
8
|
initTheme("dark", false);
|
|
@@ -50,24 +51,26 @@ function renderToolCollapsed(
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
describe("ToolExecutionComponent", () => {
|
|
53
|
-
test("renders framed header with
|
|
54
|
+
test("renders framed header with running status while tool is partial", () => {
|
|
54
55
|
const rendered = renderToolCollapsed("mcp__demo__do_thing", { ok: true });
|
|
55
56
|
|
|
56
|
-
assert.match(rendered, /
|
|
57
|
-
assert.
|
|
58
|
-
assert.match(rendered, /
|
|
57
|
+
assert.match(rendered, /demo\u00b7do_thing/);
|
|
58
|
+
assert.doesNotMatch(rendered, /Tool demo\u00b7do_thing/);
|
|
59
|
+
assert.match(rendered, /running/);
|
|
60
|
+
assert.match(rendered, /running · \d+(ms|s)/);
|
|
59
61
|
});
|
|
60
62
|
|
|
61
|
-
test("renders framed header with
|
|
63
|
+
test("renders framed header with failed status for failed tool result", () => {
|
|
62
64
|
const rendered = renderTool(
|
|
63
65
|
"mcp__demo__do_thing",
|
|
64
66
|
{ ok: true },
|
|
65
67
|
{ content: [{ type: "text", text: "boom" }], isError: true },
|
|
66
68
|
);
|
|
67
69
|
|
|
68
|
-
assert.match(rendered, /
|
|
69
|
-
assert.
|
|
70
|
-
assert.match(rendered, /
|
|
70
|
+
assert.match(rendered, /demo\u00b7do_thing/);
|
|
71
|
+
assert.doesNotMatch(rendered, /Tool demo\u00b7do_thing/);
|
|
72
|
+
assert.match(rendered, /failed/);
|
|
73
|
+
assert.match(rendered, /failed · \d+(ms|s)/);
|
|
71
74
|
assert.match(rendered, /boom/);
|
|
72
75
|
});
|
|
73
76
|
|
|
@@ -78,11 +81,55 @@ describe("ToolExecutionComponent", () => {
|
|
|
78
81
|
{ content: [], isError: false },
|
|
79
82
|
);
|
|
80
83
|
|
|
81
|
-
assert.match(rendered, /
|
|
82
|
-
assert.match(rendered, /
|
|
84
|
+
assert.match(rendered, /success · \d+(ms|s)/);
|
|
85
|
+
assert.match(rendered, /demo\u00b7noop/);
|
|
86
|
+
assert.doesNotMatch(rendered, /Completed/);
|
|
83
87
|
assert.doesNotMatch(rendered, /ok=true/);
|
|
84
88
|
});
|
|
85
89
|
|
|
90
|
+
test("exposes phase metadata for successful low-signal tool rows", () => {
|
|
91
|
+
const component = new ToolExecutionComponent(
|
|
92
|
+
"gsd_requirement_update",
|
|
93
|
+
{ id: "R001" },
|
|
94
|
+
{},
|
|
95
|
+
{ label: "Update Requirement" } as any,
|
|
96
|
+
{ requestRender() {} } as any,
|
|
97
|
+
);
|
|
98
|
+
component.updateResult({ content: [], isError: false });
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(component.getRollupPhase()?.label, "Requirement writes");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("exposes phase metadata for collapsed output-bearing generic tools", () => {
|
|
104
|
+
const component = new ToolExecutionComponent(
|
|
105
|
+
"mcp__demo__do_thing",
|
|
106
|
+
{ ok: true },
|
|
107
|
+
{},
|
|
108
|
+
undefined,
|
|
109
|
+
{ requestRender() {} } as any,
|
|
110
|
+
);
|
|
111
|
+
component.updateResult({ content: [{ type: "text", text: "important output" }], isError: false });
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(component.getRollupPhase()?.label, "Other tool actions");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("renders phase-based summaries for rolled-up tool executions", () => {
|
|
117
|
+
const phases: ToolExecutionPhase[] = [
|
|
118
|
+
{ label: "Setup / shell", count: 6, durationMs: 12 },
|
|
119
|
+
{ label: "Context reads", count: 4, durationMs: 6 },
|
|
120
|
+
{ label: "Requirement writes", count: 4, durationMs: 4 },
|
|
121
|
+
{ label: "Memory lookups", count: 4, durationMs: 4 },
|
|
122
|
+
{ label: "Finalization", count: 1, durationMs: 1 },
|
|
123
|
+
];
|
|
124
|
+
const rendered = stripAnsi(new ToolPhaseSummaryComponent(phases).render(120).join("\n"));
|
|
125
|
+
|
|
126
|
+
assert.match(rendered, /Setup \/ shell 6 actions\s+success · 12ms/);
|
|
127
|
+
assert.match(rendered, /Context reads 4 actions\s+success · 6ms/);
|
|
128
|
+
assert.match(rendered, /Requirement writes 4 actions\s+success · 4ms/);
|
|
129
|
+
assert.match(rendered, /Memory lookups 4 actions\s+success · 4ms/);
|
|
130
|
+
assert.match(rendered, /Finalization 1 action\s+success · 1ms/);
|
|
131
|
+
});
|
|
132
|
+
|
|
86
133
|
test("passes failed result status to custom result renderers", () => {
|
|
87
134
|
const rendered = renderTool(
|
|
88
135
|
"gsd_requirement_save",
|
|
@@ -99,7 +146,7 @@ describe("ToolExecutionComponent", () => {
|
|
|
99
146
|
},
|
|
100
147
|
);
|
|
101
148
|
|
|
102
|
-
assert.match(rendered, /
|
|
149
|
+
assert.match(rendered, /failed/);
|
|
103
150
|
assert.match(rendered, /custom saw error/);
|
|
104
151
|
assert.doesNotMatch(rendered, /custom saw success/);
|
|
105
152
|
});
|
|
@@ -162,14 +209,16 @@ describe("ToolExecutionComponent", () => {
|
|
|
162
209
|
{ label: "Complete Slice" },
|
|
163
210
|
);
|
|
164
211
|
|
|
165
|
-
assert.match(rendered, /
|
|
212
|
+
assert.match(rendered, /Complete Slice/);
|
|
213
|
+
assert.doesNotMatch(rendered, /Tool Complete Slice/);
|
|
166
214
|
assert.doesNotMatch(rendered, /gsd_slice_complete/);
|
|
167
215
|
});
|
|
168
216
|
|
|
169
217
|
test("frame header strips gsd_ prefix and title-cases when no label is registered", () => {
|
|
170
218
|
const rendered = renderToolCollapsed("gsd_requirement_update", { id: "R005" });
|
|
171
219
|
|
|
172
|
-
assert.match(rendered, /
|
|
220
|
+
assert.match(rendered, /Requirement Update/);
|
|
221
|
+
assert.doesNotMatch(rendered, /Tool Requirement Update/);
|
|
173
222
|
assert.doesNotMatch(rendered, /gsd_requirement_update/);
|
|
174
223
|
});
|
|
175
224
|
|
|
@@ -198,7 +247,7 @@ describe("ToolExecutionComponent", () => {
|
|
|
198
247
|
assert.doesNotMatch(rendered, /…/);
|
|
199
248
|
});
|
|
200
249
|
|
|
201
|
-
test("generic fallback
|
|
250
|
+
test("generic fallback collapses successful output rows until expanded", () => {
|
|
202
251
|
const longOutput = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join("\n");
|
|
203
252
|
const rendered = renderToolCollapsed(
|
|
204
253
|
"mcp__demo__do_thing",
|
|
@@ -206,10 +255,10 @@ describe("ToolExecutionComponent", () => {
|
|
|
206
255
|
{ content: [{ type: "text", text: longOutput }], isError: false },
|
|
207
256
|
);
|
|
208
257
|
|
|
209
|
-
assert.match(rendered, /
|
|
210
|
-
assert.match(rendered, /
|
|
211
|
-
assert.doesNotMatch(rendered, /line
|
|
212
|
-
assert.
|
|
258
|
+
assert.match(rendered, /demo\u00b7do_thing/);
|
|
259
|
+
assert.match(rendered, /success · \d+(ms|s)/);
|
|
260
|
+
assert.doesNotMatch(rendered, /line 1\b/);
|
|
261
|
+
assert.doesNotMatch(rendered, /\(15 more lines/);
|
|
213
262
|
});
|
|
214
263
|
|
|
215
264
|
test("generic fallback falls back to truncated JSON for complex args", () => {
|
|
@@ -126,8 +126,8 @@ export class AdaptiveLayoutComponent implements Component {
|
|
|
126
126
|
|
|
127
127
|
private frame(lines: string[], width: number, title: string, rightTitle: string, accent: ThemeColor): string[] {
|
|
128
128
|
return style()
|
|
129
|
-
.border("
|
|
130
|
-
.density("
|
|
129
|
+
.border("rule")
|
|
130
|
+
.density("compact")
|
|
131
131
|
.toneColor((text) => theme.fg("surfaceMuted", text))
|
|
132
132
|
.borderColor((text) => theme.fg("surfaceBorder", text))
|
|
133
133
|
.title(theme.fg("surfaceTitle", title))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD2 TUI - Assistant message card renderer for interactive terminal sessions.
|
|
1
2
|
import type { AssistantMessage } from "@gsd/pi-ai";
|
|
2
3
|
import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui";
|
|
3
4
|
import { getMarkdownTheme, theme } from "../theme/theme.js";
|