Skip to content

Commit eee816f

Browse files
committed
fix(session): fold latest archive into abstract list
Return the latest archive through pre_archive_abstracts and drop the separate latest_archive_id field so session context consumers only need one archive index path.
1 parent c6b15db commit eee816f

File tree

10 files changed

+56
-80
lines changed

10 files changed

+56
-80
lines changed

crates/ov_cli/src/output.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -364,17 +364,12 @@ fn render_session_context(
364364
compact: bool,
365365
) -> Option<String> {
366366
if !(obj.contains_key("latest_archive_overview")
367-
&& obj.contains_key("latest_archive_id")
368367
&& obj.contains_key("pre_archive_abstracts")
369368
&& obj.contains_key("messages"))
370369
{
371370
return None;
372371
}
373372

374-
let latest_archive_id = obj
375-
.get("latest_archive_id")
376-
.and_then(|v| v.as_str())
377-
.unwrap_or("");
378373
let latest_archive_overview = obj
379374
.get("latest_archive_overview")
380375
.and_then(|v| v.as_str())
@@ -385,14 +380,6 @@ fn render_session_context(
385380
.unwrap_or_else(|| "0".to_string());
386381

387382
let mut lines: Vec<String> = Vec::new();
388-
lines.push(format!(
389-
"latest_archive_id {}",
390-
if latest_archive_id.is_empty() {
391-
"(none)"
392-
} else {
393-
latest_archive_id
394-
}
395-
));
396383
lines.push(format!("estimated_tokens {}", estimated_tokens));
397384

398385
if let Some(stats) = obj.get("stats").and_then(|v| v.as_object()) {
@@ -426,7 +413,12 @@ fn render_session_context(
426413
lines.push(String::new());
427414
lines.push("latest_archive_overview".to_string());
428415
if latest_archive_overview.is_empty() {
429-
if latest_archive_id.is_empty() {
416+
let has_abstracts = obj
417+
.get("pre_archive_abstracts")
418+
.and_then(|v| v.as_array())
419+
.map(|items| !items.is_empty())
420+
.unwrap_or(false);
421+
if !has_abstracts {
430422
lines.push("(none)".to_string());
431423
} else {
432424
lines.push("(trimmed by token budget or unavailable)".to_string());

docs/en/api/05-sessions.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,12 @@ Get the assembled session context used by OpenClaw-style context rebuilding.
182182

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

190189
Notes:
191190
- `latest_archive_overview` becomes an empty string when no completed archive exists, or when the latest overview does not fit in the token budget.
192-
- `latest_archive_id` is returned whenever a latest completed archive exists, even if `latest_archive_overview` is trimmed by budget.
193191
- `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.
194192
- Only archive content that is actually returned is counted toward `estimatedTokens` and `stats.archiveTokens`.
195193
- 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.
@@ -206,7 +204,6 @@ Notes:
206204
```python
207205
context = await client.get_session_context("a1b2c3d4", token_budget=128000)
208206
print(context["latest_archive_overview"])
209-
print(context["latest_archive_id"])
210207
print(context["pre_archive_abstracts"])
211208
print(len(context["messages"]))
212209

@@ -238,8 +235,11 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
238235
"status": "ok",
239236
"result": {
240237
"latest_archive_overview": "# Session Summary\n\n**Overview**: User discussed deployment and auth setup.",
241-
"latest_archive_id": "archive_002",
242238
"pre_archive_abstracts": [
239+
{
240+
"archive_id": "archive_002",
241+
"abstract": "User discussed deployment and authentication setup."
242+
},
243243
{
244244
"archive_id": "archive_001",
245245
"abstract": "User previously discussed repository bootstrap and authentication setup."
@@ -263,14 +263,14 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
263263
"created_at": "2026-03-24T09:10:20Z"
264264
}
265265
],
266-
"estimatedTokens": 160,
266+
"estimatedTokens": 173,
267267
"stats": {
268268
"totalArchives": 2,
269269
"includedArchives": 2,
270270
"droppedArchives": 0,
271271
"failedArchives": 0,
272272
"activeTokens": 98,
273-
"archiveTokens": 62
273+
"archiveTokens": 75
274274
}
275275
}
276276
}
@@ -282,7 +282,7 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
282282

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

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

287287
This endpoint returns:
288288
- `archive_id`: the archive ID that was expanded

docs/zh/api/05-sessions.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,12 @@ openviking session get a1b2c3d4
182182

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

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

@@ -238,8 +235,11 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
238235
"status": "ok",
239236
"result": {
240237
"latest_archive_overview": "# Session Summary\n\n**Overview**: User discussed deployment and auth setup.",
241-
"latest_archive_id": "archive_002",
242238
"pre_archive_abstracts": [
239+
{
240+
"archive_id": "archive_002",
241+
"abstract": "用户讨论了部署和鉴权配置。"
242+
},
243243
{
244244
"archive_id": "archive_001",
245245
"abstract": "用户之前讨论了仓库初始化和鉴权配置。"
@@ -263,14 +263,14 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
263263
"created_at": "2026-03-24T09:10:20Z"
264264
}
265265
],
266-
"estimatedTokens": 147,
266+
"estimatedTokens": 160,
267267
"stats": {
268268
"totalArchives": 2,
269269
"includedArchives": 2,
270270
"droppedArchives": 0,
271271
"failedArchives": 0,
272272
"activeTokens": 98,
273-
"archiveTokens": 49
273+
"archiveTokens": 62
274274
}
275275
}
276276
}
@@ -282,7 +282,7 @@ ov session get-session-context a1b2c3d4 --token-budget 128000
282282

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

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

287287
该接口返回:
288288
- `archive_id`:被展开的 archive ID

examples/openclaw-plugin/__tests__/context-engine-assemble.test.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ describe("context-engine assemble()", () => {
6666
it("assembles summary archive and completed tool parts into agent messages", async () => {
6767
const { engine, client, resolveAgentId } = makeEngine({
6868
latest_archive_overview: "# Session Summary\nPreviously discussed repository setup.",
69-
latest_archive_id: "archive_001",
70-
pre_archive_abstracts: [],
69+
pre_archive_abstracts: [
70+
{
71+
archive_id: "archive_001",
72+
abstract: "Previously discussed repository setup.",
73+
},
74+
],
7175
messages: [
7276
{
7377
id: "msg_1",
@@ -104,14 +108,18 @@ describe("context-engine assemble()", () => {
104108
tokenBudget: 4096,
105109
});
106110

107-
expect(resolveAgentId).toHaveBeenCalledWith("session-1");
111+
expect(resolveAgentId).toHaveBeenCalledWith("session-1", undefined, "session-1");
108112
expect(client.getSessionContext).toHaveBeenCalledWith("session-1", 4096, "agent:session-1");
109113
expect(result.estimatedTokens).toBe(321);
110-
expect(result.systemPromptAddition).toContain("Compressed Context");
114+
expect(result.systemPromptAddition).toContain("Session Context Guide");
111115
expect(result.messages).toEqual([
112116
{
113117
role: "user",
114-
content: "# Session Summary\nPreviously discussed repository setup.",
118+
content: "[Session History Summary]\n# Session Summary\nPreviously discussed repository setup.",
119+
},
120+
{
121+
role: "user",
122+
content: "[Archive Index]\narchive_001: Previously discussed repository setup.",
115123
},
116124
{
117125
role: "assistant",
@@ -139,7 +147,6 @@ describe("context-engine assemble()", () => {
139147
it("emits a non-error toolResult for a running tool (not a synthetic error)", async () => {
140148
const { engine } = makeEngine({
141149
latest_archive_overview: "",
142-
latest_archive_id: "",
143150
pre_archive_abstracts: [],
144151
messages: [
145152
{
@@ -191,15 +198,14 @@ describe("context-engine assemble()", () => {
191198
});
192199
const text = (result.messages[1] as any).content?.[0]?.text ?? "";
193200
expect(text).toContain("interrupted");
194-
expect((result.messages[1] as { content: Array<{ text: string }> }).content[0]?.text).toContain(
201+
expect((result.messages[1] as { content: Array<{ text: string }> }).content[0]?.text).not.toContain(
195202
"missing tool result",
196203
);
197204
});
198205

199206
it("degrades tool parts without tool_id into assistant text blocks", async () => {
200207
const { engine } = makeEngine({
201208
latest_archive_overview: "",
202-
latest_archive_id: "",
203209
pre_archive_abstracts: [],
204210
messages: [
205211
{
@@ -248,7 +254,6 @@ describe("context-engine assemble()", () => {
248254
it("falls back to live messages when assembled active messages look truncated", async () => {
249255
const { engine } = makeEngine({
250256
latest_archive_overview: "",
251-
latest_archive_id: "",
252257
pre_archive_abstracts: [],
253258
messages: [
254259
{

examples/openclaw-plugin/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export type PreArchiveAbstract = {
8686

8787
export type SessionContextResult = {
8888
latest_archive_overview: string;
89-
latest_archive_id: string;
9089
pre_archive_abstracts: PreArchiveAbstract[];
9190
messages: OVMessage[];
9291
estimatedTokens: number;

examples/openclaw-plugin/context-engine.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -596,9 +596,9 @@ export function createMemoryOpenVikingContextEngine(params: {
596596
agentId,
597597
);
598598

599-
const hasArchives = !!ctx?.latest_archive_id;
600-
const activeCount = ctx?.messages?.length ?? 0;
601599
const preAbstracts = ctx?.pre_archive_abstracts ?? [];
600+
const hasArchives = !!ctx?.latest_archive_overview || preAbstracts.length > 0;
601+
const activeCount = ctx?.messages?.length ?? 0;
602602

603603
if (!ctx || (!hasArchives && activeCount === 0)) {
604604
diag("assemble_result", OVSessionId, {
@@ -633,15 +633,10 @@ export function createMemoryOpenVikingContextEngine(params: {
633633
});
634634
}
635635

636-
if (preAbstracts.length > 0 || ctx.latest_archive_id) {
636+
if (preAbstracts.length > 0) {
637637
const lines: string[] = preAbstracts.map(
638638
(a) => `${a.archive_id}: ${a.abstract}`,
639639
);
640-
if (ctx.latest_archive_id) {
641-
lines.push(
642-
`(latest: ${ctx.latest_archive_id} — see [Session History Summary] above)`,
643-
);
644-
}
645640
assembled.push({
646641
role: "user" as const,
647642
content: `[Archive Index]\n${lines.join("\n")}`,
@@ -656,7 +651,7 @@ export function createMemoryOpenVikingContextEngine(params: {
656651
if (sanitized.length === 0 && messages.length > 0) {
657652
diag("assemble_result", OVSessionId, {
658653
passthrough: true, reason: "sanitized_empty",
659-
archiveCount: preAbstracts.length + (ctx.latest_archive_id ? 1 : 0),
654+
archiveCount: preAbstracts.length,
660655
activeCount,
661656
outputMessagesCount: messages.length,
662657
inputTokenEstimate: originalTokens,
@@ -667,7 +662,7 @@ export function createMemoryOpenVikingContextEngine(params: {
667662
}
668663

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

@@ -680,7 +675,6 @@ export function createMemoryOpenVikingContextEngine(params: {
680675
estimatedTokens: assembledTokens,
681676
tokensSaved,
682677
savingPct,
683-
latestArchiveId: ctx.latest_archive_id ?? null,
684678
messages: messageDigest(sanitized),
685679
});
686680

openviking/session/session.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -740,9 +740,7 @@ async def get_session_context(self, token_budget: int = 128_000) -> Dict[str, An
740740
remaining_budget -= item["tokens"]
741741

742742
archive_tokens = latest_archive_tokens + pre_archive_tokens
743-
included_archives = (1 if include_latest_overview else 0) + len(
744-
included_pre_archive_abstracts
745-
)
743+
included_archives = len(included_pre_archive_abstracts)
746744
dropped_archives = max(
747745
0, context["total_archives"] - context["failed_archives"] - included_archives
748746
)
@@ -751,7 +749,6 @@ async def get_session_context(self, token_budget: int = 128_000) -> Dict[str, An
751749
"latest_archive_overview": (
752750
latest_archive["overview"] if include_latest_overview else ""
753751
),
754-
"latest_archive_id": latest_archive["archive_id"] if latest_archive else "",
755752
"pre_archive_abstracts": included_pre_archive_abstracts,
756753
"messages": [m.to_dict() for m in merged_messages],
757754
"estimatedTokens": message_tokens + archive_tokens,
@@ -835,8 +832,6 @@ async def _collect_session_context_components(self) -> Dict[str, Any]:
835832
archive["archive_uri"], overview
836833
),
837834
}
838-
continue
839-
840835
abstract = await self._read_archive_abstract(archive["archive_uri"])
841836
if abstract:
842837
pre_archive_abstracts.append(

tests/server/test_api_sessions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ async def test_get_session_context(client: httpx.AsyncClient):
107107
body = resp.json()
108108
assert body["status"] == "ok"
109109
assert body["result"]["latest_archive_overview"] == ""
110-
assert body["result"]["latest_archive_id"] == ""
111110
assert body["result"]["pre_archive_abstracts"] == []
112111
assert [m["parts"][0]["text"] for m in body["result"]["messages"]] == ["Current live message"]
113112

@@ -331,7 +330,6 @@ async def test_get_session_context_endpoint_returns_trimmed_latest_archive_and_m
331330

332331
result = body["result"]
333332
assert result["latest_archive_overview"] == ""
334-
assert result["latest_archive_id"] == "archive_001"
335333
assert result["pre_archive_abstracts"] == []
336334
assert len(result["messages"]) == 1
337335
assert result["messages"][0]["role"] == "assistant"

tests/server/test_http_client_sdk.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ async def test_sdk_session_lifecycle(http_client):
150150

151151
context = await client.get_session_context(session_id)
152152
assert context["latest_archive_overview"] == ""
153-
assert context["latest_archive_id"] == ""
154153
assert context["pre_archive_abstracts"] == []
155154
assert [m["parts"][0]["text"] for m in context["messages"]] == ["Hello from SDK"]
156155

0 commit comments

Comments
 (0)