@bahulam/code 2.6.0
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 +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback Client — POST tool results back to Tarang backend.
|
|
3
|
+
* Implements retry with backoff: 2 retries, 500ms delay.
|
|
4
|
+
* 4xx: no retry. 5xx/network: retry.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MAX_RETRIES = 2;
|
|
8
|
+
const RETRY_DELAY_MS = 500;
|
|
9
|
+
const TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function cleanCallbackResult(result = {}) {
|
|
16
|
+
// Clean result for the backend — strip internal CLI metadata so the LLM
|
|
17
|
+
// sees a clear, unambiguous tool result (not noisy JSON with _tool, _output_meta, etc.)
|
|
18
|
+
const cleanResult = {};
|
|
19
|
+
for (const [key, value] of Object.entries(result || {})) {
|
|
20
|
+
if (!key.startsWith('_')) {
|
|
21
|
+
cleanResult[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return cleanResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function backendToolResultForLlm(toolName, cleanResult = {}) {
|
|
28
|
+
const rawResult = cleanCallbackResult(cleanResult);
|
|
29
|
+
if (toolName === 'get_project_overview' && !Object.hasOwn(rawResult, 'error')) {
|
|
30
|
+
return {
|
|
31
|
+
output: rawResult.output || '',
|
|
32
|
+
project_resource: rawResult.project_resource,
|
|
33
|
+
already_registered: rawResult.already_registered || false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!Object.hasOwn(rawResult, 'error')) {
|
|
37
|
+
const outputText = rawResult.output || '';
|
|
38
|
+
if (rawResult.success && outputText) {
|
|
39
|
+
return { output: outputText };
|
|
40
|
+
}
|
|
41
|
+
return rawResult;
|
|
42
|
+
}
|
|
43
|
+
return rawResult;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function llmToolResultContent(toolName, result = {}) {
|
|
47
|
+
return JSON.stringify(backendToolResultForLlm(toolName, result));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send a tool execution result to the backend.
|
|
52
|
+
* @param {string} baseUrl
|
|
53
|
+
* @param {string} token
|
|
54
|
+
* @param {string} taskId
|
|
55
|
+
* @param {string} callId
|
|
56
|
+
* @param {Object} result - { success, output, ... }
|
|
57
|
+
* @returns {Promise<boolean>}
|
|
58
|
+
*/
|
|
59
|
+
export async function sendCallback(baseUrl, token, taskId, callId, result) {
|
|
60
|
+
const url = `${baseUrl}/api/callback`;
|
|
61
|
+
const cleanResult = cleanCallbackResult(result);
|
|
62
|
+
|
|
63
|
+
const body = JSON.stringify({
|
|
64
|
+
task_id: taskId,
|
|
65
|
+
call_id: callId,
|
|
66
|
+
result: cleanResult,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const headers = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Idempotency-Key': `${taskId}:${callId}`,
|
|
72
|
+
};
|
|
73
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
74
|
+
|
|
75
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
76
|
+
try {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
79
|
+
|
|
80
|
+
const resp = await fetch(url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers,
|
|
83
|
+
body,
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
|
|
89
|
+
if (resp.ok) return true;
|
|
90
|
+
|
|
91
|
+
// 4xx: client error, don't retry
|
|
92
|
+
if (resp.status >= 400 && resp.status < 500) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 5xx: server error, retry
|
|
97
|
+
if (attempt < MAX_RETRIES) {
|
|
98
|
+
await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// Network error or timeout, retry
|
|
102
|
+
if (attempt < MAX_RETRIES) {
|
|
103
|
+
await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Send a "skipped" callback for rejected operations.
|
|
113
|
+
* @param {string} baseUrl
|
|
114
|
+
* @param {string} token
|
|
115
|
+
* @param {string} taskId
|
|
116
|
+
* @param {string} callId
|
|
117
|
+
* @param {string} message
|
|
118
|
+
* @returns {Promise<boolean>}
|
|
119
|
+
*/
|
|
120
|
+
export async function sendSkippedCallback(baseUrl, token, taskId, callId, message) {
|
|
121
|
+
return sendCallback(baseUrl, token, taskId, callId, {
|
|
122
|
+
skipped: true,
|
|
123
|
+
message: message || 'User rejected operation',
|
|
124
|
+
success: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Send an approval decision to the backend's HITL handler.
|
|
130
|
+
* Called when the framework emits approval_required and the CLI
|
|
131
|
+
* has collected the user's decision via the approval menu.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} baseUrl
|
|
134
|
+
* @param {string} token
|
|
135
|
+
* @param {string} taskId
|
|
136
|
+
* @param {string} toolId - tool_id from the approval_required event
|
|
137
|
+
* @param {string} decision - "grant" or "deny"
|
|
138
|
+
* @param {string} [scope] - "once", "type", or "all" (for grant)
|
|
139
|
+
* @param {string} [reason] - optional reason
|
|
140
|
+
* @returns {Promise<boolean>}
|
|
141
|
+
*/
|
|
142
|
+
export async function sendApprovalDecision(baseUrl, token, taskId, toolId, decision, scope = 'once', reason = '') {
|
|
143
|
+
const url = `${baseUrl}/api/approval_callback`;
|
|
144
|
+
|
|
145
|
+
const body = JSON.stringify({
|
|
146
|
+
task_id: taskId,
|
|
147
|
+
tool_id: toolId,
|
|
148
|
+
decision,
|
|
149
|
+
scope,
|
|
150
|
+
reason,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const headers = {
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
'Idempotency-Key': `${taskId}:${toolId}:${decision}`,
|
|
156
|
+
};
|
|
157
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
158
|
+
|
|
159
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
163
|
+
|
|
164
|
+
const resp = await fetch(url, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers,
|
|
167
|
+
body,
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
clearTimeout(timeoutId);
|
|
172
|
+
if (resp.ok) return true;
|
|
173
|
+
if (resp.status >= 400 && resp.status < 500) return false;
|
|
174
|
+
if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
175
|
+
} catch {
|
|
176
|
+
if (attempt < MAX_RETRIES) await sleep(RETRY_DELAY_MS * (attempt + 1));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Checkpointing — save and restore file state before edits.
|
|
3
|
+
*
|
|
4
|
+
* Before any file edit, a checkpoint is created containing the
|
|
5
|
+
* original file content. The /undo command restores the last checkpoint.
|
|
6
|
+
* Checkpoints are stored in ~/.bahulam/projects/{hash}/checkpoints/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
import { checkpointsDir } from './paths.mjs';
|
|
13
|
+
|
|
14
|
+
export class CheckpointManager {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} [baseDir] - project root directory
|
|
17
|
+
*/
|
|
18
|
+
constructor(baseDir = process.cwd()) {
|
|
19
|
+
this.baseDir = baseDir;
|
|
20
|
+
this.checkpointDir = checkpointsDir(baseDir);
|
|
21
|
+
this.history = []; // Stack of checkpoint IDs
|
|
22
|
+
this.maxCheckpoints = 50;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a checkpoint for a file before editing.
|
|
27
|
+
* @param {string} filePath - absolute path to the file
|
|
28
|
+
* @returns {string|null} checkpoint ID, or null if file doesn't exist
|
|
29
|
+
*/
|
|
30
|
+
save(filePath) {
|
|
31
|
+
const absPath = path.resolve(filePath);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
35
|
+
const id = `ckpt_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
36
|
+
|
|
37
|
+
fs.mkdirSync(this.checkpointDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const checkpoint = {
|
|
40
|
+
id,
|
|
41
|
+
filePath: absPath,
|
|
42
|
+
relativePath: path.relative(this.baseDir, absPath),
|
|
43
|
+
content,
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
size: content.length,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ckptFile = path.join(this.checkpointDir, `${id}.json`);
|
|
49
|
+
fs.writeFileSync(ckptFile, JSON.stringify(checkpoint));
|
|
50
|
+
|
|
51
|
+
this.history.push(id);
|
|
52
|
+
|
|
53
|
+
// Trim old checkpoints
|
|
54
|
+
while (this.history.length > this.maxCheckpoints) {
|
|
55
|
+
const old = this.history.shift();
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(path.join(this.checkpointDir, `${old}.json`));
|
|
58
|
+
} catch {
|
|
59
|
+
// Already deleted
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return id;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Restore the most recent checkpoint (undo last edit).
|
|
71
|
+
* @returns {{ filePath: string, restored: boolean, id: string }|null}
|
|
72
|
+
*/
|
|
73
|
+
undo() {
|
|
74
|
+
if (this.history.length === 0) return null;
|
|
75
|
+
|
|
76
|
+
const id = this.history.pop();
|
|
77
|
+
const ckptFile = path.join(this.checkpointDir, `${id}.json`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const raw = fs.readFileSync(ckptFile, 'utf-8');
|
|
81
|
+
const checkpoint = JSON.parse(raw);
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(checkpoint.filePath, checkpoint.content);
|
|
84
|
+
fs.unlinkSync(ckptFile);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
id: checkpoint.id,
|
|
88
|
+
filePath: checkpoint.filePath,
|
|
89
|
+
restored: true,
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { id, filePath: null, restored: false, error: err.message };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List recent checkpoints.
|
|
98
|
+
* @param {number} [limit=10]
|
|
99
|
+
* @returns {Array}
|
|
100
|
+
*/
|
|
101
|
+
list(limit = 10) {
|
|
102
|
+
const result = [];
|
|
103
|
+
const ids = this.history.slice(-limit).reverse();
|
|
104
|
+
|
|
105
|
+
for (const id of ids) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(
|
|
108
|
+
path.join(this.checkpointDir, `${id}.json`),
|
|
109
|
+
'utf-8'
|
|
110
|
+
);
|
|
111
|
+
const ckpt = JSON.parse(raw);
|
|
112
|
+
result.push({
|
|
113
|
+
id: ckpt.id,
|
|
114
|
+
file: ckpt.relativePath,
|
|
115
|
+
timestamp: ckpt.timestamp,
|
|
116
|
+
size: ckpt.size,
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
result.push({ id, file: '?', timestamp: '?', size: 0 });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clear all checkpoints.
|
|
128
|
+
*/
|
|
129
|
+
clear() {
|
|
130
|
+
this.history.length = 0;
|
|
131
|
+
try {
|
|
132
|
+
const entries = fs.readdirSync(this.checkpointDir);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.startsWith('ckpt_') && entry.endsWith('.json')) {
|
|
135
|
+
fs.unlinkSync(path.join(this.checkpointDir, entry));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Directory doesn't exist
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const SUMMARY_PREFIX = 'Session continuity summary after /compact:';
|
|
2
|
+
|
|
3
|
+
export function parseCompactTailCount(rest = '', fallback = 8) {
|
|
4
|
+
const text = String(rest || '').trim();
|
|
5
|
+
const match = text.match(/(?:^|\s)(?:--tail=|--tail\s+)?(\d+)(?:\s|$)/);
|
|
6
|
+
const n = match ? Number(match[1]) : Number(fallback);
|
|
7
|
+
if (!Number.isFinite(n)) return fallback;
|
|
8
|
+
return Math.max(2, Math.min(50, Math.floor(n)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isCompactHistory(history = []) {
|
|
12
|
+
return typeof history?.[2]?.content === 'string'
|
|
13
|
+
&& history[2].content.startsWith(SUMMARY_PREFIX);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extractCompactSummary(history = []) {
|
|
17
|
+
const content = String(history?.[2]?.content || '');
|
|
18
|
+
if (!content.startsWith(SUMMARY_PREFIX)) return '';
|
|
19
|
+
return content.slice(SUMMARY_PREFIX.length).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function prepareCompactHistory({
|
|
23
|
+
agentHistory = [],
|
|
24
|
+
tailCount = 8,
|
|
25
|
+
minSourceMessages = 2,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const history = Array.isArray(agentHistory)
|
|
28
|
+
? agentHistory.filter(msg => msg && typeof msg.content === 'string' && msg.content.trim())
|
|
29
|
+
: [];
|
|
30
|
+
const beforeCount = history.length;
|
|
31
|
+
const retainedCount = Math.min(Math.max(2, Number(tailCount) || 8), Math.max(0, beforeCount));
|
|
32
|
+
const prefixCount = isCompactHistory(history) ? 3 : 0;
|
|
33
|
+
const sourceEnd = Math.max(prefixCount, beforeCount - retainedCount);
|
|
34
|
+
const sourceMessages = history.slice(prefixCount, sourceEnd);
|
|
35
|
+
const tail = history.slice(sourceEnd);
|
|
36
|
+
|
|
37
|
+
if (sourceMessages.length < minSourceMessages) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: beforeCount <= prefixCount + retainedCount
|
|
41
|
+
? 'not enough history beyond the retained tail'
|
|
42
|
+
: 'not enough compactable messages',
|
|
43
|
+
beforeCount,
|
|
44
|
+
prefixCount,
|
|
45
|
+
sourceMessages,
|
|
46
|
+
tail,
|
|
47
|
+
retainedCount,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
beforeCount,
|
|
54
|
+
prefixCount,
|
|
55
|
+
sourceMessages,
|
|
56
|
+
tail,
|
|
57
|
+
retainedCount: tail.length,
|
|
58
|
+
previousSummary: extractCompactSummary(history),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyCompactSummary({
|
|
63
|
+
prepared,
|
|
64
|
+
summary,
|
|
65
|
+
sessionId = '',
|
|
66
|
+
cwd = '',
|
|
67
|
+
originalRequest = '',
|
|
68
|
+
previousSourceMessageCount = 0,
|
|
69
|
+
now = new Date(),
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!prepared?.ok) throw new Error(prepared?.reason || 'history is not compactable');
|
|
72
|
+
const compactSummary = String(summary || '').trim();
|
|
73
|
+
if (!compactSummary) throw new Error('summary is required');
|
|
74
|
+
const priorCount = Math.max(0, Number(previousSourceMessageCount) || 0);
|
|
75
|
+
const sourceMessageCount = priorCount + prepared.sourceMessages.length;
|
|
76
|
+
const timestamp = now instanceof Date ? now.toISOString() : String(now || new Date().toISOString());
|
|
77
|
+
const firstUser = originalRequest || firstUserMessage(prepared.sourceMessages) || firstUserMessage(prepared.tail) || '(unknown)';
|
|
78
|
+
|
|
79
|
+
const metadata = [
|
|
80
|
+
'Compact metadata.',
|
|
81
|
+
sessionId ? `Session: ${sessionId}` : '',
|
|
82
|
+
cwd ? `Project: ${cwd}` : '',
|
|
83
|
+
`Compacted at: ${timestamp}`,
|
|
84
|
+
`Covered live messages: ${sourceMessageCount}`,
|
|
85
|
+
`Retained tail messages: ${prepared.tail.length}`,
|
|
86
|
+
].filter(Boolean).join('\n');
|
|
87
|
+
|
|
88
|
+
const agentHistory = [
|
|
89
|
+
{ role: 'user', content: metadata },
|
|
90
|
+
{ role: 'user', content: `Original user request from this compacted session:\n${firstUser}` },
|
|
91
|
+
{ role: 'user', content: `${SUMMARY_PREFIX}\n${compactSummary}` },
|
|
92
|
+
...prepared.tail,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
agentHistory,
|
|
97
|
+
summary: compactSummary,
|
|
98
|
+
sourceMessageCount,
|
|
99
|
+
previousSourceMessageCount: priorCount,
|
|
100
|
+
beforeCount: prepared.beforeCount,
|
|
101
|
+
afterCount: agentHistory.length,
|
|
102
|
+
retainedCount: prepared.tail.length,
|
|
103
|
+
compactedCount: prepared.sourceMessages.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function localCompactSummary(messages = []) {
|
|
108
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
109
|
+
const userMessages = list.filter(m => m.role === 'user').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
110
|
+
const assistantMessages = list.filter(m => m.role === 'assistant').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
111
|
+
return [
|
|
112
|
+
'Local compact summary from live conversation context.',
|
|
113
|
+
userMessages.length ? `User requests (${userMessages.length}):` : '',
|
|
114
|
+
...userMessages.slice(-8).map(text => `- ${truncate(text, 500)}`),
|
|
115
|
+
assistantMessages.length ? `Assistant progress (${assistantMessages.length}):` : '',
|
|
116
|
+
...assistantMessages.slice(-8).map(text => `- ${truncate(text, 700)}`),
|
|
117
|
+
].filter(Boolean).join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstUserMessage(messages = []) {
|
|
121
|
+
return messages.find(m => m?.role === 'user' && typeof m.content === 'string')?.content || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function truncate(text, max) {
|
|
125
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
126
|
+
return value.length > max ? value.slice(0, max - 3) + '...' : value;
|
|
127
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { contextToPromptBlock } from './project-context-loader.mjs';
|
|
2
|
+
|
|
3
|
+
export function buildContextEnvelope({
|
|
4
|
+
cwd = process.cwd(),
|
|
5
|
+
command = null,
|
|
6
|
+
args = [],
|
|
7
|
+
source = 'repl',
|
|
8
|
+
dryRun = false,
|
|
9
|
+
aliasesResolved = [],
|
|
10
|
+
effectivePolicy,
|
|
11
|
+
projectContext,
|
|
12
|
+
activeHints = [],
|
|
13
|
+
projectResources = [],
|
|
14
|
+
agentContext = {},
|
|
15
|
+
} = {}) {
|
|
16
|
+
const policy = effectivePolicy?.policy || {};
|
|
17
|
+
const commandTimeouts = policy.commands?.timeouts || {};
|
|
18
|
+
const enabled = policy.commands?.enabled || [];
|
|
19
|
+
const activeCommand = command || null;
|
|
20
|
+
const specificTimeout = activeCommand ? commandTimeouts[`${activeCommand}Seconds`] : null;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
cwd,
|
|
24
|
+
project_resources: projectResources,
|
|
25
|
+
agent_context: agentContext,
|
|
26
|
+
project_context: {
|
|
27
|
+
loaded_files: projectContext?.loaded || [],
|
|
28
|
+
changed_files: projectContext?.changed?.map(f => ({ label: f.label, path: f.path, hash: f.hash })) || [],
|
|
29
|
+
prompt_block: contextToPromptBlock(projectContext),
|
|
30
|
+
},
|
|
31
|
+
command_context: {
|
|
32
|
+
active_command: activeCommand,
|
|
33
|
+
source,
|
|
34
|
+
args,
|
|
35
|
+
dry_run: dryRun,
|
|
36
|
+
aliases_resolved: aliasesResolved,
|
|
37
|
+
runtime_limits: {
|
|
38
|
+
command_timeout_seconds: specificTimeout || commandTimeouts.defaultSeconds || 300,
|
|
39
|
+
tool_timeout_seconds: 120,
|
|
40
|
+
approval_timeout_seconds: null,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
effective_options: {
|
|
44
|
+
commands_enabled: enabled,
|
|
45
|
+
plan_owner: policy.planning?.owner || 'auto',
|
|
46
|
+
hitl_default_scope: policy.hitl?.defaultScope || 'once',
|
|
47
|
+
reask_after_minutes: policy.hitl?.reaskAfterMinutes ?? 30,
|
|
48
|
+
hook_timeout_seconds: policy.hooks?.timeoutSeconds ?? 5,
|
|
49
|
+
},
|
|
50
|
+
context_hints: activeHints,
|
|
51
|
+
available_skills: projectContext?.available_skills || [],
|
|
52
|
+
task_state: projectContext?.task_state || [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manager — tracks token usage and compacts conversation history.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Proper token estimation (4 chars ~ 1 token for English)
|
|
6
|
+
* - Micro-compaction (remove stale tool results older than 5 turns)
|
|
7
|
+
* - Keep system prompt and recent 3 turns intact during compaction
|
|
8
|
+
* - Track pre/post compaction token counts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_TOKENS = 180000; // ~200k model limit with buffer
|
|
12
|
+
const COMPACT_THRESHOLD = 0.80;
|
|
13
|
+
const CHARS_PER_TOKEN = 4; // rough estimate for English text
|
|
14
|
+
const STALE_TOOL_RESULT_TURNS = 5; // tool results older than this are micro-compacted
|
|
15
|
+
|
|
16
|
+
export class ContextManager {
|
|
17
|
+
/**
|
|
18
|
+
* @param {number} maxTokens - Maximum tokens for context window
|
|
19
|
+
*/
|
|
20
|
+
constructor(maxTokens = DEFAULT_MAX_TOKENS) {
|
|
21
|
+
this.maxTokens = maxTokens;
|
|
22
|
+
this.threshold = COMPACT_THRESHOLD;
|
|
23
|
+
this.compactionCount = 0;
|
|
24
|
+
this.lastPreCompactTokens = 0;
|
|
25
|
+
this.lastPostCompactTokens = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Estimate token count for a message array.
|
|
30
|
+
* Uses character-based heuristic (no external tokenizer dependency).
|
|
31
|
+
* @param {Array} messages - conversation messages
|
|
32
|
+
* @returns {number} estimated token count
|
|
33
|
+
*/
|
|
34
|
+
getTokenCount(messages) {
|
|
35
|
+
let chars = 0;
|
|
36
|
+
for (const msg of messages) {
|
|
37
|
+
// Role overhead (~4 tokens)
|
|
38
|
+
chars += 16;
|
|
39
|
+
|
|
40
|
+
if (typeof msg.content === 'string') {
|
|
41
|
+
chars += msg.content.length;
|
|
42
|
+
} else if (Array.isArray(msg.content)) {
|
|
43
|
+
for (const block of msg.content) {
|
|
44
|
+
if (block.type === 'text') chars += (block.text || '').length;
|
|
45
|
+
else if (block.type === 'tool_result') chars += (block.content || '').length;
|
|
46
|
+
else if (block.type === 'tool_use') chars += JSON.stringify(block.input || {}).length + 20;
|
|
47
|
+
else if (block.type === 'thinking') chars += (block.thinking || '').length;
|
|
48
|
+
else chars += JSON.stringify(block).length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if compaction is needed.
|
|
57
|
+
* @param {Array} messages - current conversation messages
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
shouldCompact(messages) {
|
|
61
|
+
const tokenCount = this.getTokenCount(messages);
|
|
62
|
+
return tokenCount >= this.maxTokens * this.threshold;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Micro-compact: remove verbose tool results from messages older than N turns.
|
|
67
|
+
* Keeps the tool call reference but truncates result content.
|
|
68
|
+
* @param {Array} messages
|
|
69
|
+
* @param {number} recentTurns - number of recent user/assistant pairs to preserve
|
|
70
|
+
* @returns {Array}
|
|
71
|
+
*/
|
|
72
|
+
microCompact(messages, recentTurns = STALE_TOOL_RESULT_TURNS) {
|
|
73
|
+
// Count turns (each user message is roughly one turn)
|
|
74
|
+
let turnCount = 0;
|
|
75
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
76
|
+
if (messages[i].role === 'user') turnCount++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (turnCount <= recentTurns) return messages;
|
|
80
|
+
|
|
81
|
+
// Mark the boundary: keep last recentTurns user messages intact
|
|
82
|
+
let usersSeen = 0;
|
|
83
|
+
let boundary = messages.length;
|
|
84
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
85
|
+
if (messages[i].role === 'user') {
|
|
86
|
+
usersSeen++;
|
|
87
|
+
if (usersSeen >= recentTurns) {
|
|
88
|
+
boundary = i;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Truncate tool results before the boundary
|
|
95
|
+
const result = messages.map((msg, idx) => {
|
|
96
|
+
if (idx >= boundary) return msg;
|
|
97
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
98
|
+
|
|
99
|
+
const newContent = msg.content.map(block => {
|
|
100
|
+
if (block.type === 'tool_result' && typeof block.content === 'string' && block.content.length > 200) {
|
|
101
|
+
return {
|
|
102
|
+
...block,
|
|
103
|
+
content: block.content.slice(0, 100) + '...[truncated]',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return block;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { ...msg, content: newContent };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compact messages by summarizing older history.
|
|
117
|
+
* Keeps the most recent N messages intact and replaces older ones
|
|
118
|
+
* with a summary message.
|
|
119
|
+
*
|
|
120
|
+
* @param {Array} messages - current conversation messages
|
|
121
|
+
* @param {number} keepRecent - number of recent messages to preserve (default 6 = ~3 turns)
|
|
122
|
+
* @returns {Array} compacted message array
|
|
123
|
+
*/
|
|
124
|
+
compact(messages, keepRecent = 6) {
|
|
125
|
+
if (messages.length <= keepRecent) return messages;
|
|
126
|
+
|
|
127
|
+
this.lastPreCompactTokens = this.getTokenCount(messages);
|
|
128
|
+
this.compactionCount++;
|
|
129
|
+
|
|
130
|
+
// First try micro-compaction
|
|
131
|
+
let working = this.microCompact(messages);
|
|
132
|
+
if (!this.shouldCompact(working)) {
|
|
133
|
+
this.lastPostCompactTokens = this.getTokenCount(working);
|
|
134
|
+
return working;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Full compaction
|
|
138
|
+
const oldMessages = messages.slice(0, -keepRecent);
|
|
139
|
+
const recentMessages = messages.slice(-keepRecent);
|
|
140
|
+
|
|
141
|
+
// Build a summary of old messages
|
|
142
|
+
const summaryParts = [];
|
|
143
|
+
for (const msg of oldMessages) {
|
|
144
|
+
const role = msg.role;
|
|
145
|
+
let text = '';
|
|
146
|
+
if (typeof msg.content === 'string') {
|
|
147
|
+
text = msg.content.slice(0, 200);
|
|
148
|
+
} else if (Array.isArray(msg.content)) {
|
|
149
|
+
text = msg.content
|
|
150
|
+
.map(b => {
|
|
151
|
+
if (b.type === 'text') return b.text?.slice(0, 100);
|
|
152
|
+
if (b.type === 'tool_use') return `[tool:${b.name}]`;
|
|
153
|
+
if (b.type === 'tool_result') return `[result:${String(b.content).slice(0, 80)}]`;
|
|
154
|
+
return `[${b.type}]`;
|
|
155
|
+
})
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(' ');
|
|
158
|
+
}
|
|
159
|
+
if (text) summaryParts.push(`${role}: ${text}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const summary = {
|
|
163
|
+
role: 'user',
|
|
164
|
+
content: `[Context compacted — summary of ${oldMessages.length} earlier messages]\n` +
|
|
165
|
+
summaryParts.join('\n').slice(0, 2000),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const compacted = [summary, ...recentMessages];
|
|
169
|
+
this.lastPostCompactTokens = this.getTokenCount(compacted);
|
|
170
|
+
return compacted;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Add a message and auto-compact if needed.
|
|
175
|
+
* @param {Array} messages - mutable message array
|
|
176
|
+
* @param {object} msg - new message to add
|
|
177
|
+
* @returns {Array} possibly compacted array with new message
|
|
178
|
+
*/
|
|
179
|
+
addMessage(messages, msg) {
|
|
180
|
+
messages.push(msg);
|
|
181
|
+
if (this.shouldCompact(messages)) {
|
|
182
|
+
return this.compact(messages);
|
|
183
|
+
}
|
|
184
|
+
return messages;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get compaction statistics.
|
|
189
|
+
* @returns {object}
|
|
190
|
+
*/
|
|
191
|
+
getStats() {
|
|
192
|
+
return {
|
|
193
|
+
compactionCount: this.compactionCount,
|
|
194
|
+
lastPreCompactTokens: this.lastPreCompactTokens,
|
|
195
|
+
lastPostCompactTokens: this.lastPostCompactTokens,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|