@pi-unipi/unipi 2.0.3 → 2.0.5
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 +4 -4
- package/package.json +2 -2
- package/packages/autocomplete/README.md +1 -0
- package/packages/autocomplete/src/provider.ts +17 -2
- package/packages/compactor/README.md +290 -73
- package/packages/compactor/skills/compactor/SKILL.md +2 -3
- package/packages/compactor/skills/compactor-detail/SKILL.md +49 -64
- package/packages/compactor/skills/compactor-doctor/SKILL.md +28 -31
- package/packages/compactor/skills/compactor-stats/SKILL.md +22 -20
- package/packages/compactor/src/commands/index.ts +4 -1
- package/packages/compactor/src/compaction/auto-trigger.ts +306 -0
- package/packages/compactor/src/config/manager.ts +1 -0
- package/packages/compactor/src/config/presets.ts +26 -0
- package/packages/compactor/src/config/schema.ts +7 -0
- package/packages/compactor/src/index.ts +74 -1
- package/packages/compactor/src/tools/context-budget.ts +18 -2
- package/packages/compactor/src/tools/register.ts +19 -11
- package/packages/compactor/src/tui/settings-overlay.ts +142 -3
- package/packages/compactor/src/types.ts +17 -0
- package/packages/core/events.ts +2 -0
- package/packages/footer/README.md +12 -0
- package/packages/footer/src/config.ts +9 -1
- package/packages/footer/src/rendering/renderer.ts +14 -1
- package/packages/footer/src/rendering/theme.ts +187 -6
- package/packages/footer/src/types.ts +5 -0
- package/packages/notify/README.md +2 -2
- package/packages/notify/commands.ts +9 -4
- package/packages/notify/events.ts +12 -2
- package/packages/notify/platforms/focus-win.ts +123 -0
- package/packages/notify/platforms/focus.ts +33 -0
- package/packages/notify/platforms/native.ts +33 -1
- package/packages/notify/settings.ts +1 -0
- package/packages/notify/tui/settings-overlay.ts +33 -7
- package/packages/notify/types.ts +8 -0
- package/packages/utility/README.md +3 -0
- package/packages/utility/src/display/capabilities.ts +29 -15
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: compactor-detail
|
|
3
|
-
description: Full compactor reference — tool parameters, anti-patterns, sandbox languages,
|
|
3
|
+
description: Full compactor reference — tool parameters, anti-patterns, sandbox languages, context budget, workflows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Compactor — Full Reference
|
|
@@ -9,70 +9,65 @@ description: Full compactor reference — tool parameters, anti-patterns, sandbo
|
|
|
9
9
|
|
|
10
10
|
### compact
|
|
11
11
|
```
|
|
12
|
-
compact()
|
|
12
|
+
compact(dryRun?: boolean)
|
|
13
13
|
```
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Agent-facing compaction request. Use `dryRun: true` to preview estimated message reduction without compacting.
|
|
15
|
+
|
|
16
|
+
For user-triggered immediate compaction, prefer the slash command `/unipi:lossless-compact`.
|
|
16
17
|
|
|
17
18
|
### session_recall
|
|
18
19
|
```
|
|
19
20
|
session_recall(query: string, mode?: "bm25" | "regex", limit?: number, offset?: number, expand?: boolean)
|
|
20
21
|
```
|
|
21
|
-
Search session history. BM25 is default
|
|
22
|
+
Search current session history. BM25 is default; regex is fallback. The recall path prefers Pi's append-only session branch so it can find messages that are no longer present in live LLM context after compaction.
|
|
23
|
+
|
|
24
|
+
Deprecated alias: `vcc_recall`.
|
|
22
25
|
|
|
23
26
|
### sandbox
|
|
24
27
|
```
|
|
25
28
|
sandbox(language: string, code: string, timeout?: number)
|
|
26
29
|
```
|
|
27
|
-
Run code in sandboxed
|
|
30
|
+
Run code in a sandboxed environment. Only stdout enters context. 100MB output cap. 30s default timeout.
|
|
31
|
+
|
|
32
|
+
Deprecated alias: `ctx_execute`.
|
|
28
33
|
|
|
29
34
|
### sandbox_file
|
|
30
35
|
```
|
|
31
36
|
sandbox_file(language: string, path: string, timeout?: number)
|
|
32
37
|
```
|
|
33
|
-
Execute file.
|
|
38
|
+
Execute a file. File content is injected as `FILE_CONTENT`.
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
sandbox_batch(items: Array<{type: "execute", language, code} | {type: "search", query}>)
|
|
38
|
-
```
|
|
39
|
-
Atomic batch — all items run, results returned together.
|
|
40
|
+
Deprecated alias: `ctx_execute_file`.
|
|
40
41
|
|
|
41
|
-
###
|
|
42
|
+
### sandbox_batch
|
|
42
43
|
```
|
|
43
|
-
|
|
44
|
+
sandbox_batch(items: Array<{ type: "execute", language: string, code: string, timeout?: number }>)
|
|
44
45
|
```
|
|
45
|
-
|
|
46
|
+
Run multiple sandbox executions and return a single combined result.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
content_search(query: string, limit?: number, offset?: number)
|
|
50
|
-
```
|
|
51
|
-
Search FTS5 index. Returns ranked results with title, content, source, rank.
|
|
52
|
-
|
|
53
|
-
### content_fetch
|
|
54
|
-
```
|
|
55
|
-
content_fetch(url: string, label?: string, chunkSize?: number)
|
|
56
|
-
```
|
|
57
|
-
Fetch URL → markdown → index. Auto-indexes for later search.
|
|
48
|
+
Deprecated alias: `ctx_batch_execute`.
|
|
58
49
|
|
|
59
50
|
### compactor_stats
|
|
60
51
|
```
|
|
61
52
|
compactor_stats()
|
|
62
53
|
```
|
|
63
|
-
Dashboard: session events, compactions, tokens saved,
|
|
54
|
+
Dashboard: session events, compactions, tokens saved, compression ratio, sandbox runs, and search queries.
|
|
55
|
+
|
|
56
|
+
Deprecated alias: `ctx_stats`.
|
|
64
57
|
|
|
65
58
|
### compactor_doctor
|
|
66
59
|
```
|
|
67
60
|
compactor_doctor()
|
|
68
61
|
```
|
|
69
|
-
Diagnostics: config file, session DB,
|
|
62
|
+
Diagnostics: config file, session DB, and available runtimes (node, python3, bash).
|
|
63
|
+
|
|
64
|
+
Deprecated alias: `ctx_doctor`.
|
|
70
65
|
|
|
71
66
|
### context_budget
|
|
72
67
|
```
|
|
73
68
|
context_budget()
|
|
74
69
|
```
|
|
75
|
-
Estimate remaining context tokens and
|
|
70
|
+
Estimate remaining context tokens and percent full. Uses Pi's live context usage when available and includes percentage auto-compaction guidance when enabled.
|
|
76
71
|
|
|
77
72
|
## Sandbox Language Reference
|
|
78
73
|
|
|
@@ -90,44 +85,34 @@ Estimate remaining context tokens and % full. Returns guidance on whether to com
|
|
|
90
85
|
| r | Rscript | 30s | - |
|
|
91
86
|
| elixir | elixir | 30s | - |
|
|
92
87
|
|
|
93
|
-
## FTS5 Search Modes
|
|
94
|
-
|
|
95
|
-
| Mode | When To Use |
|
|
96
|
-
|------|-------------|
|
|
97
|
-
| **porter** | Exact term matching with stemming |
|
|
98
|
-
| **trigram** | Fuzzy/spelling errors, partial matches |
|
|
99
|
-
| **rrf** | Best overall (Reciprocal Rank Fusion of porter+trigram) |
|
|
100
|
-
| **fuzzy** | Auto-correction of misspellings from vocabulary |
|
|
101
|
-
|
|
102
|
-
Default: `rrf` (best general-purpose).
|
|
103
|
-
|
|
104
88
|
## Anti-Patterns
|
|
105
89
|
|
|
106
|
-
1. **Don't
|
|
107
|
-
2. **Don't
|
|
108
|
-
3. **Don't use `sandbox` for file
|
|
109
|
-
4. **Don't use
|
|
110
|
-
5. **Don't
|
|
111
|
-
6. **Don't compact mid-task.** Wait for a natural break point.
|
|
90
|
+
1. **Don't compact in a tight loop.** Use `context_budget` first and compact at natural break points.
|
|
91
|
+
2. **Don't use `session_recall` for project-wide code search.** It searches the conversation/session. Use CocoIndex (`cocoindex_search`) for indexed files when installed.
|
|
92
|
+
3. **Don't use `sandbox` for file operations.** Use normal file tools for reads/writes; sandbox is for computation or quick scripts.
|
|
93
|
+
4. **Don't use empty or vague recall queries.** Search for specific filenames, issue numbers, commands, or decisions.
|
|
94
|
+
5. **Don't compact mid-thought if avoidable.** Finish the current step, then compact.
|
|
112
95
|
|
|
113
96
|
## Workflow Patterns
|
|
114
97
|
|
|
115
|
-
### Research → Index → Search → Test
|
|
116
|
-
1. `content_fetch(url)` — index reference docs
|
|
117
|
-
2. `content_search(query)` — find relevant sections
|
|
118
|
-
3. `sandbox(lang, code)` — test hypotheses
|
|
119
|
-
|
|
120
|
-
### Diagnose → Fix → Verify
|
|
121
|
-
1. `compactor_doctor` — check system health
|
|
122
|
-
2. Fix issues (install runtimes, rebuild index)
|
|
123
|
-
3. `compactor_stats` — verify metrics
|
|
124
|
-
|
|
125
98
|
### Before Complex Work
|
|
126
|
-
1. `
|
|
127
|
-
2. `
|
|
128
|
-
3. `
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
99
|
+
1. `context_budget` — check pressure.
|
|
100
|
+
2. `session_recall("goal files decisions")` — recover relevant prior context.
|
|
101
|
+
3. `compact(dryRun: true)` — preview if context is tight.
|
|
102
|
+
4. `/unipi:lossless-compact` or `compact` at a clean boundary.
|
|
103
|
+
|
|
104
|
+
### Diagnose Compactor Health
|
|
105
|
+
1. `compactor_doctor` — check config, DB, runtimes.
|
|
106
|
+
2. Fix reported issues.
|
|
107
|
+
3. `compactor_stats` — verify counters and savings.
|
|
108
|
+
|
|
109
|
+
### Long Session Recovery
|
|
110
|
+
1. `session_recall(query)` — find older details hidden by compaction.
|
|
111
|
+
2. `context_budget` — decide whether another compaction is needed.
|
|
112
|
+
3. `compactor_stats` — confirm compactions/savings are recorded.
|
|
113
|
+
|
|
114
|
+
### Project Search
|
|
115
|
+
Compactor no longer owns project content indexing. Use the CocoIndex package if installed:
|
|
116
|
+
1. `/unipi:cocoindex-init`
|
|
117
|
+
2. `/unipi:cocoindex-update`
|
|
118
|
+
3. `cocoindex_search(query)`
|
|
@@ -1,55 +1,53 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: compactor-doctor
|
|
3
|
-
description: Diagnostics — validate config, DB,
|
|
3
|
+
description: Diagnostics — validate config, session DB, runtimes, and troubleshoot compactor issues.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Compactor Doctor
|
|
7
7
|
|
|
8
8
|
Run diagnostics and troubleshoot compactor issues.
|
|
9
9
|
|
|
10
|
-
## Commands
|
|
10
|
+
## Commands and Tools
|
|
11
11
|
|
|
12
|
-
- `/unipi:compact-doctor` —
|
|
13
|
-
- `
|
|
12
|
+
- `/unipi:compact-doctor` — user-facing diagnostics
|
|
13
|
+
- `compactor_doctor` tool — agent-callable diagnostics
|
|
14
|
+
- `ctx_doctor` tool — deprecated alias
|
|
14
15
|
|
|
15
16
|
## Checks Performed
|
|
16
17
|
|
|
17
18
|
| Check | What It Validates |
|
|
18
19
|
|-------|-------------------|
|
|
19
|
-
| **Config file** | `~/.unipi/config/compactor/config.json` exists
|
|
20
|
-
| **Session DB** | SQLite connection works
|
|
21
|
-
| **
|
|
22
|
-
| **Runtime:
|
|
23
|
-
| **Runtime:
|
|
24
|
-
| **Runtime: bash** | Bash available for sandbox |
|
|
20
|
+
| **Config file** | `~/.unipi/config/compactor/config.json` exists or defaults can be used |
|
|
21
|
+
| **Session DB** | SQLite connection works and session schema is usable |
|
|
22
|
+
| **Runtime: node** | Node.js available for sandbox/runtime helpers |
|
|
23
|
+
| **Runtime: python3** | Python 3 available for Python sandbox execution |
|
|
24
|
+
| **Runtime: bash** | Bash available for shell sandbox execution |
|
|
25
25
|
|
|
26
26
|
## Status Icons
|
|
27
27
|
|
|
28
28
|
- ✅ **pass** — check succeeded
|
|
29
|
-
- ⚠️ **warn** — non-critical issue
|
|
30
|
-
- ❌ **fail** — critical issue
|
|
29
|
+
- ⚠️ **warn** — non-critical issue, such as an optional runtime missing
|
|
30
|
+
- ❌ **fail** — critical issue; a feature may not work
|
|
31
31
|
|
|
32
32
|
## Common Issues
|
|
33
33
|
|
|
34
34
|
### "Config file: Using defaults"
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
35
|
+
|
|
36
|
+
- Normal on first run.
|
|
37
|
+
- Config is created by the extension or on settings save.
|
|
38
|
+
- Fix: open `/unipi:compact-settings`, adjust if desired, then save.
|
|
38
39
|
|
|
39
40
|
### "Session DB: Connection failed"
|
|
40
|
-
- SQLite not available
|
|
41
|
-
- Check if `better-sqlite3` is installed
|
|
42
|
-
- Fix: `npm install better-sqlite3`
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
- Fix: Update system SQLite
|
|
42
|
+
- SQLite or `better-sqlite3` may be unavailable.
|
|
43
|
+
- Compaction can still degrade gracefully, but stats/recall may be limited.
|
|
44
|
+
- Fix: verify dependencies and run package install/build steps.
|
|
48
45
|
|
|
49
46
|
### "Runtime: python3 Not found"
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
47
|
+
|
|
48
|
+
- Python is not installed or not in `PATH`.
|
|
49
|
+
- Only needed for Python sandbox execution.
|
|
50
|
+
- Fix: install Python 3 or ignore if not needed.
|
|
53
51
|
|
|
54
52
|
## Manual Diagnostics
|
|
55
53
|
|
|
@@ -63,12 +61,11 @@ cat ~/.unipi/config/compactor/config.json
|
|
|
63
61
|
ls -la ~/.unipi/db/compactor/
|
|
64
62
|
```
|
|
65
63
|
|
|
66
|
-
### Check
|
|
64
|
+
### Check runtimes
|
|
67
65
|
```bash
|
|
68
|
-
|
|
66
|
+
node --version
|
|
67
|
+
python3 --version
|
|
68
|
+
bash --version
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
sqlite3 ':memory:' "CREATE VIRTUAL TABLE t USING fts5(x); SELECT 1;"
|
|
74
|
-
```
|
|
71
|
+
Project content indexing diagnostics live in the CocoIndex package, not compactor.
|
|
@@ -1,49 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: compactor-stats
|
|
3
|
-
description: Stats display — context savings, session metrics,
|
|
3
|
+
description: Stats display — context savings, session metrics, compactions, sandbox and recall/search counters.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Compactor Stats
|
|
7
7
|
|
|
8
8
|
Display and interpret compactor statistics.
|
|
9
9
|
|
|
10
|
-
## Commands
|
|
10
|
+
## Commands and Tools
|
|
11
11
|
|
|
12
|
-
- `/unipi:compact-stats` —
|
|
13
|
-
- `
|
|
12
|
+
- `/unipi:compact-stats` — user-facing dashboard
|
|
13
|
+
- `compactor_stats` tool — agent-callable stats
|
|
14
|
+
- `ctx_stats` tool — deprecated alias
|
|
14
15
|
|
|
15
16
|
## Metrics Explained
|
|
16
17
|
|
|
17
18
|
| Metric | Description |
|
|
18
19
|
|--------|-------------|
|
|
19
|
-
| **Session events** |
|
|
20
|
-
| **Compactions** | Number of
|
|
21
|
-
| **Tokens saved** | Estimated tokens freed by compaction |
|
|
22
|
-
| **
|
|
23
|
-
| **
|
|
24
|
-
| **
|
|
25
|
-
| **Search queries** | FTS5 searches performed |
|
|
20
|
+
| **Session events** | Events tracked for the current session in the compactor session DB |
|
|
21
|
+
| **Compactions** | Number of compaction events recorded |
|
|
22
|
+
| **Tokens saved** | Estimated tokens freed by compaction, using Pi token counts when available and DB fallbacks otherwise |
|
|
23
|
+
| **Compression ratio** | Approximate before/kept ratio from stored compaction stats |
|
|
24
|
+
| **Sandbox runs** | Agent sandbox executions recorded in runtime counters/DB |
|
|
25
|
+
| **Search queries** | Session recall/search queries recorded in runtime counters/DB |
|
|
26
26
|
|
|
27
27
|
## Health Indicators
|
|
28
28
|
|
|
29
|
-
- **Compactions > 0** on long sessions =
|
|
30
|
-
- **
|
|
31
|
-
- **Sandbox runs** =
|
|
29
|
+
- **Compactions > 0** on long sessions = compaction is happening.
|
|
30
|
+
- **Tokens saved > 0** after compaction = savings were recorded.
|
|
31
|
+
- **Sandbox runs** increasing = sandbox tool usage is tracked.
|
|
32
|
+
- **Search queries** increasing = recall/search activity is tracked.
|
|
32
33
|
|
|
33
34
|
## Reading Stats
|
|
34
35
|
|
|
35
|
-
```
|
|
36
|
+
```text
|
|
36
37
|
📊 Compactor Stats
|
|
37
38
|
Session events: 42
|
|
38
39
|
Compactions: 3
|
|
39
40
|
Tokens saved: 15000
|
|
40
|
-
Indexed docs: 12 (48 chunks)
|
|
41
41
|
Sandbox runs: 7
|
|
42
42
|
Search queries: 15
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
This means:
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- 7
|
|
46
|
+
|
|
47
|
+
- 42 events are tracked for the session.
|
|
48
|
+
- 3 compactions saved roughly 15K tokens.
|
|
49
|
+
- 7 sandbox executions and 15 recall/search queries were recorded.
|
|
50
|
+
|
|
51
|
+
Project content indexing stats are not owned by compactor anymore; use the CocoIndex package for indexed file search/status.
|
|
@@ -229,9 +229,12 @@ export function registerCommands(pi: ExtensionAPI, deps?: CommandDeps): void {
|
|
|
229
229
|
" /unipi:session-recall <query> — search session history\n" +
|
|
230
230
|
" /unipi:compact-stats — view stats\n" +
|
|
231
231
|
" /unipi:compact-doctor — run diagnostics\n" +
|
|
232
|
-
" /unipi:compact-settings — TUI settings\n" +
|
|
232
|
+
" /unipi:compact-settings — TUI settings, including optional % auto-compaction\n" +
|
|
233
233
|
" /unipi:compact-preset <name> — apply preset\n" +
|
|
234
234
|
"\n" +
|
|
235
|
+
"Percentage trigger:\n" +
|
|
236
|
+
" Disabled by default. Enable in /unipi:compact-settings to compact at a context % before Pi's reserve-token limit.\n" +
|
|
237
|
+
"\n" +
|
|
235
238
|
"Content indexing has moved to @pi-unipi/cocoindex:\n" +
|
|
236
239
|
" /unipi:cocoindex-init — initialize pipeline\n" +
|
|
237
240
|
" /unipi:cocoindex-update — index project files\n" +
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure percentage auto-compaction trigger decisions.
|
|
3
|
+
*
|
|
4
|
+
* The runtime wiring owns Pi ctx access; this module only decides whether a
|
|
5
|
+
* known context-usage sample should trigger UniPi's zero-LLM compaction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AutoCompactionConfig } from "../types.js";
|
|
9
|
+
|
|
10
|
+
export interface AutoCompactionUsage {
|
|
11
|
+
tokens?: number | null;
|
|
12
|
+
percent?: number | null;
|
|
13
|
+
contextWindow?: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface KnownAutoCompactionUsage {
|
|
17
|
+
tokens: number;
|
|
18
|
+
percent: number;
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AutoCompactionState {
|
|
23
|
+
/** Previous known context percentage sample. Null before first known usage. */
|
|
24
|
+
previousPercent: number | null;
|
|
25
|
+
/** Previous known context token sample. Null before first known usage. */
|
|
26
|
+
previousTokens: number | null;
|
|
27
|
+
/** True after UniPi calls ctx.compact() and before Pi reports completion/error. */
|
|
28
|
+
inFlight: boolean;
|
|
29
|
+
/** Timestamp of the last UniPi-triggered compaction attempt. */
|
|
30
|
+
lastTriggerAt: number | null;
|
|
31
|
+
/** Context tokens reported at the last UniPi-triggered compaction attempt. */
|
|
32
|
+
lastTriggerTokens: number | null;
|
|
33
|
+
/** Baseline used to require meaningful token growth for repeat high-usage triggers. */
|
|
34
|
+
repeatBaselineTokens: number | null;
|
|
35
|
+
/** After a successful compaction, consume one known usage sample as a fresh baseline. */
|
|
36
|
+
awaitingPostCompactionSample: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type AutoCompactionDecisionReason =
|
|
40
|
+
| "disabled"
|
|
41
|
+
| "unknown_usage"
|
|
42
|
+
| "in_flight"
|
|
43
|
+
| "below_threshold"
|
|
44
|
+
| "post_compaction_baseline"
|
|
45
|
+
| "threshold_reached"
|
|
46
|
+
| "threshold_crossed"
|
|
47
|
+
| "cooldown_active"
|
|
48
|
+
| "repeat_growth_needed"
|
|
49
|
+
| "repeat_growth_reached";
|
|
50
|
+
|
|
51
|
+
export interface AutoCompactionDecision {
|
|
52
|
+
shouldTrigger: boolean;
|
|
53
|
+
reason: AutoCompactionDecisionReason;
|
|
54
|
+
state: AutoCompactionState;
|
|
55
|
+
thresholdPercent: number;
|
|
56
|
+
usage?: KnownAutoCompactionUsage;
|
|
57
|
+
cooldownRemainingMs?: number;
|
|
58
|
+
tokenGrowth?: number;
|
|
59
|
+
tokensUntilRepeat?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AutoCompactionDecisionInput {
|
|
63
|
+
config?: Partial<AutoCompactionConfig> | null;
|
|
64
|
+
usage?: AutoCompactionUsage | null;
|
|
65
|
+
state: AutoCompactionState;
|
|
66
|
+
nowMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const AUTO_COMPACTION_DEFAULTS: AutoCompactionConfig = {
|
|
70
|
+
enabled: false,
|
|
71
|
+
thresholdPercent: 80,
|
|
72
|
+
cooldownMs: 60_000,
|
|
73
|
+
repeatMinGrowthTokens: 4_000,
|
|
74
|
+
notify: true,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const clamp = (value: unknown, fallback: number, min: number, max: number): number => {
|
|
78
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
79
|
+
return Math.min(max, Math.max(min, value));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function normalizeAutoCompactionConfig(config?: Partial<AutoCompactionConfig> | null): AutoCompactionConfig {
|
|
83
|
+
return {
|
|
84
|
+
enabled: config?.enabled ?? AUTO_COMPACTION_DEFAULTS.enabled,
|
|
85
|
+
thresholdPercent: clamp(
|
|
86
|
+
config?.thresholdPercent,
|
|
87
|
+
AUTO_COMPACTION_DEFAULTS.thresholdPercent,
|
|
88
|
+
1,
|
|
89
|
+
99,
|
|
90
|
+
),
|
|
91
|
+
cooldownMs: Math.round(clamp(
|
|
92
|
+
config?.cooldownMs,
|
|
93
|
+
AUTO_COMPACTION_DEFAULTS.cooldownMs,
|
|
94
|
+
0,
|
|
95
|
+
24 * 60 * 60 * 1000,
|
|
96
|
+
)),
|
|
97
|
+
repeatMinGrowthTokens: Math.round(clamp(
|
|
98
|
+
config?.repeatMinGrowthTokens,
|
|
99
|
+
AUTO_COMPACTION_DEFAULTS.repeatMinGrowthTokens,
|
|
100
|
+
0,
|
|
101
|
+
10_000_000,
|
|
102
|
+
)),
|
|
103
|
+
notify: config?.notify ?? AUTO_COMPACTION_DEFAULTS.notify,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createAutoCompactionState(): AutoCompactionState {
|
|
108
|
+
return {
|
|
109
|
+
previousPercent: null,
|
|
110
|
+
previousTokens: null,
|
|
111
|
+
inFlight: false,
|
|
112
|
+
lastTriggerAt: null,
|
|
113
|
+
lastTriggerTokens: null,
|
|
114
|
+
repeatBaselineTokens: null,
|
|
115
|
+
awaitingPostCompactionSample: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function knownUsage(usage?: AutoCompactionUsage | null): KnownAutoCompactionUsage | null {
|
|
120
|
+
if (!usage) return null;
|
|
121
|
+
const { tokens, percent, contextWindow } = usage;
|
|
122
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens < 0) return null;
|
|
123
|
+
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0) return null;
|
|
124
|
+
const known: KnownAutoCompactionUsage = { tokens, percent };
|
|
125
|
+
if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
|
|
126
|
+
known.contextWindow = contextWindow;
|
|
127
|
+
}
|
|
128
|
+
return known;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function updatePrevious(state: AutoCompactionState, usage: KnownAutoCompactionUsage): AutoCompactionState {
|
|
132
|
+
return {
|
|
133
|
+
...state,
|
|
134
|
+
previousPercent: usage.percent,
|
|
135
|
+
previousTokens: usage.tokens,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function trigger(
|
|
140
|
+
reason: Extract<AutoCompactionDecisionReason, "threshold_reached" | "threshold_crossed" | "repeat_growth_reached">,
|
|
141
|
+
state: AutoCompactionState,
|
|
142
|
+
usage: KnownAutoCompactionUsage,
|
|
143
|
+
thresholdPercent: number,
|
|
144
|
+
nowMs: number,
|
|
145
|
+
tokenGrowth?: number,
|
|
146
|
+
): AutoCompactionDecision {
|
|
147
|
+
return {
|
|
148
|
+
shouldTrigger: true,
|
|
149
|
+
reason,
|
|
150
|
+
thresholdPercent,
|
|
151
|
+
usage,
|
|
152
|
+
tokenGrowth,
|
|
153
|
+
state: {
|
|
154
|
+
...updatePrevious(state, usage),
|
|
155
|
+
inFlight: true,
|
|
156
|
+
lastTriggerAt: nowMs,
|
|
157
|
+
lastTriggerTokens: usage.tokens,
|
|
158
|
+
repeatBaselineTokens: usage.tokens,
|
|
159
|
+
awaitingPostCompactionSample: false,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Decide whether the current usage sample should trigger auto-compaction.
|
|
166
|
+
*
|
|
167
|
+
* Loop safeguards:
|
|
168
|
+
* - unknown/null usage never triggers and does not erase the previous baseline;
|
|
169
|
+
* - in-flight compactions suppress further triggers;
|
|
170
|
+
* - every repeat trigger after a prior attempt observes cooldown;
|
|
171
|
+
* - if usage remains above threshold after compaction, the first known sample is
|
|
172
|
+
* only a baseline, and a repeat requires sufficient token growth.
|
|
173
|
+
*/
|
|
174
|
+
export function decideAutoCompaction(input: AutoCompactionDecisionInput): AutoCompactionDecision {
|
|
175
|
+
const config = normalizeAutoCompactionConfig(input.config);
|
|
176
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
177
|
+
|
|
178
|
+
if (!config.enabled) {
|
|
179
|
+
return {
|
|
180
|
+
shouldTrigger: false,
|
|
181
|
+
reason: "disabled",
|
|
182
|
+
thresholdPercent: config.thresholdPercent,
|
|
183
|
+
state: input.state,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (input.state.inFlight) {
|
|
188
|
+
return {
|
|
189
|
+
shouldTrigger: false,
|
|
190
|
+
reason: "in_flight",
|
|
191
|
+
thresholdPercent: config.thresholdPercent,
|
|
192
|
+
state: input.state,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const usage = knownUsage(input.usage);
|
|
197
|
+
if (!usage) {
|
|
198
|
+
return {
|
|
199
|
+
shouldTrigger: false,
|
|
200
|
+
reason: "unknown_usage",
|
|
201
|
+
thresholdPercent: config.thresholdPercent,
|
|
202
|
+
state: input.state,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let nextState = updatePrevious(input.state, usage);
|
|
207
|
+
|
|
208
|
+
if (input.state.awaitingPostCompactionSample) {
|
|
209
|
+
nextState = {
|
|
210
|
+
...nextState,
|
|
211
|
+
awaitingPostCompactionSample: false,
|
|
212
|
+
repeatBaselineTokens: usage.percent >= config.thresholdPercent ? usage.tokens : null,
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
shouldTrigger: false,
|
|
216
|
+
reason: usage.percent >= config.thresholdPercent ? "post_compaction_baseline" : "below_threshold",
|
|
217
|
+
thresholdPercent: config.thresholdPercent,
|
|
218
|
+
usage,
|
|
219
|
+
state: nextState,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (usage.percent < config.thresholdPercent) {
|
|
224
|
+
return {
|
|
225
|
+
shouldTrigger: false,
|
|
226
|
+
reason: "below_threshold",
|
|
227
|
+
thresholdPercent: config.thresholdPercent,
|
|
228
|
+
usage,
|
|
229
|
+
state: {
|
|
230
|
+
...nextState,
|
|
231
|
+
repeatBaselineTokens: null,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (input.state.lastTriggerAt !== null) {
|
|
237
|
+
const elapsedMs = Math.max(0, nowMs - input.state.lastTriggerAt);
|
|
238
|
+
if (elapsedMs < config.cooldownMs) {
|
|
239
|
+
return {
|
|
240
|
+
shouldTrigger: false,
|
|
241
|
+
reason: "cooldown_active",
|
|
242
|
+
thresholdPercent: config.thresholdPercent,
|
|
243
|
+
usage,
|
|
244
|
+
cooldownRemainingMs: config.cooldownMs - elapsedMs,
|
|
245
|
+
state: nextState,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const previousPercent = input.state.previousPercent;
|
|
251
|
+
if (previousPercent === null) {
|
|
252
|
+
return trigger("threshold_reached", input.state, usage, config.thresholdPercent, nowMs);
|
|
253
|
+
}
|
|
254
|
+
if (previousPercent < config.thresholdPercent) {
|
|
255
|
+
return trigger("threshold_crossed", input.state, usage, config.thresholdPercent, nowMs);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// We were already above threshold. Re-trigger only after enough growth.
|
|
259
|
+
if (input.state.lastTriggerAt === null) {
|
|
260
|
+
return trigger("threshold_reached", input.state, usage, config.thresholdPercent, nowMs);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const baselineTokens = input.state.repeatBaselineTokens
|
|
264
|
+
?? input.state.lastTriggerTokens
|
|
265
|
+
?? input.state.previousTokens
|
|
266
|
+
?? usage.tokens;
|
|
267
|
+
const tokenGrowth = Math.max(0, usage.tokens - baselineTokens);
|
|
268
|
+
|
|
269
|
+
if (tokenGrowth < config.repeatMinGrowthTokens) {
|
|
270
|
+
return {
|
|
271
|
+
shouldTrigger: false,
|
|
272
|
+
reason: "repeat_growth_needed",
|
|
273
|
+
thresholdPercent: config.thresholdPercent,
|
|
274
|
+
usage,
|
|
275
|
+
tokenGrowth,
|
|
276
|
+
tokensUntilRepeat: config.repeatMinGrowthTokens - tokenGrowth,
|
|
277
|
+
state: nextState,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return trigger(
|
|
282
|
+
"repeat_growth_reached",
|
|
283
|
+
input.state,
|
|
284
|
+
usage,
|
|
285
|
+
config.thresholdPercent,
|
|
286
|
+
nowMs,
|
|
287
|
+
tokenGrowth,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function markAutoCompactionComplete(state: AutoCompactionState): AutoCompactionState {
|
|
292
|
+
return {
|
|
293
|
+
...state,
|
|
294
|
+
inFlight: false,
|
|
295
|
+
awaitingPostCompactionSample: true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function markAutoCompactionError(state: AutoCompactionState, nowMs?: number): AutoCompactionState {
|
|
300
|
+
return {
|
|
301
|
+
...state,
|
|
302
|
+
inFlight: false,
|
|
303
|
+
awaitingPostCompactionSample: false,
|
|
304
|
+
lastTriggerAt: state.lastTriggerAt ?? nowMs ?? null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -116,6 +116,7 @@ export function migrateConfig(partial: Partial<CompactorConfig>): CompactorConfi
|
|
|
116
116
|
sandboxExecution: mergeStrategy("sandboxExecution", defaults.sandboxExecution, partial.sandboxExecution),
|
|
117
117
|
toolDisplay: mergeStrategy("toolDisplay", defaults.toolDisplay, partial.toolDisplay),
|
|
118
118
|
pipeline: mergeStrategy("pipeline", defaults.pipeline, (partial as any).pipeline) as any,
|
|
119
|
+
autoCompaction: mergeStrategy("autoCompaction", defaults.autoCompaction, partial.autoCompaction),
|
|
119
120
|
overrideDefaultCompaction: partial.overrideDefaultCompaction ?? defaults.overrideDefaultCompaction,
|
|
120
121
|
debug: partial.debug ?? defaults.debug,
|
|
121
122
|
showTruncationHints: partial.showTruncationHints ?? defaults.showTruncationHints,
|