@serkanalgur/opencodev2-slim 2.0.14 → 2.0.15
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/package.json +1 -1
- package/src/index.ts +14 -2
- package/src/lib/config.ts +1 -0
- package/src/lib/strategies.ts +126 -1
- package/src/lib/types.ts +2 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
pruneInPlace,
|
|
17
17
|
injectLimitNudges,
|
|
18
18
|
findLastUserMessage,
|
|
19
|
+
autoCompress,
|
|
19
20
|
} from "./lib/strategies"
|
|
20
21
|
import { getSystemPrompt, getCompressToolDescription } from "./lib/prompts"
|
|
21
22
|
import { buildPanelData, renderPanel } from "./lib/tui"
|
|
@@ -385,12 +386,12 @@ export default Plugin.define({
|
|
|
385
386
|
event.system.push({ type: "text", text: getSystemPrompt() })
|
|
386
387
|
})
|
|
387
388
|
|
|
388
|
-
// ─── Messages Transform Hook (sync)
|
|
389
|
+
// ─── Messages Transform Hook (sync → async) ─────────────────────────
|
|
389
390
|
// DCP pipeline for every outgoing request: sync compression blocks,
|
|
390
391
|
// replace covered ranges with summary placeholders, prune (dedup +
|
|
391
392
|
// purge errored tool inputs), then apply DCP limit rules as anchored
|
|
392
393
|
// nudges. Session history is never modified — only this request.
|
|
393
|
-
await ctx.session.hook("context", (event) => {
|
|
394
|
+
await ctx.session.hook("context", async (event) => {
|
|
394
395
|
const sessionId = event.sessionID
|
|
395
396
|
const config = getConfig(sessionId)
|
|
396
397
|
if (!config.enabled) return
|
|
@@ -442,6 +443,17 @@ export default Plugin.define({
|
|
|
442
443
|
const limits = resolveCompressLimits(config, state, providerId, modelId)
|
|
443
444
|
injectLimitNudges(state, config, event.messages, totalTokens, limits)
|
|
444
445
|
|
|
446
|
+
// 5) Auto-compress: when over the max limit, directly compress old
|
|
447
|
+
// messages without waiting for the model to call the compress tool.
|
|
448
|
+
// Registers a compression block so future requests use the summary.
|
|
449
|
+
if (totalTokens > limits.max) {
|
|
450
|
+
try {
|
|
451
|
+
await autoCompress(state, config, event.messages, totalTokens, limits)
|
|
452
|
+
} catch {
|
|
453
|
+
// Best-effort: auto-compress failure should never break the request.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
445
457
|
saveSessionState(state, config.persistence.directory)
|
|
446
458
|
})
|
|
447
459
|
|
package/src/lib/config.ts
CHANGED
package/src/lib/strategies.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MessageWithParts, SlimConfig, SessionState, CompressionBlock } from "./types"
|
|
2
|
-
import { getToolName, getMessageText } from "./compress"
|
|
2
|
+
import { getToolName, getMessageText, getToolResultContent, countTokens } from "./compress"
|
|
3
3
|
import { contextLimitNudge, turnNudge, iterationNudge, NUDGE_MARKERS } from "./prompts"
|
|
4
|
+
import { addCompressionRecord } from "./state"
|
|
4
5
|
|
|
5
6
|
// ─── DCP-style Compression Blocks ───────────────────────────────────────────
|
|
6
7
|
//
|
|
@@ -608,4 +609,128 @@ export function injectLimitNudges(
|
|
|
608
609
|
)
|
|
609
610
|
|
|
610
611
|
state.nudges = nudges
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─── Auto-compress: directly compress when over limit ───────────────────────
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Automatically compresses old messages when context exceeds the max limit.
|
|
618
|
+
* Called from the context hook when overMax is true — no model cooperation needed.
|
|
619
|
+
* Registers a compression block so future requests use the summary instead.
|
|
620
|
+
*/
|
|
621
|
+
export async function autoCompress(
|
|
622
|
+
state: SessionState,
|
|
623
|
+
config: SlimConfig,
|
|
624
|
+
messages: any[],
|
|
625
|
+
currentTokens: number,
|
|
626
|
+
limits: { max: number; min: number },
|
|
627
|
+
): Promise<{ compressed: boolean; messageCount?: number; tokensSaved?: number }> {
|
|
628
|
+
if (config.compress.permission === "deny") return { compressed: false }
|
|
629
|
+
if (state.manualMode) return { compressed: false }
|
|
630
|
+
if (limits.max <= 0) return { compressed: false }
|
|
631
|
+
if (currentTokens <= limits.max) return { compressed: false }
|
|
632
|
+
|
|
633
|
+
// Throttle: don't auto-compress more than once every 5 minutes
|
|
634
|
+
const now = Date.now()
|
|
635
|
+
const lastAuto = (state as any).lastAutoCompressTime ?? 0
|
|
636
|
+
if (now - lastAuto < 5 * 60 * 1000) return { compressed: false }
|
|
637
|
+
|
|
638
|
+
// Don't auto-compress if the model just compressed in the last assistant turn
|
|
639
|
+
const lastAssistant = [...messages].reverse().find((m: any) => m?.role === "assistant")
|
|
640
|
+
if (lastAssistant && messageHasCompress(lastAssistant)) return { compressed: false }
|
|
641
|
+
|
|
642
|
+
const keepRecent = Math.max(2, config.compress.keepRecent ?? 5)
|
|
643
|
+
const messageWithParts: MessageWithParts[] = messages.map((m: any) => ({
|
|
644
|
+
info: {
|
|
645
|
+
id: m?.id ?? m?.info?.id ?? "",
|
|
646
|
+
role: m?.role ?? m?.info?.role ?? "user",
|
|
647
|
+
sessionID: m?.sessionID ?? m?.info?.sessionID ?? "",
|
|
648
|
+
time: { created: Date.now() },
|
|
649
|
+
} as any,
|
|
650
|
+
parts: m?.parts ?? m?.content ?? [],
|
|
651
|
+
}))
|
|
652
|
+
|
|
653
|
+
// Select messages to compress: all except recent ones, with >100 tokens
|
|
654
|
+
const targetIndices: number[] = []
|
|
655
|
+
let inputTokens = 0
|
|
656
|
+
for (let i = 0; i < messageWithParts.length - keepRecent; i++) {
|
|
657
|
+
const msg = messageWithParts[i]
|
|
658
|
+
const text = getMessageText(msg) + getToolResultContent(msg)
|
|
659
|
+
const tokens = await countTokens(text)
|
|
660
|
+
if (tokens < 100) continue
|
|
661
|
+
targetIndices.push(i)
|
|
662
|
+
inputTokens += tokens
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (targetIndices.length === 0) {
|
|
666
|
+
;(state as any).lastAutoCompressTime = now
|
|
667
|
+
return { compressed: false }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Build summary
|
|
671
|
+
const targetMessages = targetIndices.map((i) => messageWithParts[i])
|
|
672
|
+
const summary = await buildCompressionSummary(
|
|
673
|
+
targetMessages,
|
|
674
|
+
"auto-compress: context limit exceeded",
|
|
675
|
+
config.compress.protectedTools,
|
|
676
|
+
config.compress.protectUserMessages,
|
|
677
|
+
)
|
|
678
|
+
const outputTokens = await countTokens(summary)
|
|
679
|
+
|
|
680
|
+
// Register compression block
|
|
681
|
+
const sorted = [...targetIndices].sort((a, b) => a - b)
|
|
682
|
+
const coveredIndices = new Set(sorted)
|
|
683
|
+
let anchorIndex = sorted[sorted.length - 1] + 1
|
|
684
|
+
if (anchorIndex >= messageWithParts.length) {
|
|
685
|
+
anchorIndex = messageWithParts.length - 1
|
|
686
|
+
coveredIndices.delete(anchorIndex)
|
|
687
|
+
}
|
|
688
|
+
if (anchorIndex < 0 || anchorIndex >= messageWithParts.length) {
|
|
689
|
+
;(state as any).lastAutoCompressTime = now
|
|
690
|
+
return { compressed: false }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const anchorId = messageWithParts[anchorIndex].info?.id
|
|
694
|
+
if (!anchorId) {
|
|
695
|
+
;(state as any).lastAutoCompressTime = now
|
|
696
|
+
return { compressed: false }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const coveredIds = [...coveredIndices]
|
|
700
|
+
.map((i) => messageWithParts[i].info?.id)
|
|
701
|
+
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
702
|
+
if (coveredIds.length === 0) {
|
|
703
|
+
;(state as any).lastAutoCompressTime = now
|
|
704
|
+
return { compressed: false }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
registerCompressionBlock(state, {
|
|
708
|
+
coveredIds,
|
|
709
|
+
anchorMessageId: anchorId,
|
|
710
|
+
summary,
|
|
711
|
+
topic: "auto-compress",
|
|
712
|
+
summaryTokens: outputTokens,
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
// Record compression stats
|
|
716
|
+
const ratio = inputTokens > 0 ? 1 - outputTokens / inputTokens : 0
|
|
717
|
+
addCompressionRecord(
|
|
718
|
+
state,
|
|
719
|
+
{
|
|
720
|
+
timestamp: now,
|
|
721
|
+
inputTokens,
|
|
722
|
+
outputTokens,
|
|
723
|
+
ratio,
|
|
724
|
+
messageCount: targetMessages.length,
|
|
725
|
+
success: true,
|
|
726
|
+
},
|
|
727
|
+
config.adaptive.learningRate,
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
;(state as any).lastAutoCompressTime = now
|
|
731
|
+
return {
|
|
732
|
+
compressed: true,
|
|
733
|
+
messageCount: targetMessages.length,
|
|
734
|
+
tokensSaved: inputTokens - outputTokens,
|
|
735
|
+
}
|
|
611
736
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface SlimConfig {
|
|
|
28
28
|
nudgeForce?: "strong" | "soft"
|
|
29
29
|
protectUserMessages: boolean
|
|
30
30
|
protectedTools: string[]
|
|
31
|
+
/** Number of recent messages to always keep during auto-compress (default: 5) */
|
|
32
|
+
keepRecent?: number
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
// Strategy settings
|