Skip to content
Merged
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
20 changes: 6 additions & 14 deletions crates/ov_cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,17 +364,12 @@ fn render_session_context(
compact: bool,
) -> Option<String> {
if !(obj.contains_key("latest_archive_overview")
&& obj.contains_key("latest_archive_id")
&& obj.contains_key("pre_archive_abstracts")
&& obj.contains_key("messages"))
{
return None;
}

let latest_archive_id = obj
.get("latest_archive_id")
.and_then(|v| v.as_str())
.unwrap_or("");
let latest_archive_overview = obj
.get("latest_archive_overview")
.and_then(|v| v.as_str())
Expand All @@ -385,14 +380,6 @@ fn render_session_context(
.unwrap_or_else(|| "0".to_string());

let mut lines: Vec<String> = Vec::new();
lines.push(format!(
"latest_archive_id {}",
if latest_archive_id.is_empty() {
"(none)"
} else {
latest_archive_id
}
));
lines.push(format!("estimated_tokens {}", estimated_tokens));

if let Some(stats) = obj.get("stats").and_then(|v| v.as_object()) {
Expand Down Expand Up @@ -426,7 +413,12 @@ fn render_session_context(
lines.push(String::new());
lines.push("latest_archive_overview".to_string());
if latest_archive_overview.is_empty() {
if latest_archive_id.is_empty() {
let has_abstracts = obj
.get("pre_archive_abstracts")
.and_then(|v| v.as_array())
.map(|items| !items.is_empty())
.unwrap_or(false);
if !has_abstracts {
lines.push("(none)".to_string());
} else {
lines.push("(trimmed by token budget or unavailable)".to_string());
Expand Down
16 changes: 8 additions & 8 deletions docs/en/api/05-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,12 @@ Get the assembled session context used by OpenClaw-style context rebuilding.

This endpoint returns:
- `latest_archive_overview`: the `overview` of the latest completed archive, when it fits the token budget
- `latest_archive_id`: the ID of the latest completed archive, used for archive expansion
- `pre_archive_abstracts`: lightweight history entries for older completed archives, each containing `archive_id` and `abstract`
- `pre_archive_abstracts`: lightweight entries for completed archives, each containing `archive_id` and `abstract`
- `messages`: all incomplete archive messages after the latest completed archive, plus current live session messages
- `stats`: token and inclusion stats for the returned context

Notes:
- `latest_archive_overview` becomes an empty string when no completed archive exists, or when the latest overview does not fit in the token budget.
- `latest_archive_id` is returned whenever a latest completed archive exists, even if `latest_archive_overview` is trimmed by budget.
- `token_budget` is applied to the assembled payload after active `messages`: `latest_archive_overview` has higher priority than `pre_archive_abstracts`, and older abstracts are dropped first when budget is tight.
- Only archive content that is actually returned is counted toward `estimatedTokens` and `stats.archiveTokens`.
- Session commit generates an archive summary during Phase 2 for every non-empty archive attempt. Only archives with a completed `.done` marker are exposed here.
Expand All @@ -206,7 +204,6 @@ Notes:
```python
context = await client.get_session_context("a1b2c3d4", token_budget=128000)
print(context["latest_archive_overview"])
print(context["latest_archive_id"])
print(context["pre_archive_abstracts"])
print(len(context["messages"]))

Expand Down Expand Up @@ -238,8 +235,11 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
"status": "ok",
"result": {
"latest_archive_overview": "# Session Summary\n\n**Overview**: User discussed deployment and auth setup.",
"latest_archive_id": "archive_002",
"pre_archive_abstracts": [
{
"archive_id": "archive_002",
"abstract": "User discussed deployment and authentication setup."
},
{
"archive_id": "archive_001",
"abstract": "User previously discussed repository bootstrap and authentication setup."
Expand All @@ -263,14 +263,14 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
"created_at": "2026-03-24T09:10:20Z"
}
],
"estimatedTokens": 160,
"estimatedTokens": 173,
"stats": {
"totalArchives": 2,
"includedArchives": 2,
"droppedArchives": 0,
"failedArchives": 0,
"activeTokens": 98,
"archiveTokens": 62
"archiveTokens": 75
}
}
}
Expand All @@ -282,7 +282,7 @@ ov session get-session-context a1b2c3d4 --token-budget 128000

Get the full contents of one completed archive for a session.

This endpoint is intended to work with `latest_archive_id` and `pre_archive_abstracts[*].archive_id` returned by `get_session_context()`.
This endpoint is intended to work with `pre_archive_abstracts[*].archive_id` returned by `get_session_context()`.

This endpoint returns:
- `archive_id`: the archive ID that was expanded
Expand Down
16 changes: 8 additions & 8 deletions docs/zh/api/05-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,12 @@ openviking session get a1b2c3d4

该接口返回:
- `latest_archive_overview`:最新一个已完成归档的 `overview` 文本,在 token budget 足够时返回
- `latest_archive_id`:最新一个已完成归档的 ID,用于后续展开 archive 详情
- `pre_archive_abstracts`:更早历史归档的轻量列表,每项只包含 `archive_id` 和 `abstract`
- `pre_archive_abstracts`:已完成归档的轻量列表,每项只包含 `archive_id` 和 `abstract`
- `messages`:最新已完成归档之后的所有未完成归档消息,再加上当前 live session 消息
- `stats`:返回结果对应的 token 与纳入统计

说明:
- 没有可用 completed archive,或最新 overview 超出 token budget 时,`latest_archive_overview` 返回空字符串。
- 只要存在最新 completed archive,就会返回 `latest_archive_id`;即使 `latest_archive_overview` 因 budget 被裁剪,这个 ID 仍然可用。
- `token_budget` 会在 active `messages` 之后作用于 assembled archive payload:`latest_archive_overview` 优先级高于 `pre_archive_abstracts`,预算紧张时先淘汰最旧的 abstracts。
- 只有最终实际返回的 archive 内容,才会计入 `estimatedTokens` 和 `stats.archiveTokens`。
- 当前每次有消息的 session commit 都会在 Phase 2 生成 archive 摘要;只有带 `.done` 标记的 completed archive 才会被这里返回。
Expand All @@ -206,7 +204,6 @@ openviking session get a1b2c3d4
```python
context = await client.get_session_context("a1b2c3d4", token_budget=128000)
print(context["latest_archive_overview"])
print(context["latest_archive_id"])
print(context["pre_archive_abstracts"])
print(len(context["messages"]))

Expand Down Expand Up @@ -238,8 +235,11 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
"status": "ok",
"result": {
"latest_archive_overview": "# Session Summary\n\n**Overview**: User discussed deployment and auth setup.",
"latest_archive_id": "archive_002",
"pre_archive_abstracts": [
{
"archive_id": "archive_002",
"abstract": "用户讨论了部署和鉴权配置。"
},
{
"archive_id": "archive_001",
"abstract": "用户之前讨论了仓库初始化和鉴权配置。"
Expand All @@ -263,14 +263,14 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
"created_at": "2026-03-24T09:10:20Z"
}
],
"estimatedTokens": 147,
"estimatedTokens": 160,
"stats": {
"totalArchives": 2,
"includedArchives": 2,
"droppedArchives": 0,
"failedArchives": 0,
"activeTokens": 98,
"archiveTokens": 49
"archiveTokens": 62
}
}
}
Expand All @@ -282,7 +282,7 @@ ov session get-session-context a1b2c3d4 --token-budget 128000

获取某次已完成归档的完整内容。

该接口通常配合 `get_session_context()` 返回的 `latest_archive_id` 或 `pre_archive_abstracts[*].archive_id` 使用。
该接口通常配合 `get_session_context()` 返回的 `pre_archive_abstracts[*].archive_id` 使用。

该接口返回:
- `archive_id`:被展开的 archive ID
Expand Down
23 changes: 14 additions & 9 deletions examples/openclaw-plugin/__tests__/context-engine-assemble.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ describe("context-engine assemble()", () => {
it("assembles summary archive and completed tool parts into agent messages", async () => {
const { engine, client, resolveAgentId } = makeEngine({
latest_archive_overview: "# Session Summary\nPreviously discussed repository setup.",
latest_archive_id: "archive_001",
pre_archive_abstracts: [],
pre_archive_abstracts: [
{
archive_id: "archive_001",
abstract: "Previously discussed repository setup.",
},
],
messages: [
{
id: "msg_1",
Expand Down Expand Up @@ -104,14 +108,18 @@ describe("context-engine assemble()", () => {
tokenBudget: 4096,
});

expect(resolveAgentId).toHaveBeenCalledWith("session-1");
expect(resolveAgentId).toHaveBeenCalledWith("session-1", undefined, "session-1");
expect(client.getSessionContext).toHaveBeenCalledWith("session-1", 4096, "agent:session-1");
expect(result.estimatedTokens).toBe(321);
expect(result.systemPromptAddition).toContain("Compressed Context");
expect(result.systemPromptAddition).toContain("Session Context Guide");
expect(result.messages).toEqual([
{
role: "user",
content: "# Session Summary\nPreviously discussed repository setup.",
content: "[Session History Summary]\n# Session Summary\nPreviously discussed repository setup.",
},
{
role: "user",
content: "[Archive Index]\narchive_001: Previously discussed repository setup.",
},
{
role: "assistant",
Expand Down Expand Up @@ -139,7 +147,6 @@ describe("context-engine assemble()", () => {
it("emits a non-error toolResult for a running tool (not a synthetic error)", async () => {
const { engine } = makeEngine({
latest_archive_overview: "",
latest_archive_id: "",
pre_archive_abstracts: [],
messages: [
{
Expand Down Expand Up @@ -191,15 +198,14 @@ describe("context-engine assemble()", () => {
});
const text = (result.messages[1] as any).content?.[0]?.text ?? "";
expect(text).toContain("interrupted");
expect((result.messages[1] as { content: Array<{ text: string }> }).content[0]?.text).toContain(
expect((result.messages[1] as { content: Array<{ text: string }> }).content[0]?.text).not.toContain(
"missing tool result",
);
});

it("degrades tool parts without tool_id into assistant text blocks", async () => {
const { engine } = makeEngine({
latest_archive_overview: "",
latest_archive_id: "",
pre_archive_abstracts: [],
messages: [
{
Expand Down Expand Up @@ -248,7 +254,6 @@ describe("context-engine assemble()", () => {
it("falls back to live messages when assembled active messages look truncated", async () => {
const { engine } = makeEngine({
latest_archive_overview: "",
latest_archive_id: "",
pre_archive_abstracts: [],
messages: [
{
Expand Down
1 change: 0 additions & 1 deletion examples/openclaw-plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ export type PreArchiveAbstract = {

export type SessionContextResult = {
latest_archive_overview: string;
latest_archive_id: string;
pre_archive_abstracts: PreArchiveAbstract[];
messages: OVMessage[];
estimatedTokens: number;
Expand Down
16 changes: 5 additions & 11 deletions examples/openclaw-plugin/context-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,9 @@ export function createMemoryOpenVikingContextEngine(params: {
agentId,
);

const hasArchives = !!ctx?.latest_archive_id;
const activeCount = ctx?.messages?.length ?? 0;
const preAbstracts = ctx?.pre_archive_abstracts ?? [];
const hasArchives = !!ctx?.latest_archive_overview || preAbstracts.length > 0;
const activeCount = ctx?.messages?.length ?? 0;

if (!ctx || (!hasArchives && activeCount === 0)) {
diag("assemble_result", OVSessionId, {
Expand Down Expand Up @@ -633,15 +633,10 @@ export function createMemoryOpenVikingContextEngine(params: {
});
}

if (preAbstracts.length > 0 || ctx.latest_archive_id) {
if (preAbstracts.length > 0) {
const lines: string[] = preAbstracts.map(
(a) => `${a.archive_id}: ${a.abstract}`,
);
if (ctx.latest_archive_id) {
lines.push(
`(latest: ${ctx.latest_archive_id} — see [Session History Summary] above)`,
);
}
assembled.push({
role: "user" as const,
content: `[Archive Index]\n${lines.join("\n")}`,
Expand All @@ -656,7 +651,7 @@ export function createMemoryOpenVikingContextEngine(params: {
if (sanitized.length === 0 && messages.length > 0) {
diag("assemble_result", OVSessionId, {
passthrough: true, reason: "sanitized_empty",
archiveCount: preAbstracts.length + (ctx.latest_archive_id ? 1 : 0),
archiveCount: preAbstracts.length,
activeCount,
outputMessagesCount: messages.length,
inputTokenEstimate: originalTokens,
Expand All @@ -667,7 +662,7 @@ export function createMemoryOpenVikingContextEngine(params: {
}

const assembledTokens = roughEstimate(sanitized);
const archiveCount = preAbstracts.length + (ctx.latest_archive_id ? 1 : 0);
const archiveCount = preAbstracts.length;
const tokensSaved = originalTokens - assembledTokens;
const savingPct = originalTokens > 0 ? Math.round((tokensSaved / originalTokens) * 100) : 0;

Expand All @@ -680,7 +675,6 @@ export function createMemoryOpenVikingContextEngine(params: {
estimatedTokens: assembledTokens,
tokensSaved,
savingPct,
latestArchiveId: ctx.latest_archive_id ?? null,
messages: messageDigest(sanitized),
});

Expand Down
7 changes: 1 addition & 6 deletions openviking/session/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,9 +740,7 @@ async def get_session_context(self, token_budget: int = 128_000) -> Dict[str, An
remaining_budget -= item["tokens"]

archive_tokens = latest_archive_tokens + pre_archive_tokens
included_archives = (1 if include_latest_overview else 0) + len(
included_pre_archive_abstracts
)
included_archives = len(included_pre_archive_abstracts)
dropped_archives = max(
0, context["total_archives"] - context["failed_archives"] - included_archives
)
Expand All @@ -751,7 +749,6 @@ async def get_session_context(self, token_budget: int = 128_000) -> Dict[str, An
"latest_archive_overview": (
latest_archive["overview"] if include_latest_overview else ""
),
"latest_archive_id": latest_archive["archive_id"] if latest_archive else "",
"pre_archive_abstracts": included_pre_archive_abstracts,
"messages": [m.to_dict() for m in merged_messages],
"estimatedTokens": message_tokens + archive_tokens,
Expand Down Expand Up @@ -835,8 +832,6 @@ async def _collect_session_context_components(self) -> Dict[str, Any]:
archive["archive_uri"], overview
),
}
continue

abstract = await self._read_archive_abstract(archive["archive_uri"])
if abstract:
pre_archive_abstracts.append(
Expand Down
2 changes: 0 additions & 2 deletions tests/server/test_api_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ async def test_get_session_context(client: httpx.AsyncClient):
body = resp.json()
assert body["status"] == "ok"
assert body["result"]["latest_archive_overview"] == ""
assert body["result"]["latest_archive_id"] == ""
assert body["result"]["pre_archive_abstracts"] == []
assert [m["parts"][0]["text"] for m in body["result"]["messages"]] == ["Current live message"]

Expand Down Expand Up @@ -331,7 +330,6 @@ async def test_get_session_context_endpoint_returns_trimmed_latest_archive_and_m

result = body["result"]
assert result["latest_archive_overview"] == ""
assert result["latest_archive_id"] == "archive_001"
assert result["pre_archive_abstracts"] == []
assert len(result["messages"]) == 1
assert result["messages"][0]["role"] == "assistant"
Expand Down
1 change: 0 additions & 1 deletion tests/server/test_http_client_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ async def test_sdk_session_lifecycle(http_client):

context = await client.get_session_context(session_id)
assert context["latest_archive_overview"] == ""
assert context["latest_archive_id"] == ""
assert context["pre_archive_abstracts"] == []
assert [m["parts"][0]["text"] for m in context["messages"]] == ["Hello from SDK"]

Expand Down
Loading
Loading