@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencodev2-slim",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Smart context management plugin for OpenCode v2 - semantic compression, cost-aware pruning, adaptive thresholds",
5
5
  "keywords": [
6
6
  "opencode",
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
@@ -19,6 +19,7 @@ const DEFAULT_CONFIG: SlimConfig = {
19
19
  nudgeForce: "soft",
20
20
  protectUserMessages: false,
21
21
  protectedTools: ["task", "skill", "todowrite", "todoread"],
22
+ keepRecent: 5,
22
23
  },
23
24
  strategies: {
24
25
  deduplication: {
@@ -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