@miphamai/cli 0.37.0 → 0.39.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/bin/mipham.ts +25 -0
- package/package.json +1 -1
- package/src/agent/cross-session/discovery.ts +45 -1
- package/src/agent/cross-session/file-inbox.ts +15 -0
- package/src/agent/message-router.ts +22 -2
- package/src/agent/sub-agent.ts +4 -3
- package/src/config/defaults.ts +19 -1
- package/src/config/keys-manager.ts +11 -1
- package/src/config/loader.ts +20 -10
- package/src/core/context.ts +86 -56
- package/src/core/credential-masker/output-scrub.ts +5 -0
- package/src/core/crsi-sandbox.ts +19 -7
- package/src/core/engine.ts +69 -35
- package/src/core/instructions.ts +4 -42
- package/src/core/permission.ts +7 -1
- package/src/core/red-team.ts +20 -5
- package/src/core/self-critique.ts +3 -1
- package/src/core/session-log.ts +225 -0
- package/src/core/session-store.ts +119 -34
- package/src/core/workspace-trust.ts +25 -2
- package/src/daemon/auth.ts +9 -6
- package/src/daemon/cors.ts +24 -16
- package/src/daemon/rate-limiter.ts +4 -0
- package/src/daemon/server.ts +24 -3
- package/src/i18n-core/locales/en-US.json +4 -0
- package/src/i18n-core/locales/zh-CN.json +4 -0
- package/src/index.tsx +62 -30
- package/src/mcp/client.ts +56 -17
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/oauth.ts +1 -1
- package/src/providers/llm-replay.ts +36 -0
- package/src/providers/llm.ts +16 -0
- package/src/providers/registry.ts +2 -1
- package/src/security/gate.ts +10 -0
- package/src/security/url.ts +37 -14
- package/src/shared/constants.ts +4 -486
- package/src/shared/types.ts +6 -437
- package/src/skills/fork-executor.ts +3 -1
- package/src/skills/loader.ts +2 -1
- package/src/skills/seam.ts +18 -0
- package/src/tools/agent/agent.ts +1 -0
- package/src/tools/agent/skill.ts +1 -0
- package/src/tools/exec/bash.ts +141 -129
- package/src/tools/exec/git.ts +17 -0
- package/src/tools/file/read.ts +73 -66
- package/src/tools/file/write.ts +1 -1
- package/src/tools/index.ts +54 -132
- package/src/tools/network/web-fetch.ts +41 -26
- package/src/tools/seam.ts +25 -0
- package/src/tools/validation.ts +91 -0
- package/src/ui/app.tsx +36 -0
- package/src/ui/commands.ts +27 -6
- package/src/ui/input.tsx +21 -0
- package/src/vajra/compose/assemble.ts +19 -0
- package/src/vajra/compose/bundle.ts +39 -0
- package/src/vajra/compose/dump.ts +5 -0
- package/src/vajra/compose/index.ts +4 -0
- package/src/vajra/compose/mount.ts +30 -0
- package/src/vajra/context.ts +183 -0
- package/src/vajra/events.ts +13 -0
- package/src/vajra/index.ts +4 -0
- package/src/vajra/leaf/plan-runner.ts +94 -0
- package/src/vajra/service.ts +15 -0
- package/src/workflow/primitives/agent.ts +3 -1
- package/src/workflow/runtime.ts +23 -15
- package/src/workflow/sandbox.ts +51 -9
- package/src/core/compaction-progress.ts +0 -205
- package/src/core/context-compact.ts +0 -124
- package/src/core/context-drain.ts +0 -113
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1.1: Compaction Progress Tracker
|
|
3
|
-
*
|
|
4
|
-
* Provides user-visible progress feedback during context compaction:
|
|
5
|
-
* - Phase indicator (which compaction layer is active)
|
|
6
|
-
* - Message count (how many messages being processed)
|
|
7
|
-
* - Retry countdown for API-based summarization
|
|
8
|
-
* - Stuck detection hints
|
|
9
|
-
*
|
|
10
|
-
* Designed as an event emitter that the UI layer subscribes to.
|
|
11
|
-
* Falls back to stderr output when no UI subscriber is registered.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// ── Types ──
|
|
15
|
-
|
|
16
|
-
export type CompactionPhase =
|
|
17
|
-
'microcompact' | 'snip' | 'reactive-compact' | 'emergency-drain' | 'complete'
|
|
18
|
-
|
|
19
|
-
export interface CompactionStatus {
|
|
20
|
-
phase: CompactionPhase
|
|
21
|
-
/** Human-readable description of current action */
|
|
22
|
-
message: string
|
|
23
|
-
/** 0-100 percentage, or -1 if indeterminate */
|
|
24
|
-
percent: number
|
|
25
|
-
/** Total messages before compaction */
|
|
26
|
-
messagesBefore: number
|
|
27
|
-
/** Messages remaining after compaction */
|
|
28
|
-
messagesAfter: number
|
|
29
|
-
/** Tokens before compaction */
|
|
30
|
-
tokensBefore: number
|
|
31
|
-
/** Tokens after compaction */
|
|
32
|
-
tokensAfter: number
|
|
33
|
-
/** Elapsed ms since compaction started */
|
|
34
|
-
elapsedMs: number
|
|
35
|
-
/** Retry count for summarization API calls */
|
|
36
|
-
retryCount: number
|
|
37
|
-
/** Whether a retry is in progress */
|
|
38
|
-
retrying: boolean
|
|
39
|
-
/** Whether the compaction appears stuck (no progress for >30s) */
|
|
40
|
-
stuck: boolean
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export type ProgressCallback = (status: CompactionStatus) => void
|
|
44
|
-
|
|
45
|
-
// ── Constants ──
|
|
46
|
-
|
|
47
|
-
/** Timeout in ms after which we consider compaction "stuck" */
|
|
48
|
-
const STUCK_TIMEOUT_MS = 30_000
|
|
49
|
-
|
|
50
|
-
/** Max retries for summarizer API calls */
|
|
51
|
-
const MAX_SUMMARIZER_RETRIES = 3
|
|
52
|
-
|
|
53
|
-
/** Retry delay in ms between summarizer attempts */
|
|
54
|
-
const RETRY_DELAY_MS = 2_000
|
|
55
|
-
|
|
56
|
-
// ── Tracker ──
|
|
57
|
-
|
|
58
|
-
export class CompactionProgressTracker {
|
|
59
|
-
private callback: ProgressCallback | null = null
|
|
60
|
-
private status: CompactionStatus
|
|
61
|
-
private startTime = 0
|
|
62
|
-
private lastProgressTime = 0
|
|
63
|
-
|
|
64
|
-
constructor(messagesBefore: number, tokensBefore: number) {
|
|
65
|
-
this.status = {
|
|
66
|
-
phase: 'microcompact',
|
|
67
|
-
message: 'Starting compaction...',
|
|
68
|
-
percent: 0,
|
|
69
|
-
messagesBefore,
|
|
70
|
-
messagesAfter: messagesBefore,
|
|
71
|
-
tokensBefore,
|
|
72
|
-
tokensAfter: tokensBefore,
|
|
73
|
-
elapsedMs: 0,
|
|
74
|
-
retryCount: 0,
|
|
75
|
-
retrying: false,
|
|
76
|
-
stuck: false,
|
|
77
|
-
}
|
|
78
|
-
this.startTime = Date.now()
|
|
79
|
-
this.lastProgressTime = Date.now()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Register a UI callback for progress updates. */
|
|
83
|
-
onProgress(cb: ProgressCallback): void {
|
|
84
|
-
this.callback = cb
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Update phase with new values. */
|
|
88
|
-
update(partial: {
|
|
89
|
-
phase?: CompactionPhase
|
|
90
|
-
message?: string
|
|
91
|
-
percent?: number
|
|
92
|
-
messagesAfter?: number
|
|
93
|
-
tokensAfter?: number
|
|
94
|
-
retrying?: boolean
|
|
95
|
-
}): void {
|
|
96
|
-
if (partial.phase !== undefined) this.status.phase = partial.phase
|
|
97
|
-
if (partial.message !== undefined) this.status.message = partial.message
|
|
98
|
-
if (partial.percent !== undefined) this.status.percent = partial.percent
|
|
99
|
-
if (partial.messagesAfter !== undefined) this.status.messagesAfter = partial.messagesAfter
|
|
100
|
-
if (partial.tokensAfter !== undefined) this.status.tokensAfter = partial.tokensAfter
|
|
101
|
-
if (partial.retrying !== undefined) this.status.retrying = partial.retrying
|
|
102
|
-
|
|
103
|
-
this.status.elapsedMs = Date.now() - this.startTime
|
|
104
|
-
|
|
105
|
-
// Check for stuck detection
|
|
106
|
-
if (this.status.phase !== 'complete') {
|
|
107
|
-
const timeSinceProgress = Date.now() - this.lastProgressTime
|
|
108
|
-
this.status.stuck = timeSinceProgress > STUCK_TIMEOUT_MS
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.emit()
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Mark current retry attempt. */
|
|
115
|
-
onRetry(attempt: number): void {
|
|
116
|
-
this.status.retryCount = attempt
|
|
117
|
-
this.status.retrying = true
|
|
118
|
-
this.emit()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Mark retry as complete (success or giving up). */
|
|
122
|
-
onRetryComplete(): void {
|
|
123
|
-
this.status.retrying = false
|
|
124
|
-
this.lastProgressTime = Date.now()
|
|
125
|
-
this.status.stuck = false
|
|
126
|
-
this.emit()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Mark compaction as complete. */
|
|
130
|
-
complete(messagesAfter: number, tokensAfter: number): void {
|
|
131
|
-
this.status.phase = 'complete'
|
|
132
|
-
this.status.percent = 100
|
|
133
|
-
this.status.messagesAfter = messagesAfter
|
|
134
|
-
this.status.tokensAfter = tokensAfter
|
|
135
|
-
this.status.message = `Compaction complete: ${this.status.messagesBefore} → ${messagesAfter} messages`
|
|
136
|
-
this.status.elapsedMs = Date.now() - this.startTime
|
|
137
|
-
this.emit()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Get current status. */
|
|
141
|
-
getStatus(): CompactionStatus {
|
|
142
|
-
return { ...this.status }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Generate a compact summary string for CLI display. */
|
|
146
|
-
getSummary(): string {
|
|
147
|
-
const s = this.status
|
|
148
|
-
const elapsed = (s.elapsedMs / 1000).toFixed(1)
|
|
149
|
-
const stuckHint = s.stuck ? ' (stalled — press Esc to cancel)' : ''
|
|
150
|
-
const retryHint = s.retrying ? ` [retry ${s.retryCount}/${MAX_SUMMARIZER_RETRIES}...]` : ''
|
|
151
|
-
const pct = s.percent >= 0 ? ` ${s.percent}%` : ''
|
|
152
|
-
return `🔄 [${s.phase}]${pct} ${s.message}${retryHint}${stuckHint} (${elapsed}s)`
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private emit(): void {
|
|
156
|
-
if (this.callback) {
|
|
157
|
-
try {
|
|
158
|
-
this.callback(this.getStatus())
|
|
159
|
-
} catch {
|
|
160
|
-
// UI callback failure shouldn't block compaction
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ── Static helpers ──
|
|
166
|
-
|
|
167
|
-
/** Retry a summarizer call with backoff and progress tracking. */
|
|
168
|
-
static async retrySummarizer(
|
|
169
|
-
summarizer: () => Promise<string>,
|
|
170
|
-
tracker: CompactionProgressTracker,
|
|
171
|
-
messagesToSummarize: number,
|
|
172
|
-
): Promise<string> {
|
|
173
|
-
let lastError: Error | undefined
|
|
174
|
-
|
|
175
|
-
for (let attempt = 1; attempt <= MAX_SUMMARIZER_RETRIES; attempt++) {
|
|
176
|
-
try {
|
|
177
|
-
tracker.update({
|
|
178
|
-
message: `Summarizing ${messagesToSummarize} messages${attempt > 1 ? ` (attempt ${attempt}/${MAX_SUMMARIZER_RETRIES})` : ''}...`,
|
|
179
|
-
percent: 30 + attempt * 20,
|
|
180
|
-
retrying: attempt > 1,
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
const result = await summarizer()
|
|
184
|
-
tracker.onRetryComplete()
|
|
185
|
-
return result
|
|
186
|
-
} catch (err) {
|
|
187
|
-
lastError = err instanceof Error ? err : new Error(String(err))
|
|
188
|
-
|
|
189
|
-
if (attempt < MAX_SUMMARIZER_RETRIES) {
|
|
190
|
-
tracker.onRetry(attempt)
|
|
191
|
-
// Wait before retry
|
|
192
|
-
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS))
|
|
193
|
-
tracker.onRetryComplete()
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
tracker.update({
|
|
199
|
-
message: `Summarization failed after ${MAX_SUMMARIZER_RETRIES} attempts — falling back to truncation`,
|
|
200
|
-
percent: 60,
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
throw lastError ?? new Error('Summarization failed')
|
|
204
|
-
}
|
|
205
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { ContextManager, Summarizer } from './context'
|
|
2
|
-
import { snipMessages } from './context-snip'
|
|
3
|
-
import { CompactionProgressTracker } from './compaction-progress'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Layer 3: Reactive Compact — API-based summarization.
|
|
7
|
-
*
|
|
8
|
-
* When the context exceeds the compaction threshold, uses an LLM summarizer
|
|
9
|
-
* to condense the conversation history, keeping recent messages intact.
|
|
10
|
-
*
|
|
11
|
-
* After compaction, the context contains:
|
|
12
|
-
* 1. A summary message of the truncated history
|
|
13
|
-
* 2. The most recent N messages (kept intact)
|
|
14
|
-
* 3. The system prompt (unchanged)
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Adaptive keep-recent count: scales with context window size.
|
|
18
|
-
* 128K → 20, 200K → 20, 500K → 40, 1M → 80
|
|
19
|
-
*/
|
|
20
|
-
function getKeepRecent(contextWindow: number): number {
|
|
21
|
-
return Math.max(20, Math.floor(contextWindow / 12500))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CompactResult {
|
|
25
|
-
/** Messages before compaction */
|
|
26
|
-
beforeCount: number
|
|
27
|
-
/** Messages after compaction */
|
|
28
|
-
afterCount: number
|
|
29
|
-
/** Whether compaction was actually performed */
|
|
30
|
-
compacted: boolean
|
|
31
|
-
/** Progress summary for display */
|
|
32
|
-
summary: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function reactiveCompact(
|
|
36
|
-
context: ContextManager,
|
|
37
|
-
summarizer: Summarizer,
|
|
38
|
-
heading: string,
|
|
39
|
-
keepRecent?: number,
|
|
40
|
-
): Promise<CompactResult> {
|
|
41
|
-
// Use explicit override, or adaptive default based on context window
|
|
42
|
-
const effectiveKeepRecent = keepRecent ?? getKeepRecent(context.getMaxTokens())
|
|
43
|
-
const messages = context.getMessages()
|
|
44
|
-
const tokensBefore = context.getEstimatedTokens()
|
|
45
|
-
|
|
46
|
-
if (messages.length <= effectiveKeepRecent + 4) {
|
|
47
|
-
return {
|
|
48
|
-
beforeCount: messages.length,
|
|
49
|
-
afterCount: messages.length,
|
|
50
|
-
compacted: false,
|
|
51
|
-
summary: `Context has ${messages.length} messages — no compaction needed (threshold: ${effectiveKeepRecent + 4}).`,
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ── Progress tracker ──
|
|
56
|
-
const tracker = new CompactionProgressTracker(messages.length, tokensBefore)
|
|
57
|
-
tracker.update({ phase: 'snip', message: 'Snipping empty pairs...', percent: 5 })
|
|
58
|
-
|
|
59
|
-
// First, run snip to remove empty pairs
|
|
60
|
-
const { messages: snipped } = snipMessages(messages)
|
|
61
|
-
tracker.update({
|
|
62
|
-
phase: 'snip',
|
|
63
|
-
message: `Snipped ${messages.length - snipped.length} empty pairs`,
|
|
64
|
-
percent: 15,
|
|
65
|
-
messagesAfter: snipped.length,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
if (snipped.length <= effectiveKeepRecent + 4) {
|
|
69
|
-
const afterCount = snipped.length
|
|
70
|
-
tracker.complete(afterCount, tokensBefore)
|
|
71
|
-
return {
|
|
72
|
-
beforeCount: messages.length,
|
|
73
|
-
afterCount,
|
|
74
|
-
compacted: true,
|
|
75
|
-
summary: `Snip-only: ${messages.length} → ${afterCount} messages (removed ${messages.length - afterCount} empty pairs).`,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Split: old messages to summarize, recent messages to keep
|
|
80
|
-
const toSummarize = snipped.slice(0, -effectiveKeepRecent)
|
|
81
|
-
const toKeep = snipped.slice(-effectiveKeepRecent)
|
|
82
|
-
|
|
83
|
-
tracker.update({
|
|
84
|
-
phase: 'reactive-compact',
|
|
85
|
-
message: `Summarizing ${toSummarize.length} messages...`,
|
|
86
|
-
percent: 20,
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// Generate summary with retry logic
|
|
90
|
-
let summary: string
|
|
91
|
-
try {
|
|
92
|
-
summary = await CompactionProgressTracker.retrySummarizer(
|
|
93
|
-
() => summarizer(toSummarize, heading),
|
|
94
|
-
tracker,
|
|
95
|
-
toSummarize.length,
|
|
96
|
-
)
|
|
97
|
-
} catch {
|
|
98
|
-
// On summarizer failure, do a simple truncation
|
|
99
|
-
tracker.update({
|
|
100
|
-
message: `Summarizer unavailable — truncating to last ${effectiveKeepRecent} messages.`,
|
|
101
|
-
percent: 80,
|
|
102
|
-
})
|
|
103
|
-
summary = `Earlier conversation (${toSummarize.length} messages) omitted due to context limits.`
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Rebuild messages: summary + recent
|
|
107
|
-
const summaryMsg = {
|
|
108
|
-
role: 'user' as const,
|
|
109
|
-
content: `[Earlier conversation summary — ${heading}]: ${summary.slice(0, 8000)}`,
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const newMessages = [summaryMsg, ...toKeep]
|
|
113
|
-
context.replaceMessages(newMessages)
|
|
114
|
-
|
|
115
|
-
const tokensAfter = context.getEstimatedTokens()
|
|
116
|
-
tracker.complete(newMessages.length, tokensAfter)
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
beforeCount: messages.length,
|
|
120
|
-
afterCount: newMessages.length,
|
|
121
|
-
compacted: true,
|
|
122
|
-
summary: tracker.getSummary(),
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { ContextManager } from './context'
|
|
2
|
-
import type { CompactionProgressTracker } from './compaction-progress'
|
|
3
|
-
|
|
4
|
-
const MINIMAL_KEEP = 5
|
|
5
|
-
let drainLevel = 0
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Layer 4: Emergency Drain — 413 error recovery.
|
|
9
|
-
*
|
|
10
|
-
* Called when a 413 (context too large) error is received.
|
|
11
|
-
* Progressively strips messages until the context fits:
|
|
12
|
-
*
|
|
13
|
-
* Level 1: Drop earliest 50% of messages
|
|
14
|
-
* Level 2+: Keep only system prompt + last 5 messages
|
|
15
|
-
*
|
|
16
|
-
* Returns drain result with progress summary.
|
|
17
|
-
*/
|
|
18
|
-
export interface DrainResult {
|
|
19
|
-
/** Whether recovery was possible */
|
|
20
|
-
recovered: boolean
|
|
21
|
-
/** Messages before drain */
|
|
22
|
-
beforeCount: number
|
|
23
|
-
/** Messages after drain */
|
|
24
|
-
afterCount: number
|
|
25
|
-
/** Current drain level */
|
|
26
|
-
level: number
|
|
27
|
-
/** Human-readable summary */
|
|
28
|
-
summary: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function emergencyDrain(
|
|
32
|
-
context: ContextManager,
|
|
33
|
-
progress?: CompactionProgressTracker,
|
|
34
|
-
): Promise<DrainResult> {
|
|
35
|
-
const messages = context.getMessages()
|
|
36
|
-
|
|
37
|
-
if (messages.length <= MINIMAL_KEEP) {
|
|
38
|
-
return {
|
|
39
|
-
recovered: false,
|
|
40
|
-
beforeCount: messages.length,
|
|
41
|
-
afterCount: messages.length,
|
|
42
|
-
level: drainLevel,
|
|
43
|
-
summary: `Cannot drain further: already at minimal ${messages.length} messages.`,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
progress?.update({
|
|
48
|
-
phase: 'emergency-drain',
|
|
49
|
-
message: `Emergency drain level ${drainLevel + 1}: recovering from 413 error...`,
|
|
50
|
-
percent: 50,
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
if (drainLevel === 0) {
|
|
54
|
-
// First attempt: drop earliest 50%
|
|
55
|
-
const keepCount = Math.max(MINIMAL_KEEP, Math.floor(messages.length / 2))
|
|
56
|
-
const kept = messages.slice(-keepCount)
|
|
57
|
-
context.replaceMessages(kept)
|
|
58
|
-
drainLevel++
|
|
59
|
-
progress?.update({
|
|
60
|
-
phase: 'emergency-drain',
|
|
61
|
-
message: `Drain level 1: dropped ${messages.length - keepCount} messages (${messages.length} → ${keepCount})`,
|
|
62
|
-
percent: 80,
|
|
63
|
-
messagesAfter: keepCount,
|
|
64
|
-
})
|
|
65
|
-
return {
|
|
66
|
-
recovered: true,
|
|
67
|
-
beforeCount: messages.length,
|
|
68
|
-
afterCount: keepCount,
|
|
69
|
-
level: drainLevel,
|
|
70
|
-
summary: `Emergency drain level 1: ${messages.length} → ${keepCount} messages (dropped earliest 50%).`,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Subsequent attempts: keep only system + last MINIMAL_KEEP messages
|
|
75
|
-
const kept = messages.slice(-MINIMAL_KEEP)
|
|
76
|
-
|
|
77
|
-
// Add a summary placeholder if we're dropping a lot
|
|
78
|
-
if (messages.length > MINIMAL_KEEP * 2) {
|
|
79
|
-
const summaryMsg = {
|
|
80
|
-
role: 'user' as const,
|
|
81
|
-
content: `[Emergency context drain level ${drainLevel + 1}: ${messages.length - MINIMAL_KEEP} earlier messages discarded due to token limit.]`,
|
|
82
|
-
}
|
|
83
|
-
context.replaceMessages([summaryMsg, ...kept])
|
|
84
|
-
} else {
|
|
85
|
-
context.replaceMessages(kept)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
drainLevel++
|
|
89
|
-
progress?.update({
|
|
90
|
-
phase: 'emergency-drain',
|
|
91
|
-
message: `Drain level ${drainLevel}: ${messages.length} → ${kept.length} messages`,
|
|
92
|
-
percent: 90,
|
|
93
|
-
messagesAfter: kept.length,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
recovered: true,
|
|
98
|
-
beforeCount: messages.length,
|
|
99
|
-
afterCount: kept.length,
|
|
100
|
-
level: drainLevel,
|
|
101
|
-
summary: `Emergency drain level ${drainLevel}: ${messages.length} → ${kept.length} messages.`,
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Reset drain level (call at session start). */
|
|
106
|
-
export function resetDrainLevel(): void {
|
|
107
|
-
drainLevel = 0
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Get current drain level for display. */
|
|
111
|
-
export function getDrainLevel(): number {
|
|
112
|
-
return drainLevel
|
|
113
|
-
}
|