mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 01:33:53 +00:00
feat(context-engine): report compaction successors as typed session targets (#101182)
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
8e2a9a52395b4260db088d846fbe05c0b0efb49206fa269defca962ffd460b56 plugin-sdk-api-baseline.json
|
||||
f47745180987a636a48bad5c5b68b517894f5fbc5a09aa69a310fe482bc03450 plugin-sdk-api-baseline.jsonl
|
||||
6e49a90dc8b8f7e935705d0097ceb95f577b59982ee4d8e9fe9c18be49a69a6a plugin-sdk-api-baseline.json
|
||||
2a6c0ffe663d64b4a7c0d9caba4c51fb95507116357aa073874f0d5014bbb3c4 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -217,8 +217,11 @@ Required members:
|
||||
</ParamField>
|
||||
|
||||
`compact` returns a `CompactResult`. When compaction rotates the active
|
||||
transcript, `result.sessionId` and `result.sessionFile` identify the successor
|
||||
session that the next retry or turn must use.
|
||||
transcript, `result.sessionTarget` (a typed `ContextEngineSessionTarget`
|
||||
carrying the storage mode, session identity, and transcript artifact path)
|
||||
identifies the successor session that the next retry or turn must use;
|
||||
`result.sessionId` mirrors the successor id. `result.sessionFile` is
|
||||
deprecated - report successors through `sessionTarget` instead.
|
||||
|
||||
Optional members:
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
),
|
||||
publicExports: readPluginSdkSurfaceBudgetEnv(
|
||||
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
|
||||
10462,
|
||||
10463,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
resolveContextEngineOwnerPluginId,
|
||||
} from "../../context-engine/registry.js";
|
||||
import { buildContextEngineRuntimeSettings } from "../../context-engine/runtime-settings.js";
|
||||
import type {
|
||||
ContextEngine,
|
||||
ContextEngineRuntimeContext,
|
||||
ContextEngineRuntimeSettings,
|
||||
import {
|
||||
resolveCompactionSuccessorTranscript,
|
||||
type ContextEngine,
|
||||
type ContextEngineRuntimeContext,
|
||||
type ContextEngineRuntimeSettings,
|
||||
} from "../../context-engine/types.js";
|
||||
import {
|
||||
createFileBackedCompactionCheckpointStore,
|
||||
@@ -440,8 +441,9 @@ export async function compactEmbeddedAgentSession(
|
||||
reason: formatErrorMessage(compactErr),
|
||||
};
|
||||
}
|
||||
const delegatedSessionId = result.result?.sessionId;
|
||||
const delegatedSessionFile = result.result?.sessionFile;
|
||||
const delegatedSuccessor = resolveCompactionSuccessorTranscript(result);
|
||||
const delegatedSessionId = delegatedSuccessor.sessionId;
|
||||
const delegatedSessionFile = delegatedSuccessor.sessionFile;
|
||||
const delegatedRotatedTranscript =
|
||||
(typeof delegatedSessionId === "string" && delegatedSessionId !== params.sessionId) ||
|
||||
(typeof delegatedSessionFile === "string" && delegatedSessionFile !== params.sessionFile);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
resolveContextEngineOwnerPluginId,
|
||||
} from "../../context-engine/registry.js";
|
||||
import { buildContextEngineRuntimeSettings } from "../../context-engine/runtime-settings.js";
|
||||
import { resolveCompactionSuccessorTranscript } from "../../context-engine/types.js";
|
||||
import {
|
||||
assertAgentRunLifecycleGenerationCurrent,
|
||||
captureAgentRunLifecycleGeneration,
|
||||
@@ -1873,13 +1874,12 @@ async function runEmbeddedAgentInternal(
|
||||
compactResult: Awaited<ReturnType<typeof contextEngine.compact>>,
|
||||
): string | undefined => {
|
||||
const previousSessionId = activeSessionId;
|
||||
const nextSessionId = compactResult.result?.sessionId;
|
||||
const nextSessionFile = compactResult.result?.sessionFile;
|
||||
adoptActiveSessionId(nextSessionId);
|
||||
if (nextSessionFile && nextSessionFile !== activeSessionFile) {
|
||||
activeSessionFile = nextSessionFile;
|
||||
const successor = resolveCompactionSuccessorTranscript(compactResult);
|
||||
adoptActiveSessionId(successor.sessionId);
|
||||
if (successor.sessionFile && successor.sessionFile !== activeSessionFile) {
|
||||
activeSessionFile = successor.sessionFile;
|
||||
}
|
||||
return nextSessionId && nextSessionId !== previousSessionId
|
||||
return successor.sessionId && successor.sessionId !== previousSessionId
|
||||
? previousSessionId
|
||||
: undefined;
|
||||
};
|
||||
@@ -1939,7 +1939,9 @@ async function runEmbeddedAgentInternal(
|
||||
messageCount: -1,
|
||||
compactedCount: -1,
|
||||
tokenCount: compactResult.result?.tokensAfter,
|
||||
sessionFile: compactResult.result?.sessionFile ?? activeSessionFile,
|
||||
sessionFile:
|
||||
resolveCompactionSuccessorTranscript(compactResult).sessionFile ??
|
||||
activeSessionFile,
|
||||
...(previousSessionId ? { previousSessionId } : {}),
|
||||
},
|
||||
resolveActiveHookContext(),
|
||||
|
||||
@@ -26,14 +26,15 @@ import type {
|
||||
ContextEngineFactoryContext,
|
||||
ContextEngineRegistrationResult,
|
||||
} from "./registry.js";
|
||||
import type {
|
||||
ContextEngine,
|
||||
ContextEngineInfo,
|
||||
AssembleResult,
|
||||
CompactResult,
|
||||
ContextEngineMaintenanceResult,
|
||||
BootstrapResult,
|
||||
IngestResult,
|
||||
import {
|
||||
resolveCompactionSuccessorTranscript,
|
||||
type ContextEngine,
|
||||
type ContextEngineInfo,
|
||||
type AssembleResult,
|
||||
type CompactResult,
|
||||
type ContextEngineMaintenanceResult,
|
||||
type BootstrapResult,
|
||||
type IngestResult,
|
||||
} from "./types.js";
|
||||
|
||||
const { compactEmbeddedAgentSessionDirectMock } = vi.hoisted(() => ({
|
||||
@@ -599,10 +600,79 @@ describe("Engine contract tests", () => {
|
||||
tokensBefore: 0,
|
||||
tokensAfter: 0,
|
||||
details: undefined,
|
||||
sessionTarget: {
|
||||
kind: "file",
|
||||
sessionId: "s2",
|
||||
sessionFile: "/tmp/session.json",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("delegateCompactionToRuntime reports the rotated successor as a typed session target", async () => {
|
||||
compactEmbeddedAgentSessionDirectMock.mockResolvedValue({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
result: {
|
||||
summary: "compacted",
|
||||
firstKeptEntryId: "e1",
|
||||
tokensBefore: 100,
|
||||
tokensAfter: 10,
|
||||
sessionId: "rotated-id",
|
||||
sessionFile: "/tmp/rotated.jsonl",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await delegateCompactionToRuntime({
|
||||
sessionId: "s-rotate",
|
||||
sessionKey: "agent:main:test",
|
||||
sessionFile: "/tmp/session.json",
|
||||
runtimeContext: {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentId: "main",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.result?.sessionTarget).toEqual({
|
||||
kind: "file",
|
||||
agentId: "main",
|
||||
sessionId: "rotated-id",
|
||||
sessionKey: "agent:main:test",
|
||||
sessionFile: "/tmp/rotated.jsonl",
|
||||
});
|
||||
// Deprecated raw fields stay populated for shipped plugin-sdk readers.
|
||||
expect(result.result?.sessionId).toBe("rotated-id");
|
||||
expect(result.result?.sessionFile).toBe("/tmp/rotated.jsonl");
|
||||
});
|
||||
|
||||
it("resolveCompactionSuccessorTranscript prefers the typed target over deprecated raw fields", () => {
|
||||
expect(
|
||||
resolveCompactionSuccessorTranscript({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
result: {
|
||||
tokensBefore: 1,
|
||||
sessionId: "raw-id",
|
||||
sessionFile: "/tmp/raw.jsonl",
|
||||
sessionTarget: {
|
||||
kind: "file",
|
||||
sessionId: "target-id",
|
||||
sessionFile: "/tmp/target.jsonl",
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual({ sessionId: "target-id", sessionFile: "/tmp/target.jsonl" });
|
||||
|
||||
// Raw-field fallback covers shipped engines that predate sessionTarget.
|
||||
expect(
|
||||
resolveCompactionSuccessorTranscript({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
result: { tokensBefore: 1, sessionId: "raw-id", sessionFile: "/tmp/raw.jsonl" },
|
||||
}),
|
||||
).toEqual({ sessionId: "raw-id", sessionFile: "/tmp/raw.jsonl" });
|
||||
});
|
||||
|
||||
it("delegateCompactionToRuntime forwards the caller abortSignal to the runtime (#89868)", async () => {
|
||||
installCompactRuntimeSpy();
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -56,6 +56,13 @@ export async function delegateCompactionToRuntime(
|
||||
typeof runtimeContext.workspaceDir === "string" ? runtimeContext.workspaceDir : process.cwd(),
|
||||
});
|
||||
|
||||
// Post-compaction live session: the runtime reports rotation through the
|
||||
// result sessionId/sessionFile pair; otherwise the input session stays live.
|
||||
const successorSessionId = result.result?.sessionId ?? params.sessionId;
|
||||
const successorSessionFile = result.result?.sessionFile ?? params.sessionFile;
|
||||
const agentId = runtimeContext.sessionTarget?.agentId ?? runtimeContext.agentId;
|
||||
const sessionKey = params.sessionKey ?? runtimeContext.sessionKey;
|
||||
|
||||
return {
|
||||
ok: result.ok,
|
||||
compacted: result.compacted,
|
||||
@@ -68,7 +75,16 @@ export async function delegateCompactionToRuntime(
|
||||
tokensAfter: result.result.tokensAfter,
|
||||
details: result.result.details,
|
||||
sessionId: result.result.sessionId,
|
||||
// Deprecated raw path stays populated for shipped plugin-sdk readers
|
||||
// of the delegate result; sessionTarget is the canonical successor.
|
||||
sessionFile: result.result.sessionFile,
|
||||
sessionTarget: {
|
||||
kind: "file",
|
||||
...(agentId ? { agentId } : {}),
|
||||
sessionId: successorSessionId,
|
||||
...(sessionKey ? { sessionKey } : {}),
|
||||
sessionFile: successorSessionFile,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -125,6 +125,25 @@ export class ContextEngineRuntimeSettingsUnsupportedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed session target for compaction results.
|
||||
*
|
||||
* Makes the storage mode explicit and carries precise session identity
|
||||
* instead of a raw file path string. File-backed is the only storage mode
|
||||
* today; new storage modes extend this union when they ship.
|
||||
*/
|
||||
export type ContextEngineSessionTarget = {
|
||||
kind: "file";
|
||||
/** Agent that owns the session, when the caller resolved it. */
|
||||
agentId?: string;
|
||||
/** Runtime session id for the post-compaction live session. */
|
||||
sessionId: string;
|
||||
/** Stable session key used for aliases, policy, and store resolution. */
|
||||
sessionKey?: string;
|
||||
/** Live transcript artifact path for the file-backed session. */
|
||||
sessionFile: string;
|
||||
};
|
||||
|
||||
export type CompactResult = {
|
||||
ok: boolean;
|
||||
compacted: boolean;
|
||||
@@ -137,11 +156,38 @@ export type CompactResult = {
|
||||
details?: unknown;
|
||||
/** Session id after compaction, when the runtime rotated transcripts. */
|
||||
sessionId?: string;
|
||||
/** Session file after compaction, when the runtime rotated transcripts. */
|
||||
/** Typed post-compaction live session target; successor when the runtime rotated transcripts. */
|
||||
sessionTarget?: ContextEngineSessionTarget;
|
||||
/**
|
||||
* Raw session file path after compaction.
|
||||
*
|
||||
* @deprecated Use `sessionTarget`. Shipped plugin-sdk contract: released
|
||||
* third-party context engines (v2026.6.x and earlier) report rotated
|
||||
* transcripts through this field. Remove once typed session targets are
|
||||
* the only successor contract.
|
||||
*/
|
||||
sessionFile?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the post-compaction live transcript identity from a compact result.
|
||||
*
|
||||
* Prefers the typed `sessionTarget`. Reading the raw fields is the named
|
||||
* compat path for shipped third-party engines that predate `sessionTarget`;
|
||||
* it is removed together with the deprecated `sessionFile` result field.
|
||||
*/
|
||||
export function resolveCompactionSuccessorTranscript(result: CompactResult): {
|
||||
sessionId?: string;
|
||||
sessionFile?: string;
|
||||
} {
|
||||
const target = result.result?.sessionTarget;
|
||||
return {
|
||||
sessionId: target?.sessionId ?? result.result?.sessionId,
|
||||
sessionFile: target?.sessionFile ?? result.result?.sessionFile,
|
||||
};
|
||||
}
|
||||
|
||||
export type IngestResult = {
|
||||
/** Whether the message was ingested (false if duplicate or no-op) */
|
||||
ingested: boolean;
|
||||
|
||||
@@ -135,6 +135,7 @@ export type {
|
||||
ContextEngineRuntimeMode,
|
||||
ContextEngineRuntimeSettings,
|
||||
ContextEngineSelectionSource,
|
||||
ContextEngineSessionTarget,
|
||||
IngestBatchResult,
|
||||
IngestResult,
|
||||
SubagentEndReason,
|
||||
|
||||
Reference in New Issue
Block a user