Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/openclaw-plugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export type MemoryOpenVikingConfig = {
recallPreferAbstract?: boolean;
recallTokenBudget?: number;
commitTokenThreshold?: number;
/** Ratio of model context window to use as commit threshold (0.0-1.0 exclusive).
* When set with a known context window, overrides commitTokenThreshold.
* Example: 0.38 with a 1M token model = ~380K threshold. */
commitTokenThresholdRatio?: number;
bypassSessionPatterns?: string[];
ingestReplyAssist?: boolean;
ingestReplyAssistMinSpeakerTurns?: number;
Expand Down Expand Up @@ -160,6 +164,7 @@ export const memoryOpenVikingConfigSchema = {
"recallPreferAbstract",
"recallTokenBudget",
"commitTokenThreshold",
"commitTokenThresholdRatio",
"bypassSessionPatterns",
"ingestReplyAssist",
"ingestReplyAssistMinSpeakerTurns",
Expand Down Expand Up @@ -231,6 +236,14 @@ export const memoryOpenVikingConfigSchema = {
0,
Math.min(100_000, Math.floor(toNumber(cfg.commitTokenThreshold, DEFAULT_COMMIT_TOKEN_THRESHOLD))),
),
commitTokenThresholdRatio: (() => {
const raw = toNumber(cfg.commitTokenThresholdRatio, 0);
if (raw > 0 && raw < 1) return raw;
if (raw !== 0) {
throw new Error("commitTokenThresholdRatio must be between 0 and 1 (exclusive)");
}
return 0;
})(),
bypassSessionPatterns: toStringArray(
cfg.bypassSessionPatterns,
toStringArray(
Expand Down Expand Up @@ -373,6 +386,12 @@ export const memoryOpenVikingConfigSchema = {
advanced: true,
help: "Minimum estimated pending tokens before auto-commit triggers. Set to 0 to commit every turn.",
},
commitTokenThresholdRatio: {
label: "Commit Threshold Ratio",
placeholder: "0.38",
advanced: true,
help: "Ratio of model context window (0-1) to use as commit threshold. Overrides commitTokenThreshold when set. Example: 0.38 with 1M context = ~380K token threshold.",
},
ingestReplyAssist: {
label: "Ingest Reply Assist",
help: "When transcript-like memory ingestion is detected, add a lightweight reply instruction to reduce NO_REPLY.",
Expand Down
25 changes: 24 additions & 1 deletion openviking/session/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,17 @@ def __init__(
ctx: Optional[RequestContext] = None,
session_id: Optional[str] = None,
auto_commit_threshold: int = 8000,
auto_commit_threshold_ratio: Optional[float] = None,
context_window: Optional[int] = None,
):
if auto_commit_threshold_ratio is not None:
if not (0.0 < auto_commit_threshold_ratio < 1.0):
raise ValueError(
"auto_commit_threshold_ratio must be between 0.0 and 1.0 (exclusive)"
)
if context_window is not None and context_window <= 0:
raise ValueError("context_window must be a positive integer")

self._viking_fs = viking_fs
self._vikingdb_manager = vikingdb_manager
self._session_compressor = session_compressor
Expand All @@ -171,6 +181,8 @@ def __init__(
self.session_id = session_id or str(uuid4())
self.created_at = int(datetime.now(timezone.utc).timestamp() * 1000)
self._auto_commit_threshold = auto_commit_threshold
self._auto_commit_threshold_ratio = auto_commit_threshold_ratio
self._context_window = context_window
self._session_uri = f"viking://session/{self.user.user_space_name()}/{self.session_id}"

self._messages: List[Message] = []
Expand All @@ -182,6 +194,18 @@ def __init__(

logger.info(f"Session created: {self.session_id} for user {self.user}")

@property
def effective_commit_threshold(self) -> int:
"""Compute the effective auto-commit threshold.

Priority: ratio * context_window > fixed value > default (8000).
When auto_commit_threshold_ratio and context_window are both set,
the threshold is computed as a percentage of the model's context window.
"""
if self._auto_commit_threshold_ratio is not None and self._context_window is not None:
return int(self._context_window * self._auto_commit_threshold_ratio)
return self._auto_commit_threshold

async def load(self):
"""Load session data from storage."""
if self._loaded:
Expand Down Expand Up @@ -474,7 +498,6 @@ async def commit_async(self) -> Dict[str, Any]:
"trace_id": trace_id,
}


async def _run_memory_extraction(
self,
task_id: str,
Expand Down
104 changes: 104 additions & 0 deletions tests/session/test_session_threshold_ratio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: AGPL-3.0

"""Tests for ratio-based auto-commit threshold (issue #1172)."""

from unittest.mock import MagicMock

import pytest

from openviking.session.session import Session


def _make_session(**kwargs):
"""Create a Session with minimal mocked dependencies."""
viking_fs = MagicMock()
return Session(viking_fs=viking_fs, **kwargs)


class TestEffectiveCommitThreshold:
"""Test effective_commit_threshold property."""

def test_default_threshold(self):
"""Default threshold is 8000 when no ratio or context_window."""
session = _make_session()
assert session.effective_commit_threshold == 8000

def test_fixed_threshold(self):
"""Fixed threshold is used when no ratio is set."""
session = _make_session(auto_commit_threshold=50000)
assert session.effective_commit_threshold == 50000

def test_ratio_with_context_window(self):
"""Ratio * context_window is used when both are provided."""
session = _make_session(
auto_commit_threshold_ratio=0.38,
context_window=1_000_000,
)
assert session.effective_commit_threshold == 380000

def test_ratio_overrides_fixed(self):
"""Ratio takes precedence over fixed threshold."""
session = _make_session(
auto_commit_threshold=20000,
auto_commit_threshold_ratio=0.38,
context_window=200_000,
)
assert session.effective_commit_threshold == 76000

def test_ratio_without_context_window_falls_back(self):
"""Ratio alone (no context_window) falls back to fixed threshold."""
session = _make_session(
auto_commit_threshold=15000,
auto_commit_threshold_ratio=0.38,
)
assert session.effective_commit_threshold == 15000

def test_context_window_without_ratio_falls_back(self):
"""context_window alone (no ratio) falls back to fixed threshold."""
session = _make_session(
auto_commit_threshold=15000,
context_window=1_000_000,
)
assert session.effective_commit_threshold == 15000

def test_small_model(self):
"""Ratio works for small models (200K context)."""
session = _make_session(
auto_commit_threshold_ratio=0.38,
context_window=200_000,
)
assert session.effective_commit_threshold == 76000


class TestThresholdValidation:
"""Test parameter validation."""

def test_ratio_too_high(self):
with pytest.raises(ValueError, match="between 0.0 and 1.0"):
_make_session(auto_commit_threshold_ratio=1.0)

def test_ratio_too_low(self):
with pytest.raises(ValueError, match="between 0.0 and 1.0"):
_make_session(auto_commit_threshold_ratio=0.0)

def test_ratio_negative(self):
with pytest.raises(ValueError, match="between 0.0 and 1.0"):
_make_session(auto_commit_threshold_ratio=-0.5)

def test_ratio_above_one(self):
with pytest.raises(ValueError, match="between 0.0 and 1.0"):
_make_session(auto_commit_threshold_ratio=1.5)

def test_context_window_zero(self):
with pytest.raises(ValueError, match="positive integer"):
_make_session(context_window=0)

def test_context_window_negative(self):
with pytest.raises(ValueError, match="positive integer"):
_make_session(context_window=-100)

def test_valid_ratio(self):
"""Valid ratio does not raise."""
session = _make_session(auto_commit_threshold_ratio=0.5, context_window=100000)
assert session.effective_commit_threshold == 50000
Loading