token-optimizer-opencode 1.0.3

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.
Files changed (90) hide show
  1. package/README.md +127 -0
  2. package/dist/activity/intel.d.ts +10 -0
  3. package/dist/activity/intel.d.ts.map +1 -0
  4. package/dist/activity/intel.js +26 -0
  5. package/dist/activity/intel.js.map +1 -0
  6. package/dist/activity/tracker.d.ts +12 -0
  7. package/dist/activity/tracker.d.ts.map +1 -0
  8. package/dist/activity/tracker.js +101 -0
  9. package/dist/activity/tracker.js.map +1 -0
  10. package/dist/compaction/checkpoint.d.ts +17 -0
  11. package/dist/compaction/checkpoint.d.ts.map +1 -0
  12. package/dist/compaction/checkpoint.js +82 -0
  13. package/dist/compaction/checkpoint.js.map +1 -0
  14. package/dist/compaction/dynamic-instructions.d.ts +3 -0
  15. package/dist/compaction/dynamic-instructions.d.ts.map +1 -0
  16. package/dist/compaction/dynamic-instructions.js +54 -0
  17. package/dist/compaction/dynamic-instructions.js.map +1 -0
  18. package/dist/continuity/matcher.d.ts +14 -0
  19. package/dist/continuity/matcher.d.ts.map +1 -0
  20. package/dist/continuity/matcher.js +57 -0
  21. package/dist/continuity/matcher.js.map +1 -0
  22. package/dist/continuity/restore.d.ts +4 -0
  23. package/dist/continuity/restore.d.ts.map +1 -0
  24. package/dist/continuity/restore.js +53 -0
  25. package/dist/continuity/restore.js.map +1 -0
  26. package/dist/dashboard/generator.d.ts +8 -0
  27. package/dist/dashboard/generator.d.ts.map +1 -0
  28. package/dist/dashboard/generator.js +282 -0
  29. package/dist/dashboard/generator.js.map +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +447 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/nudges/loop-detection.d.ts +6 -0
  35. package/dist/nudges/loop-detection.d.ts.map +1 -0
  36. package/dist/nudges/loop-detection.js +56 -0
  37. package/dist/nudges/loop-detection.js.map +1 -0
  38. package/dist/nudges/quality-nudge.d.ts +7 -0
  39. package/dist/nudges/quality-nudge.d.ts.map +1 -0
  40. package/dist/nudges/quality-nudge.js +28 -0
  41. package/dist/nudges/quality-nudge.js.map +1 -0
  42. package/dist/nudges/tool-call-warn.d.ts +7 -0
  43. package/dist/nudges/tool-call-warn.d.ts.map +1 -0
  44. package/dist/nudges/tool-call-warn.js +20 -0
  45. package/dist/nudges/tool-call-warn.js.map +1 -0
  46. package/dist/plugin.d.ts +9 -0
  47. package/dist/plugin.d.ts.map +1 -0
  48. package/dist/plugin.js +5 -0
  49. package/dist/plugin.js.map +1 -0
  50. package/dist/quality/curves.d.ts +5 -0
  51. package/dist/quality/curves.d.ts.map +1 -0
  52. package/dist/quality/curves.js +102 -0
  53. package/dist/quality/curves.js.map +1 -0
  54. package/dist/quality/scoring.d.ts +39 -0
  55. package/dist/quality/scoring.d.ts.map +1 -0
  56. package/dist/quality/scoring.js +239 -0
  57. package/dist/quality/scoring.js.map +1 -0
  58. package/dist/quality/signals.d.ts +23 -0
  59. package/dist/quality/signals.d.ts.map +1 -0
  60. package/dist/quality/signals.js +100 -0
  61. package/dist/quality/signals.js.map +1 -0
  62. package/dist/storage/session-store.d.ts +73 -0
  63. package/dist/storage/session-store.d.ts.map +1 -0
  64. package/dist/storage/session-store.js +259 -0
  65. package/dist/storage/session-store.js.map +1 -0
  66. package/dist/storage/trends.d.ts +28 -0
  67. package/dist/storage/trends.d.ts.map +1 -0
  68. package/dist/storage/trends.js +125 -0
  69. package/dist/storage/trends.js.map +1 -0
  70. package/dist/tools/dashboard.d.ts +3 -0
  71. package/dist/tools/dashboard.d.ts.map +1 -0
  72. package/dist/tools/dashboard.js +45 -0
  73. package/dist/tools/dashboard.js.map +1 -0
  74. package/dist/tools/token-status.d.ts +9 -0
  75. package/dist/tools/token-status.d.ts.map +1 -0
  76. package/dist/tools/token-status.js +51 -0
  77. package/dist/tools/token-status.js.map +1 -0
  78. package/dist/util/context-window.d.ts +2 -0
  79. package/dist/util/context-window.d.ts.map +1 -0
  80. package/dist/util/context-window.js +83 -0
  81. package/dist/util/context-window.js.map +1 -0
  82. package/dist/util/env.d.ts +23 -0
  83. package/dist/util/env.d.ts.map +1 -0
  84. package/dist/util/env.js +63 -0
  85. package/dist/util/env.js.map +1 -0
  86. package/dist/util/grade.d.ts +4 -0
  87. package/dist/util/grade.d.ts.map +1 -0
  88. package/dist/util/grade.js +32 -0
  89. package/dist/util/grade.js.map +1 -0
  90. package/package.json +74 -0
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Token Optimizer for OpenCode
2
+
3
+ Context quality scoring, smart compaction, and session continuity for [OpenCode](https://github.com/anomalyco/opencode). Full parity with the Claude Code Token Optimizer plugin.
4
+
5
+ ## What It Does
6
+
7
+ Token Optimizer monitors your OpenCode sessions and helps you get the most out of your context window:
8
+
9
+ - **7-signal quality scoring** with dual ResourceHealth (monotonic) + SessionEfficiency (rolling window) architecture
10
+ - **Smart compaction** with mode-aware PRESERVE/DROP guidance (code/debug/review/infra/general)
11
+ - **Session continuity** that restores context from prior sessions via keyword matching
12
+ - **Quality nudges** that warn when context health drops, fill exceeds thresholds, or retry loops are detected
13
+ - **Dashboard** with quality trends, session history, and daily aggregates
14
+ - **`token_status` tool** for on-demand quality reports
15
+ - **`token_dashboard` tool** to generate and open the visual dashboard
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ opencode plugin token-optimizer-opencode
21
+ ```
22
+
23
+ Or add it to your `opencode.json` (or `.opencode/opencode.jsonc`) plugin array:
24
+
25
+ ```jsonc
26
+ {
27
+ "plugin": ["token-optimizer-opencode"]
28
+ }
29
+ ```
30
+
31
+ ### Offline / no-npm install
32
+
33
+ If you can't (or don't want to) install from npm, clone the repo and run the
34
+ bundled installer. It builds a single-file plugin and copies it into
35
+ `~/.config/opencode/plugins/`, which OpenCode auto-loads at startup:
36
+
37
+ ```bash
38
+ git clone https://github.com/alexgreensh/token-optimizer.git
39
+ token-optimizer/install.sh --opencode
40
+ ```
41
+
42
+ Requires [bun](https://bun.sh) (OpenCode's own runtime). Re-run after a
43
+ `git pull` to update.
44
+
45
+ ## Configure
46
+
47
+ Add plugin options in `.opencode/opencode.jsonc`:
48
+
49
+ ```jsonc
50
+ {
51
+ "plugin": [
52
+ ["token-optimizer-opencode", {
53
+ "qualityWindow": 20,
54
+ "features": {
55
+ "qualityNudges": true,
56
+ "loopDetection": true,
57
+ "smartCompaction": true,
58
+ "continuity": true,
59
+ "activityTracking": true,
60
+ "trends": true
61
+ }
62
+ }]
63
+ ]
64
+ }
65
+ ```
66
+
67
+ All options are optional. Defaults are shown above.
68
+
69
+ ## Environment Variables
70
+
71
+ Override any threshold via environment variables:
72
+
73
+ | Variable | Default | Description |
74
+ |----------|---------|-------------|
75
+ | `TOKEN_OPTIMIZER_QUALITY_WINDOW` | 20 | Rolling window size for ratio signals |
76
+ | `TOKEN_OPTIMIZER_TOOL_CALL_WARN` | auto | Tool call warning threshold (scales with context window) |
77
+ | `TOKEN_OPTIMIZER_TOOL_CALL_CRITICAL` | auto | Tool call critical threshold |
78
+ | `TOKEN_OPTIMIZER_CHECKPOINT_RETENTION_DAYS` | 7 | Days to keep checkpoints |
79
+ | `TOKEN_OPTIMIZER_CHECKPOINT_RETENTION_MAX` | 50 | Max checkpoints to scan for restore |
80
+ | `TOKEN_OPTIMIZER_RELEVANCE_THRESHOLD` | 0.3 | Min relevance score for checkpoint restore |
81
+ | `TOKEN_OPTIMIZER_NUDGES` | true | Enable quality nudges |
82
+ | `TOKEN_OPTIMIZER_LOOP_DETECTION` | true | Enable retry loop detection |
83
+ | `TOKEN_OPTIMIZER_SMART_COMPACTION` | true | Enable compaction context injection |
84
+ | `TOKEN_OPTIMIZER_CONTINUITY` | true | Enable session continuity |
85
+ | `TOKEN_OPTIMIZER_ACTIVITY` | true | Enable activity tracking |
86
+ | `TOKEN_OPTIMIZER_TRENDS` | true | Enable trends collection |
87
+
88
+ ## Quality Scoring
89
+
90
+ The quality score uses a dual-composite architecture:
91
+
92
+ **ResourceHealth** (monotonic, can only decrease within a session):
93
+ - Context fill degradation (50%) - MRCR-curve-based quality estimate
94
+ - Compaction depth (30%) - information loss from repeated compaction
95
+ - Absolute waste tokens (20%) - stale reads + bloated results
96
+
97
+ **SessionEfficiency** (rolling window, can rise or fall):
98
+ - Stale reads (30%) - re-reading files after writing them
99
+ - Bloated results (30%) - large tool outputs never referenced
100
+ - Decision density (20%) - ratio of substantive messages
101
+ - Agent efficiency (20%) - agent dispatch result/prompt ratio
102
+
103
+ Grades: S (90+), A (80+), B (70+), C (55+), D (40+), F (<40)
104
+
105
+ ## Hooks Used
106
+
107
+ | Hook | Purpose |
108
+ |------|---------|
109
+ | `chat.message` | Track user messages, trigger quality scoring |
110
+ | `tool.execute.before` | Record file reads |
111
+ | `tool.execute.after` | Record tool results, file writes, agent dispatches, activity tracking |
112
+ | `experimental.chat.system.transform` | Inject warnings, restore session continuity |
113
+ | `experimental.session.compacting` | Inject mode-aware compaction guidance, capture checkpoint |
114
+ | `experimental.compaction.autocontinue` | Reset signals post-compaction, refresh quality |
115
+ | `event` | Handle session lifecycle (created/deleted) |
116
+
117
+ ## Model Support
118
+
119
+ Context window sizes are mapped for 30+ models across all major providers:
120
+ Anthropic (Opus/Sonnet 1M, Haiku 200K), OpenAI (GPT-5.x, GPT-4.1, o3/o4), Google (Gemini 2.x/3.x),
121
+ DeepSeek, Qwen, Mistral, xAI Grok, and more.
122
+
123
+ MRCR quality curves are calibrated per model family for accurate fill-degradation estimates.
124
+
125
+ ## License
126
+
127
+ PolyForm Noncommercial 1.0.0
@@ -0,0 +1,10 @@
1
+ /** Tool output at or above this size (chars) is treated as "large". */
2
+ export declare const LARGE_OUTPUT_THRESHOLD = 8192;
3
+ /**
4
+ * Rate-limit marker for large tool outputs, scoped to a single session's state.
5
+ * `recentSummaries` is mutated in place (it lives on the per-session state, so
6
+ * each session keeps its own cooldown). Returns true if the event was counted,
7
+ * false if the per-window cap is already reached.
8
+ */
9
+ export declare function trackLargeOutputEvent(recentSummaries: number[]): boolean;
10
+ //# sourceMappingURL=intel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intel.d.ts","sourceRoot":"","sources":["../../src/activity/intel.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,OAAO,CAcxE"}
@@ -0,0 +1,26 @@
1
+ const MAX_SUMMARIES_PER_WINDOW = 3;
2
+ const COOLDOWN_WINDOW_MS = 5 * 60 * 1000;
3
+ /** Tool output at or above this size (chars) is treated as "large". */
4
+ export const LARGE_OUTPUT_THRESHOLD = 8192;
5
+ /**
6
+ * Rate-limit marker for large tool outputs, scoped to a single session's state.
7
+ * `recentSummaries` is mutated in place (it lives on the per-session state, so
8
+ * each session keeps its own cooldown). Returns true if the event was counted,
9
+ * false if the per-window cap is already reached.
10
+ */
11
+ export function trackLargeOutputEvent(recentSummaries) {
12
+ const now = Date.now();
13
+ const cutoff = now - COOLDOWN_WINDOW_MS;
14
+ // Drop expired markers in place.
15
+ let w = 0;
16
+ for (let r = 0; r < recentSummaries.length; r++) {
17
+ if (recentSummaries[r] >= cutoff)
18
+ recentSummaries[w++] = recentSummaries[r];
19
+ }
20
+ recentSummaries.length = w;
21
+ if (recentSummaries.length >= MAX_SUMMARIES_PER_WINDOW)
22
+ return false;
23
+ recentSummaries.push(now);
24
+ return true;
25
+ }
26
+ //# sourceMappingURL=intel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intel.js","sourceRoot":"","sources":["../../src/activity/intel.ts"],"names":[],"mappings":"AAAA,MAAM,wBAAwB,GAAG,CAAC,CAAC;AACnC,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzC,uEAAuE;AACvE,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,eAAyB;IAC7D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,GAAG,GAAG,kBAAkB,CAAC;IAExC,iCAAiC;IACjC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChD,IAAI,eAAe,CAAC,CAAC,CAAC,IAAI,MAAM;YAAE,eAAe,CAAC,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC;IAE3B,IAAI,eAAe,CAAC,MAAM,IAAI,wBAAwB;QAAE,OAAO,KAAK,CAAC;IACrE,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { SessionStore } from "../storage/session-store.js";
2
+ export declare const isFileReadTool: (tool: string) => boolean;
3
+ export declare const isFileWriteTool: (tool: string) => boolean;
4
+ export declare const isAgentDispatchTool: (tool: string) => boolean;
5
+ /** Read a file-path argument under either naming convention (filePath | file_path). */
6
+ export declare function extractFilePath(args: unknown): string | null;
7
+ export type ToolBucket = "edit" | "read" | "agent" | "mcp" | "bash_infra" | "bash_git" | "bash_install" | "bash_other" | "web" | "other";
8
+ export type SessionMode = "code" | "debug" | "review" | "infra" | "general";
9
+ export declare function classifyTool(toolName: string, command?: string): ToolBucket;
10
+ export declare function detectMode(recentBuckets: ToolBucket[], hasRecentErrors?: boolean): SessionMode;
11
+ export declare function logToolUse(store: SessionStore, toolName: string, command?: string, hasError?: boolean, resultSize?: number): SessionMode | null;
12
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/activity/tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAmBhE,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,OAAoC,CAAC;AACnF,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,OAA+B,CAAC;AAC/E,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,OAAyC,CAAC;AAE7F,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK5D;AAYD,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,YAAY,GACZ,UAAU,GACV,cAAc,GACd,YAAY,GACZ,KAAK,GACL,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW,GAAG,UAAU,CAa/E;AAED,wBAAgB,UAAU,CAAC,aAAa,EAAE,UAAU,EAAE,EAAE,eAAe,GAAE,OAAe,GAAG,WAAW,CAmBrG;AAED,wBAAgB,UAAU,CACxB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,MAAW,EACpB,QAAQ,GAAE,OAAe,EACzB,UAAU,GAAE,MAAU,GACrB,WAAW,GAAG,IAAI,CAiCpB"}
@@ -0,0 +1,101 @@
1
+ const WINDOW_SIZE = 10;
2
+ const PRUNE_THRESHOLD = 30;
3
+ const PRUNE_KEEP = 20;
4
+ // Tool names span two ecosystems: Claude Code (PascalCase: Read/Write/Edit/Agent)
5
+ // and OpenCode native (lowercase: read/write/edit/task/grep/glob/bash). Both must
6
+ // be recognized or activity tracking silently no-ops on whichever host is in use.
7
+ const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "file_write", "file_edit", "edit", "write"]);
8
+ const READ_TOOLS = new Set(["Read", "Glob", "Grep", "file_read", "find", "read", "grep", "glob"]);
9
+ const AGENT_TOOLS = new Set(["Agent", "TaskCreate", "TaskUpdate", "TaskGet", "TaskList", "task"]);
10
+ // Focused sets for the hooks that record a single file path / dispatch. The
11
+ // writers are exactly EDIT_TOOLS; reads/dispatches are narrower than the broad
12
+ // classifier sets (no Glob/Grep, no Task* status tools).
13
+ const FILE_READ_TOOLS = new Set(["Read", "file_read", "read"]);
14
+ const AGENT_DISPATCH_TOOLS = new Set(["Agent", "TaskCreate", "task"]);
15
+ export const isFileReadTool = (tool) => FILE_READ_TOOLS.has(tool);
16
+ export const isFileWriteTool = (tool) => EDIT_TOOLS.has(tool);
17
+ export const isAgentDispatchTool = (tool) => AGENT_DISPATCH_TOOLS.has(tool);
18
+ /** Read a file-path argument under either naming convention (filePath | file_path). */
19
+ export function extractFilePath(args) {
20
+ if (!args || typeof args !== "object")
21
+ return null;
22
+ const a = args;
23
+ const p = a.filePath ?? a.file_path;
24
+ return typeof p === "string" ? p : null;
25
+ }
26
+ // Module-level prune cadence: run the COUNT+DELETE only every PRUNE_THRESHOLD
27
+ // calls instead of COUNT(*) on every single tool call (cheap, keeps the table
28
+ // bounded). Shared across sessions, which only affects which call triggers a prune.
29
+ let callsSincePrune = 0;
30
+ const INFRA_BASH_RE = /\b(?:systemctl|nginx|docker|kubectl|service|daemon|launchctl|brew|apt|apt-get|yum|dnf|pacman)\b/;
31
+ const GIT_WRITE_RE = /\bgit\s+(?:push|pull|merge|rebase|cherry-pick|tag)\b/;
32
+ const INSTALL_RE = /\b(?:pip|npm|pnpm|yarn|bun|cargo|go)\s+(?:install|add|update|upgrade)\b/;
33
+ export function classifyTool(toolName, command = "") {
34
+ if (EDIT_TOOLS.has(toolName))
35
+ return "edit";
36
+ if (READ_TOOLS.has(toolName))
37
+ return "read";
38
+ if (AGENT_TOOLS.has(toolName))
39
+ return "agent";
40
+ if (toolName.startsWith("mcp__") || toolName.startsWith("mcp_"))
41
+ return "mcp";
42
+ if (toolName === "Bash" || toolName === "shell" || toolName === "bash") {
43
+ if (INFRA_BASH_RE.test(command))
44
+ return "bash_infra";
45
+ if (GIT_WRITE_RE.test(command))
46
+ return "bash_git";
47
+ if (INSTALL_RE.test(command))
48
+ return "bash_install";
49
+ return "bash_other";
50
+ }
51
+ if (toolName === "WebSearch" || toolName === "WebFetch")
52
+ return "web";
53
+ return "other";
54
+ }
55
+ export function detectMode(recentBuckets, hasRecentErrors = false) {
56
+ if (recentBuckets.length < 3)
57
+ return "general";
58
+ const editCount = recentBuckets.filter((b) => b === "edit").length;
59
+ const readCount = recentBuckets.filter((b) => b === "read").length;
60
+ const infraCount = recentBuckets.filter((b) => b === "bash_infra" || b === "bash_git" || b === "bash_install").length;
61
+ const webCount = recentBuckets.filter((b) => b === "web").length;
62
+ const bashOther = recentBuckets.filter((b) => b === "bash_other").length;
63
+ if (infraCount >= 3)
64
+ return "infra";
65
+ if (hasRecentErrors && readCount >= 3 && editCount <= 1)
66
+ return "debug";
67
+ if (editCount >= 4)
68
+ return "code";
69
+ if (readCount >= 4 && editCount === 0)
70
+ return "review";
71
+ if (webCount >= 3)
72
+ return "review";
73
+ if (editCount >= 2 && (bashOther >= 2 || readCount >= 2))
74
+ return "code";
75
+ return "general";
76
+ }
77
+ export function logToolUse(store, toolName, command = "", hasError = false, resultSize = 0) {
78
+ try {
79
+ const bucket = classifyTool(toolName, command);
80
+ const db = store.connect();
81
+ db.run("INSERT INTO activity_log (tool_name, tool_bucket, has_error, result_size, timestamp) VALUES (?, ?, ?, ?, ?)", [toolName.slice(0, 64), bucket, hasError ? 1 : 0, resultSize, Date.now() / 1000]);
82
+ const rows = db
83
+ .query("SELECT tool_bucket, has_error FROM activity_log ORDER BY id DESC LIMIT ?")
84
+ .all(WINDOW_SIZE);
85
+ // Prune on a fixed cadence instead of COUNT(*) every call.
86
+ if (++callsSincePrune >= PRUNE_THRESHOLD) {
87
+ callsSincePrune = 0;
88
+ db.run("DELETE FROM activity_log WHERE id NOT IN (SELECT id FROM activity_log ORDER BY id DESC LIMIT ?)", [PRUNE_KEEP]);
89
+ }
90
+ const recentBuckets = rows.map((r) => r.tool_bucket);
91
+ const hasRecentErrors = rows.some((r) => r.has_error === 1);
92
+ const mode = detectMode(recentBuckets, hasRecentErrors);
93
+ store.setMeta("current_mode", mode);
94
+ return mode;
95
+ }
96
+ catch (e) {
97
+ console.warn("[Token Optimizer] logToolUse failed:", e);
98
+ return null;
99
+ }
100
+ }
101
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/activity/tracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { type SessionStore } from "../storage/session-store.js";
2
+ import type { SessionMode } from "../activity/tracker.js";
3
+ import type { TokenOptimizerConfig } from "../util/env.js";
4
+ export interface Checkpoint {
5
+ sessionId: string;
6
+ trigger: string;
7
+ mode: SessionMode;
8
+ qualityScore: number | null;
9
+ fillPct: number | null;
10
+ activeFiles: string[];
11
+ decisions: string[];
12
+ content: string;
13
+ createdAt: number;
14
+ }
15
+ export declare function captureCheckpoint(store: SessionStore, sessionId: string, trigger: string, mode: SessionMode, qualityScore: number | null, fillPct: number | null): Checkpoint;
16
+ export declare function pruneCheckpoints(store: SessionStore, config: TokenOptimizerConfig): number;
17
+ //# sourceMappingURL=checkpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint.d.ts","sourceRoot":"","sources":["../../src/compaction/checkpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAqB,MAAM,6BAA6B,CAAC;AACnF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,UAAU,CA6EZ;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAY1F"}
@@ -0,0 +1,82 @@
1
+ import { sanitizeSessionId } from "../storage/session-store.js";
2
+ export function captureCheckpoint(store, sessionId, trigger, mode, qualityScore, fillPct) {
3
+ const recentReads = store.getRecentReads(20);
4
+ const recentWrites = store.getRecentWrites(20);
5
+ const allPaths = new Set();
6
+ for (const r of recentReads)
7
+ allPaths.add(r.path);
8
+ for (const w of recentWrites)
9
+ allPaths.add(w.path);
10
+ const activeFiles = [...allPaths].slice(0, 15);
11
+ const decisions = [];
12
+ const cachedData = store.getQualityCache()?.data;
13
+ if (cachedData) {
14
+ try {
15
+ const parsed = JSON.parse(cachedData);
16
+ if (Array.isArray(parsed.decisions)) {
17
+ decisions.push(...parsed.decisions.slice(0, 10));
18
+ }
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ }
24
+ const safeSessionId = sanitizeSessionId(sessionId);
25
+ // This content is later injected into a system prompt, so neutralize anything
26
+ // that isn't plausibly part of a file path. Drop punctuation an attacker would
27
+ // use to smuggle instructions, collapse whitespace, and cap length.
28
+ const sanitizePath = (p) => p.replace(/[^a-zA-Z0-9 /._-]/g, "").replace(/\s+/g, " ").trim().slice(0, 512);
29
+ const lines = [
30
+ `# Checkpoint: ${trigger}`,
31
+ `Session: ${safeSessionId}`,
32
+ `Mode: ${mode}`,
33
+ `Quality: ${qualityScore !== null ? Math.round(qualityScore) : "N/A"}/100`,
34
+ `Fill: ${fillPct !== null ? Math.round(fillPct * 100) : "N/A"}%`,
35
+ "",
36
+ "## Active Files",
37
+ ...activeFiles.map((f) => `- ${sanitizePath(f)}`),
38
+ ];
39
+ if (decisions.length > 0) {
40
+ lines.push("", "## Decisions");
41
+ for (const d of decisions) {
42
+ lines.push(`- ${d.replace(/[\r\n]/g, " ").slice(0, 200)}`);
43
+ }
44
+ }
45
+ const content = lines.join("\n");
46
+ const db = store.connect();
47
+ db.run(`INSERT INTO checkpoints (session_id, trigger, mode, quality_score, fill_pct, active_files, decisions, content, created_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
49
+ sessionId,
50
+ trigger,
51
+ mode,
52
+ qualityScore,
53
+ fillPct,
54
+ JSON.stringify(activeFiles),
55
+ JSON.stringify(decisions),
56
+ content,
57
+ Date.now() / 1000,
58
+ ]);
59
+ return {
60
+ sessionId,
61
+ trigger,
62
+ mode,
63
+ qualityScore,
64
+ fillPct,
65
+ activeFiles,
66
+ decisions,
67
+ content,
68
+ createdAt: Date.now() / 1000,
69
+ };
70
+ }
71
+ export function pruneCheckpoints(store, config) {
72
+ if (config.checkpointRetentionDays <= 0)
73
+ return 0;
74
+ const db = store.connect();
75
+ const cutoff = Date.now() / 1000 - config.checkpointRetentionDays * 86400;
76
+ // Always keep the 3 newest checkpoints, even if they're past the cutoff. A
77
+ // short retention window or a clock skew must never wipe the checkpoint we
78
+ // just wrote and need for continuity restore.
79
+ const result = db.run("DELETE FROM checkpoints WHERE created_at < ? AND id NOT IN (SELECT id FROM checkpoints ORDER BY created_at DESC LIMIT 3)", [cutoff]);
80
+ return result.changes;
81
+ }
82
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint.js","sourceRoot":"","sources":["../../src/compaction/checkpoint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { SessionMode } from "../activity/tracker.js";
2
+ export declare function generateCompactionContext(mode: SessionMode, activeFiles: string[], qualityScore: number | null, fillPct: number | null): string[];
3
+ //# sourceMappingURL=dynamic-instructions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-instructions.d.ts","sourceRoot":"","sources":["../../src/compaction/dynamic-instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AA4C1D,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,MAAM,EAAE,EACrB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,EAAE,CAoBV"}
@@ -0,0 +1,54 @@
1
+ const MODE_INSTRUCTIONS = {
2
+ code: [
3
+ "PRESERVE: All file paths edited or created in this session.",
4
+ "PRESERVE: Recent Edit/Write tool calls with their file paths and the intent behind each change.",
5
+ "PRESERVE: Build/test outcomes and any error patterns being investigated.",
6
+ "DROP: Full file contents already persisted to disk (keep paths, drop bodies).",
7
+ "DROP: Intermediate Read results for files that were subsequently edited.",
8
+ ].join("\n"),
9
+ debug: [
10
+ "PRESERVE: Error messages, stack traces, and exception types.",
11
+ "PRESERVE: Hypotheses tested and their outcomes (confirmed/rejected).",
12
+ "PRESERVE: Root cause analysis progress and remaining candidates.",
13
+ "DROP: Verbose log output already analyzed.",
14
+ "DROP: Read results for files confirmed not involved.",
15
+ ].join("\n"),
16
+ review: [
17
+ "PRESERVE: Files reviewed and findings per file.",
18
+ "PRESERVE: Code patterns flagged and severity assessments.",
19
+ "PRESERVE: Coverage notes and areas not yet reviewed.",
20
+ "DROP: Full file contents (keep paths and line references).",
21
+ "DROP: Grep/Glob results that were scanned but yielded no findings.",
22
+ ].join("\n"),
23
+ infra: [
24
+ "PRESERVE: Infrastructure commands executed and their outcomes.",
25
+ "PRESERVE: Service states, deployment steps completed.",
26
+ "PRESERVE: Configuration changes made and their locations.",
27
+ "DROP: Verbose command output already summarized.",
28
+ "DROP: Repeated status checks with identical output.",
29
+ ].join("\n"),
30
+ general: [
31
+ "PRESERVE: Key decisions and the reasoning behind them.",
32
+ "PRESERVE: Action items and commitments made.",
33
+ "PRESERVE: File paths mentioned as relevant to ongoing work.",
34
+ "DROP: Exploratory reads that did not inform decisions.",
35
+ "DROP: Verbose tool output already summarized.",
36
+ ].join("\n"),
37
+ };
38
+ export function generateCompactionContext(mode, activeFiles, qualityScore, fillPct) {
39
+ const context = [];
40
+ context.push(`[Token Optimizer] Session mode: ${mode}`);
41
+ context.push(MODE_INSTRUCTIONS[mode]);
42
+ if (activeFiles.length > 0) {
43
+ const sanitized = activeFiles.slice(0, 15).map((f) => f.replace(/[\r\n]/g, " ").slice(0, 256));
44
+ context.push(`Active files (PRESERVE paths): ${sanitized.join(", ")}`);
45
+ }
46
+ if (qualityScore !== null) {
47
+ context.push(`Context quality before compaction: ${Math.round(qualityScore)}/100`);
48
+ }
49
+ if (fillPct !== null && fillPct > 0.7) {
50
+ context.push("HIGH FILL WARNING: Aggressively drop verbose output and intermediate results.");
51
+ }
52
+ return context;
53
+ }
54
+ //# sourceMappingURL=dynamic-instructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-instructions.js","sourceRoot":"","sources":["../../src/compaction/dynamic-instructions.ts"],"names":[],"mappings":"AAEA,MAAM,iBAAiB,GAAgC;IACrD,IAAI,EAAE;QACJ,6DAA6D;QAC7D,iGAAiG;QACjG,0EAA0E;QAC1E,+EAA+E;QAC/E,0EAA0E;KAC3E,CAAC,IAAI,CAAC,IAAI,CAAC;IAEZ,KAAK,EAAE;QACL,8DAA8D;QAC9D,sEAAsE;QACtE,kEAAkE;QAClE,4CAA4C;QAC5C,sDAAsD;KACvD,CAAC,IAAI,CAAC,IAAI,CAAC;IAEZ,MAAM,EAAE;QACN,iDAAiD;QACjD,2DAA2D;QAC3D,sDAAsD;QACtD,4DAA4D;QAC5D,oEAAoE;KACrE,CAAC,IAAI,CAAC,IAAI,CAAC;IAEZ,KAAK,EAAE;QACL,gEAAgE;QAChE,uDAAuD;QACvD,2DAA2D;QAC3D,kDAAkD;QAClD,qDAAqD;KACtD,CAAC,IAAI,CAAC,IAAI,CAAC;IAEZ,OAAO,EAAE;QACP,wDAAwD;QACxD,8CAA8C;QAC9C,6DAA6D;QAC7D,wDAAwD;QACxD,+CAA+C;KAChD,CAAC,IAAI,CAAC,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,UAAU,yBAAyB,CACvC,IAAiB,EACjB,WAAqB,EACrB,YAA2B,EAC3B,OAAsB;IAEtB,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,OAAO,CAAC,IAAI,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC/F,OAAO,CAAC,IAAI,CAAC,kCAAkC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,sCAAsC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACrF,CAAC;IAED,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,GAAG,GAAG,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IAChG,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export declare function scoreRelevance(userPrompt: string, checkpointContent: string): number;
2
+ export interface CheckpointMatch {
3
+ content: string;
4
+ score: number;
5
+ sessionId: string;
6
+ mode: string;
7
+ }
8
+ export declare function findBestCheckpoint(userPrompt: string, checkpoints: Array<{
9
+ session_id: string;
10
+ content: string;
11
+ mode: string;
12
+ created_at: number;
13
+ }>, threshold: number, maxChars?: number): CheckpointMatch | null;
14
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../../src/continuity/matcher.ts"],"names":[],"mappings":"AAoBA,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAYpF;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAUD,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7F,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,MAAa,GACtB,eAAe,GAAG,IAAI,CAkBxB"}
@@ -0,0 +1,57 @@
1
+ const STOP_WORDS = new Set([
2
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
3
+ "have", "has", "had", "do", "does", "did", "will", "would", "shall",
4
+ "should", "may", "might", "must", "can", "could", "to", "of", "in",
5
+ "for", "on", "with", "at", "by", "from", "as", "into", "about",
6
+ "like", "through", "after", "over", "between", "out", "up", "down",
7
+ "that", "this", "it", "its", "my", "your", "his", "her", "we", "they",
8
+ "them", "what", "which", "who", "when", "where", "how", "not", "no",
9
+ "but", "or", "and", "if", "then", "so", "than", "too", "very", "just",
10
+ "i", "me", "let", "us",
11
+ ]);
12
+ function extractKeywords(text) {
13
+ return text
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9\s_.-]/g, " ")
16
+ .split(/\s+/)
17
+ .filter((w) => w.length > 2 && !STOP_WORDS.has(w));
18
+ }
19
+ export function scoreRelevance(userPrompt, checkpointContent) {
20
+ const promptKeywords = extractKeywords(userPrompt);
21
+ if (promptKeywords.length === 0)
22
+ return 0;
23
+ const contentLower = checkpointContent.toLowerCase();
24
+ let matches = 0;
25
+ for (const kw of promptKeywords) {
26
+ if (contentLower.includes(kw))
27
+ matches++;
28
+ }
29
+ return matches / promptKeywords.length;
30
+ }
31
+ function safeSlice(str, maxChars) {
32
+ if (str.length <= maxChars)
33
+ return str;
34
+ let end = maxChars;
35
+ const code = str.charCodeAt(end - 1);
36
+ if (code >= 0xd800 && code <= 0xdbff)
37
+ end--;
38
+ return str.slice(0, end) + "\n[... truncated]";
39
+ }
40
+ export function findBestCheckpoint(userPrompt, checkpoints, threshold, maxChars = 2000) {
41
+ let best = null;
42
+ let bestScore = 0;
43
+ for (const cp of checkpoints) {
44
+ const score = scoreRelevance(userPrompt, cp.content);
45
+ if (score > bestScore && score >= threshold) {
46
+ bestScore = score;
47
+ best = {
48
+ content: safeSlice(cp.content, maxChars),
49
+ score,
50
+ sessionId: cp.session_id,
51
+ mode: cp.mode,
52
+ };
53
+ }
54
+ }
55
+ return best;
56
+ }
57
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../src/continuity/matcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type CheckpointMatch } from "./matcher.js";
2
+ import type { TokenOptimizerConfig } from "../util/env.js";
3
+ export declare function restoreCheckpoint(dataDir: string, userPrompt: string, currentSessionId: string, config: TokenOptimizerConfig): CheckpointMatch | null;
4
+ //# sourceMappingURL=restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restore.d.ts","sourceRoot":"","sources":["../../src/continuity/restore.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAExE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,oBAAoB,GAC3B,eAAe,GAAG,IAAI,CA4DxB"}
@@ -0,0 +1,53 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+ import { findBestCheckpoint } from "./matcher.js";
5
+ import { sanitizeSessionId } from "../storage/session-store.js";
6
+ export function restoreCheckpoint(dataDir, userPrompt, currentSessionId, config) {
7
+ if (!config.features.continuity)
8
+ return null;
9
+ const sessDir = join(dataDir, "token-optimizer", "sessions");
10
+ if (!existsSync(sessDir))
11
+ return null;
12
+ // DB filenames are the sanitized session id, so compare like-for-like or a
13
+ // session whose id contains special chars would fail to exclude its own file.
14
+ const safeCurrentId = sanitizeSessionId(currentSessionId);
15
+ const cutoff = config.checkpointRetentionDays <= 0
16
+ ? 0
17
+ : Date.now() / 1000 - config.checkpointRetentionDays * 86400;
18
+ const allCheckpoints = [];
19
+ try {
20
+ const allFiles = readdirSync(sessDir);
21
+ const dbFiles = allFiles.filter((f) => f.endsWith(".db")).slice(0, config.checkpointRetentionMax);
22
+ for (const file of dbFiles) {
23
+ const sessionId = file.replace(".db", "");
24
+ if (sessionId === safeCurrentId)
25
+ continue;
26
+ const dbPath = join(sessDir, file);
27
+ let db = null;
28
+ try {
29
+ db = new Database(dbPath, { readonly: true });
30
+ db.exec("PRAGMA busy_timeout=500");
31
+ const rows = db
32
+ .query("SELECT session_id, content, mode, created_at FROM checkpoints WHERE created_at > ? ORDER BY created_at DESC LIMIT ?")
33
+ .all(cutoff, config.checkpointRetentionMax);
34
+ allCheckpoints.push(...rows);
35
+ }
36
+ catch {
37
+ // skip corrupt/locked DBs
38
+ }
39
+ finally {
40
+ db?.close();
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ if (allCheckpoints.length === 0)
48
+ return null;
49
+ allCheckpoints.sort((a, b) => b.created_at - a.created_at);
50
+ const candidates = allCheckpoints.slice(0, config.checkpointRetentionMax);
51
+ return findBestCheckpoint(userPrompt, candidates, config.relevanceThreshold, config.checkpointMaxChars);
52
+ }
53
+ //# sourceMappingURL=restore.js.map