diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json index 9857b3eab9b3..4ae69604289c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness-runtime.json @@ -1 +1 @@ -{"contentHash":"935472a542772d2a003ced8e4ff4b21082d4a7ceb87162605eb3f081f3134c9e","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} +{"contentHash":"e93bb5a79e7a848c462ec88f8060fc87829dcd063da2c9698c27e19129754afc","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json index 24eefbb73de6..459c94444fd9 100644 --- a/docs/.generated/plugin-sdk-api-baseline/agent-harness.json +++ b/docs/.generated/plugin-sdk-api-baseline/agent-harness.json @@ -1 +1 @@ -{"contentHash":"3012ea8e6338549d7523326c5cf006e0cf6bcaef8cf0cfbd3b36001d18a00a79","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} +{"contentHash":"622cc8a24de6f41ff9d514399b034b7884d4f2e0f6f2bf75136640c80b1d8186","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-core.json b/docs/.generated/plugin-sdk-api-baseline/channel-core.json index 0bf2ea8934ed..ba5ed25bf944 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-core.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-core.json @@ -1 +1 @@ -{"contentHash":"dee0293611efcd65fa6ada3b091a8cd4cc4f9fc08322ee09f66b86d06c7c0da7","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} +{"contentHash":"edc6c7ca91050af6dfceb136b3887f41548707f48115ed8c0ee34d097f6e524f","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json index 66e2c65b5d45..adf07b9167a9 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-entry-contract.json @@ -1 +1 @@ -{"contentHash":"db8f468e891f92ca526beea61a7d31aa4ec3c274869418a97f107e880b3b0d43","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} +{"contentHash":"4b3acf9c914456173c538f6635a95d797b96a1238eaab20314cd3b4c17809c23","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json index 13da9ad43bc2..328e20d36d58 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-inbound.json @@ -1 +1 @@ -{"contentHash":"334b7b3c52e2b74c4540b10d94b3bc31ef6278324faa9cc48834b8599100267d","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} +{"contentHash":"81f7f67df730331d9fc2de922d1524833548a09ad69b2eb916440113ba91494c","entrypoint":"channel-inbound","importSpecifier":"openclaw/plugin-sdk/channel-inbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-message.json b/docs/.generated/plugin-sdk-api-baseline/channel-message.json index e95917b67e51..16900c237107 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-message.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-message.json @@ -1 +1 @@ -{"contentHash":"76a2a221a6a6f2295037d6ee58cac1d82adac001c6dc0467724c41ca1487d555","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} +{"contentHash":"a9c548d441e74fe5c60af72f3225ff71ad75ba503554c7496ed174d9162d7f43","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json index cfd278b733c7..86b5c15596d2 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-outbound.json @@ -1 +1 @@ -{"contentHash":"a288e638fab5e190ba81e2312d52a2fb4fbbfe09a764ce8ae71aead70c1a1f22","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} +{"contentHash":"eb36acbe0e79d92d2acc8545bafb3efb998a73c6c198b0f89449a497a85448ca","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json index 651d8f0f190c..b6b5234276b0 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-pairing.json @@ -1 +1 @@ -{"contentHash":"eb3ff0bf0aafe996ccfe512e5c7c9f6643240e07666e15f1990cf24f2d894e16","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} +{"contentHash":"76c953ab6a8a13215b8a608a8256f544ee9dbb8603f916e276a87ec1f020498d","entrypoint":"channel-pairing","importSpecifier":"openclaw/plugin-sdk/channel-pairing"} diff --git a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json index 15a4c72d7de6..8c98311b7989 100644 --- a/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json +++ b/docs/.generated/plugin-sdk-api-baseline/channel-plugin-common.json @@ -1 +1 @@ -{"contentHash":"fc7650d41aa0ee4feeb7d4a8c624f00666e5a280c0e400c2637dc4c90639f406","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} +{"contentHash":"ecd4319e3dd1c8b390bc41546d784a99c1e640d3fed8d46ee041b2396f6b2538","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"} diff --git a/docs/.generated/plugin-sdk-api-baseline/core.json b/docs/.generated/plugin-sdk-api-baseline/core.json index 8737f0d2b568..1d952e7a2b6c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/core.json +++ b/docs/.generated/plugin-sdk-api-baseline/core.json @@ -1 +1 @@ -{"contentHash":"b7ef17f9a6a055f6ccc6082ecfad2b3e14e54eb2538d8bcae2b14d48c38efc02","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} +{"contentHash":"028ba363143c607ec08a4343df43d00cd6eee9a75aa5a56b1f3a8bef0f3e639f","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"} diff --git a/docs/.generated/plugin-sdk-api-baseline/discord.json b/docs/.generated/plugin-sdk-api-baseline/discord.json index 24f9eb22e1cb..c004ef52a5ec 100644 --- a/docs/.generated/plugin-sdk-api-baseline/discord.json +++ b/docs/.generated/plugin-sdk-api-baseline/discord.json @@ -1 +1 @@ -{"contentHash":"13b35ecb4af318bc7688c39b790fa90c145886358e3256cfe24a70412a20e70f","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} +{"contentHash":"a9fedd2f64413f0f04751fc7551dbedb18e95573cf1bca3801de7defdf11a903","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"} diff --git a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json index a494515a345e..1fcf8d8ae3cd 100644 --- a/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json +++ b/docs/.generated/plugin-sdk-api-baseline/inbound-reply-dispatch.json @@ -1 +1 @@ -{"contentHash":"bc04e864df9f24ba9d6436e27030679e2e8ad0143e6f9f4012118c6526a43395","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} +{"contentHash":"db3caebf92c46468b996948e3a0ed6efeca1957a77704719fbf6bd81fc07095a","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"} diff --git a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json index 2ef4f748d271..6d04eb08dcdd 100644 --- a/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/meeting-runtime.json @@ -1 +1 @@ -{"contentHash":"b4e87ccdd939708f75d8b5807a7faf341ae1be06300cd6ad26b79173d1ba17a6","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} +{"contentHash":"10d52d83824b4b410912572ff36f135297fbeb8bb96328928fd30728e3f4546e","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json index 3316374c0ba5..a7e94fb755db 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-command-runtime.json @@ -1 +1 @@ -{"contentHash":"c8a32cced54577967d7086f920ddb577287b2e5098adac880d1845d4525c2541","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"} +{"contentHash":"a5dc90e2ddd9593efb159e77de52604f8e62d82a680879d22dbee1bbd0dda4ec","entrypoint":"plugin-command-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-command-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json index 833f8e91c400..9c1d7d69443f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-entry.json @@ -1 +1 @@ -{"contentHash":"83527106b0a04b3f7b812c47001498bd005fd9911f8dd0b97acd35d4d7f61f1f","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} +{"contentHash":"f1026c641b4a60fc6743fa07bced87548bf93ab1a7ac52076dde31e7ce6abe33","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"} diff --git a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json index abf3aaefae8f..ec0550d76ead 100644 --- a/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/plugin-runtime.json @@ -1 +1 @@ -{"contentHash":"e326d4d2b59155856361367d5aadebf7962baaebe8ef91155cedb636e0871f92","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} +{"contentHash":"f6370f0532670140d2a80069c39f492f62d2cf79f6f0245635e03492c0570131","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json index 1c9c1684eeba..5ed92b9f7048 100644 --- a/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json +++ b/docs/.generated/plugin-sdk-api-baseline/provider-catalog-runtime.json @@ -1 +1 @@ -{"contentHash":"f2d93ae0543dda1166f7f56c6a2f61910dd35ba4f539e4f31a89004020d04e18","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} +{"contentHash":"5436af0315589c464d451a2cfdca71dc52b79a0906580c6656ebddb790792e48","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"} diff --git a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json index 5670570f226f..e0a55fc1a715 100644 --- a/docs/.generated/plugin-sdk-api-baseline/runtime-store.json +++ b/docs/.generated/plugin-sdk-api-baseline/runtime-store.json @@ -1 +1 @@ -{"contentHash":"63d8847d2564ebc8e8e33a3aab6aa09b196bb2c9f028e9342bc9967c33a194d4","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} +{"contentHash":"6deafa205ad659665dc2894386fed7c30743b3a171831bbbc3d6edeaacf6b46f","entrypoint":"runtime-store","importSpecifier":"openclaw/plugin-sdk/runtime-store"} diff --git a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json index 2e1a8efe4145..b12c98cea03c 100644 --- a/docs/.generated/plugin-sdk-api-baseline/session-catalog.json +++ b/docs/.generated/plugin-sdk-api-baseline/session-catalog.json @@ -1 +1 @@ -{"contentHash":"8a19841ae6719cf88f86feb2ef6a73707d49486e8f75f9e547f565b64e1fb1af","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} +{"contentHash":"f82ffb1754104e576ecf21e622fbcc63c70333e8b1962371d4f868c8c3f35ede","entrypoint":"session-catalog","importSpecifier":"openclaw/plugin-sdk/session-catalog"} diff --git a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json index cbfd0fca157a..899b0cb4fad8 100644 --- a/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json +++ b/docs/.generated/plugin-sdk-api-baseline/tool-plugin.json @@ -1 +1 @@ -{"contentHash":"0fa1302f61d179c11329b49a6d7e1d242f8de7ab73c4e515667343b789a39e7d","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} +{"contentHash":"98be777b4b8309d6f8ff70db300b312482170eda016b93b94430ed137e932557","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"} diff --git a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json index dfd0d2300d1f..99b2b07eae4f 100644 --- a/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json +++ b/docs/.generated/plugin-sdk-api-baseline/webhook-ingress.json @@ -1 +1 @@ -{"contentHash":"3d1a41189c7b6a7d0318c9d262737190bdfcf375a7cd12869f90ad906cb2cfcc","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} +{"contentHash":"54038626c9146bfc9bc4681fb345c39ab7de151b78c901fa125109c596cfd665","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"} diff --git a/docs/concepts/context-engine.md b/docs/concepts/context-engine.md index 7f5eb091fafc..03ca332eb3fc 100644 --- a/docs/concepts/context-engine.md +++ b/docs/concepts/context-engine.md @@ -132,7 +132,7 @@ export default function register(api) { acceptedHostParams: ["sessionKey", "runtimeContext"], transcriptSemantics: { currentTurnFence: "before-current-turn-entry-v1", - turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1", + turnAdvancementIdempotency: "atomic-idempotent-v1", }, }, @@ -166,15 +166,7 @@ export default function register(api) { return { ok: true, compacted: true }; }, - async commitTurn({ advancementKey, messages, prePromptMessageCount }) { - // Retain v1 while durable rows from older plugin versions can exist. - return await commitAcceptedTurn({ - advancementKey, - messages: messages.slice(prePromptMessageCount), - }); - }, - - async commitTurnLocal({ advancementKey, messages }) { + async commitTurn({ advancementKey, messages }) { // Atomically store the accepted turn and advancementKey. Return // "duplicate" when that exact key was committed by an earlier retry. return await commitAcceptedTurn({ @@ -232,9 +224,9 @@ receive every current host field. For durable admitted turns, declare both transcript semantics: - `currentTurnFence: "before-current-turn-entry-v1"` -- `turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1"` +- `turnAdvancementIdempotency: "atomic-idempotent-v1"` -and implement `commitTurnLocal(...)` as one atomic, idempotent write keyed by +and implement `commitTurn(...)` as one atomic, idempotent write keyed by `advancementKey`. Return `{ status: "committed" }` for the first write and `{ status: "duplicate" }` when a host retry presents an already-committed key. The `messages` payload contains only the inclusive range from the admitted user @@ -243,15 +235,9 @@ transcript during bootstrap or rebuild should read it through the transcript cursor API, `readSessionTranscriptVisibleMessageDelta(...)`. Pre-turn transcript reads during bootstrap, maintenance, assembly, and retries then see the exact transcript prefix before the admitted user message. The host -calls `commitTurnLocal` only for the accepted successful turn; failed or aborted +calls `commitTurn` only for the accepted successful turn; failed or aborted turns do not advance context-engine state. -The turn-local contract is additive: engines must also retain `commitTurn(...)` -with the older `atomic-idempotent-v1` full-history semantics. OpenClaw uses it -to drain durable v1 work queued before a plugin upgrade. New commits use -`commitTurnLocal(...)`, so transcript history is obtained through the cursor -API instead of being repeated in every durable commit. - Without the full declaration and method, OpenClaw uses the legacy context path for the whole logical turn, including retries. The configured context-engine slot is not changed, and OpenClaw tries the configured engine again on the next diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index a47abc9d7a9a..f674421fe98c 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -626,20 +626,15 @@ For an end-to-end authoring guide, see To participate in durable admitted turns, context engines must declare `currentTurnFence: "before-current-turn-entry-v1"` and -`turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1"` under -`info.transcriptSemantics`, then implement `commitTurnLocal(...)` as an atomic, -idempotent write keyed by `advancementKey`. Retain `commitTurn(...)` with its -v1 full-history behavior so queued work survives plugin upgrades. OpenClaw supplies only the inclusive +`turnAdvancementIdempotency: "atomic-idempotent-v1"` under +`info.transcriptSemantics`, then implement `commitTurn(...)` as an atomic, +idempotent write keyed by `advancementKey`. OpenClaw supplies only the inclusive accepted turn, from its admitted user entry through its terminal entry; use the `readSessionTranscriptVisibleMessageDelta(...)` cursor API to bootstrap or rebuild earlier history. Without the full contract, OpenClaw uses the legacy context path for the whole logical turn and its retries, leaves the configured engine unchanged, and tries that engine again on the next logical turn. -The older `atomic-idempotent-v1` and `commitTurn(...)` contract supplies -transcript history plus `prePromptMessageCount`. The turn-local declaration is -valid only when the engine implements both commit methods. - ### Deprecated memory embedding adapters | Method | What it registers | diff --git a/src/agents/harness/context-engine-turn-attempt.test.ts b/src/agents/harness/context-engine-turn-attempt.test.ts index 8f6f7846940e..6e6c0fc6738c 100644 --- a/src/agents/harness/context-engine-turn-attempt.test.ts +++ b/src/agents/harness/context-engine-turn-attempt.test.ts @@ -28,43 +28,7 @@ afterEach(() => { // Keep durable-engine setup identical across range and recovery cases so each // test varies only the transcript state that owns the behavior under test. -function createTurnLocalDurableLease() { - const commitTurnLocal = vi.fn>(async () => ({ - status: "committed", - })); - const engine: ContextEngine = { - info: { - id: "test", - name: "Test", - transcriptSemantics: { - currentTurnFence: "before-current-turn-entry-v1", - turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1", - }, - }, - ingest: async () => ({ ingested: true }), - assemble: async ({ messages }) => ({ messages, estimatedTokens: 0 }), - compact: async () => ({ ok: true, compacted: false }), - commitTurn: async () => ({ status: "committed" }), - commitTurnLocal, - }; - const lease = { - engine, - effectiveEngine: engine, - effectiveEngineId: "test", - effectiveEnginePluginId: undefined, - degraded: false, - degradedReason: undefined, - selectForHost: vi.fn(), - degradeBeforeStart: vi.fn(), - begin: vi.fn(), - deferDisposalUntil: () => undefined, - dispose: async () => undefined, - } satisfies ContextEngineLogicalTurnLease; - return { commitTurnLocal, lease }; -} - -// Model the shipped v1 SDK contract independently from the turn-local helper. -function createV1DurableLease() { +function createDurableLease() { const commitTurn = vi.fn>(async () => ({ status: "committed", })); @@ -288,7 +252,6 @@ describe("accepted context-engine turn finalization", () => { }, maxEvents: 2, maxBytes: 1024, - messageRange: "turn-local-v1", }), ).toMatchObject({ kind: "ok", @@ -298,7 +261,7 @@ describe("accepted context-engine turn finalization", () => { ], }); - const { commitTurnLocal, lease } = createTurnLocalDurableLease(); + const { commitTurn, lease } = createDurableLease(); const admission = { ...admitted.anchor, logicalTurnId: "logical-turn-1", @@ -327,8 +290,8 @@ describe("accepted context-engine turn finalization", () => { await finalizeAcceptedContextEngineTurn({ facts: baseFacts, lease }); - expect(commitTurnLocal).toHaveBeenCalledOnce(); - expect(commitTurnLocal).toHaveBeenCalledWith( + expect(commitTurn).toHaveBeenCalledOnce(); + expect(commitTurn).toHaveBeenCalledWith( expect.objectContaining({ messages: [ expect.objectContaining({ role: "user", content: "current" }), @@ -336,7 +299,7 @@ describe("accepted context-engine turn finalization", () => { ], }), ); - expect(commitTurnLocal.mock.calls[0]?.[0]).not.toHaveProperty("prePromptMessageCount"); + expect(commitTurn.mock.calls[0]?.[0]).not.toHaveProperty("prePromptMessageCount"); const warn = vi.fn(); await finalizeAcceptedContextEngineTurn({ @@ -351,7 +314,7 @@ describe("accepted context-engine turn finalization", () => { warn, }); - expect(commitTurnLocal).toHaveBeenCalledOnce(); + expect(commitTurn).toHaveBeenCalledOnce(); expect(warn).toHaveBeenCalledWith( "[context-engine] skipped accepted turn advancement: accepted context-engine transcript range is stale", ); @@ -434,7 +397,7 @@ describe("accepted context-engine turn finalization", () => { warn, }); - expect(commitTurnLocal).toHaveBeenCalledOnce(); + expect(commitTurn).toHaveBeenCalledOnce(); expect(warn).toHaveBeenCalledWith( "[context-engine] skipped accepted turn advancement: accepted context-engine transcript range is non-descendant", ); @@ -484,7 +447,7 @@ describe("accepted context-engine turn finalization", () => { prefix: [0, 1, 2].map((index) => `prefix-${index} ${padding}`), sessionId: "large-prefix-turn", }); - const { commitTurnLocal, lease } = createTurnLocalDurableLease(); + const { commitTurn, lease } = createDurableLease(); const warn = vi.fn(); await finalizeAcceptedContextEngineTurn({ facts, lease, warn }); @@ -492,8 +455,8 @@ describe("accepted context-engine turn finalization", () => { expect(warn).not.toHaveBeenCalledWith( expect.stringContaining("accepted context-engine transcript range is too-large"), ); - expect(commitTurnLocal).toHaveBeenCalledOnce(); - const commitParams = commitTurnLocal.mock.calls[0]?.[0]; + expect(commitTurn).toHaveBeenCalledOnce(); + const commitParams = commitTurn.mock.calls[0]?.[0]; expect(commitParams?.messages).toEqual([ expect.objectContaining({ role: "user", content: "current" }), expect.objectContaining({ role: "assistant", content: "answer" }), @@ -501,29 +464,6 @@ describe("accepted context-engine turn finalization", () => { expect(commitParams).not.toHaveProperty("prePromptMessageCount"); }); - it("preserves the shipped v1 full-transcript commit contract", async () => { - const { facts } = await createAcceptedTurnFixture({ - answer: "answer", - logicalTurnId: "logical-turn-v1", - prefix: ["prior"], - sessionId: "v1-turn", - }); - const { commitTurn, lease } = createV1DurableLease(); - - await finalizeAcceptedContextEngineTurn({ facts, lease }); - - expect(commitTurn).toHaveBeenCalledWith( - expect.objectContaining({ - messages: [ - expect.objectContaining({ content: "prior" }), - expect.objectContaining({ content: "current" }), - expect.objectContaining({ content: "answer" }), - ], - prePromptMessageCount: 1, - }), - ); - }); - it("still blocks an accepted turn whose own range exceeds the cap", async () => { const { admission, database, facts } = await createAcceptedTurnFixture({ answer: `answer ${"x".repeat(9 * 1024 * 1024)}`, @@ -531,12 +471,12 @@ describe("accepted context-engine turn finalization", () => { prefix: ["prior"], sessionId: "oversized-turn", }); - const { commitTurnLocal, lease } = createTurnLocalDurableLease(); + const { commitTurn, lease } = createDurableLease(); const warn = vi.fn(); await finalizeAcceptedContextEngineTurn({ facts, lease, warn }); - expect(commitTurnLocal).not.toHaveBeenCalled(); + expect(commitTurn).not.toHaveBeenCalled(); expect(warn).toHaveBeenCalledWith( "[context-engine] skipped accepted turn advancement: accepted context-engine transcript range is too-large", ); diff --git a/src/agents/harness/context-engine-turn-attempt.ts b/src/agents/harness/context-engine-turn-attempt.ts index 52542e31e02b..4b126dd557a9 100644 --- a/src/agents/harness/context-engine-turn-attempt.ts +++ b/src/agents/harness/context-engine-turn-attempt.ts @@ -219,8 +219,6 @@ export async function finalizeAcceptedContextEngineTurn(params: { throw new Error("accepted context engine does not support durable turn advancement"); } const admission = params.facts.boundary.admission; - const turnAdvancementIdempotency = - params.lease.engine.info.transcriptSemantics!.turnAdvancementIdempotency!; const database = openOpenClawAgentDatabase({ agentId: admission.agentId, path: admission.storePath, @@ -231,16 +229,11 @@ export async function finalizeAcceptedContextEngineTurn(params: { engineId: params.lease.effectiveEngineId, isHeartbeat: params.facts.isHeartbeat === true, ownerPluginId: params.lease.effectiveEnginePluginId, - turnAdvancementIdempotency, }); const closedTurn = readClosedTranscriptTurn({ boundary: params.facts.boundary, maxEvents: ACCEPTED_TURN_MAX_EVENTS, maxBytes: ACCEPTED_TURN_MAX_BYTES, - messageRange: - turnAdvancementIdempotency === "atomic-idempotent-turn-local-v1" - ? "turn-local-v1" - : "full-transcript-v1", }); if (closedTurn.kind !== "ok") { if (!isRetryableContextEngineTurnReadFailure(closedTurn.kind)) { @@ -263,11 +256,6 @@ export async function finalizeAcceptedContextEngineTurn(params: { boundary: params.facts.boundary, isHeartbeat: params.facts.isHeartbeat === true, messages: closedTurn.messages, - prePromptMessageCount: - turnAdvancementIdempotency === "atomic-idempotent-turn-local-v1" - ? undefined - : closedTurn.prePromptMessageCount, - turnAdvancementIdempotency, }, }); await drainContextEngineTurnOutbox({ diff --git a/src/agents/harness/context-engine-turn-outbox.test.ts b/src/agents/harness/context-engine-turn-outbox.test.ts index 4c001285ac65..670b3e4e5ec8 100644 --- a/src/agents/harness/context-engine-turn-outbox.test.ts +++ b/src/agents/harness/context-engine-turn-outbox.test.ts @@ -74,7 +74,6 @@ function createPayload(params: { boundary, isHeartbeat: false, messages: [], - prePromptMessageCount: params.sequence, }; } @@ -138,52 +137,6 @@ describe("context-engine turn outbox", () => { ).toBeUndefined(); }); - it("drains a versionless v1 row after an engine adds the turn-local contract", async () => { - const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-context-outbox-upgrade-")); - tempDirs.push(stateDir); - const database = openOpenClawAgentDatabase({ - agentId: "main", - env: { OPENCLAW_STATE_DIR: stateDir }, - }); - const payload = createPayload({ - advancementKey: "session-a:legacy-ready", - databasePath: database.path, - sequence: 3, - sessionId: "session-a", - }); - enqueueContextEngineTurnCommit({ database, engineId: "test", payload }); - const commitTurn = vi.fn>(async () => ({ - status: "committed", - })); - const commitTurnLocal = vi.fn>(async () => ({ - status: "committed", - })); - const engine = { - info: { - id: "test", - name: "Test", - transcriptSemantics: { - turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1", - }, - }, - ingest: async () => ({ ingested: true }), - assemble: async ({ messages }) => ({ messages, estimatedTokens: 0 }), - compact: async () => ({ ok: true, compacted: false }), - commitTurn, - commitTurnLocal, - } satisfies ContextEngine; - - await drainContextEngineTurnOutbox({ - database, - engine, - engineId: "test", - warn: vi.fn(), - }); - - expect(commitTurn).toHaveBeenCalledWith(expect.objectContaining({ prePromptMessageCount: 3 })); - expect(commitTurnLocal).not.toHaveBeenCalled(); - }); - it("drains prior work before fresh-turn assembly and records dispatch admission", async () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-context-outbox-recovery-")); tempDirs.push(stateDir); @@ -232,7 +185,6 @@ describe("context-engine turn outbox", () => { database, engineId: "test", isHeartbeat: true, - turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1", }); const current = await appendTranscriptMessage(target, { message: { role: "user", content: "second" }, @@ -252,7 +204,7 @@ describe("context-engine turn outbox", () => { message: currentMessage, target: async () => undefined, }); - const commitTurnLocal = vi.fn>(async () => ({ + const commitTurn = vi.fn>(async () => ({ status: "committed", })); const engine = { @@ -261,14 +213,13 @@ describe("context-engine turn outbox", () => { name: "Test", transcriptSemantics: { currentTurnFence: "before-current-turn-entry-v1", - turnAdvancementIdempotency: "atomic-idempotent-turn-local-v1", + turnAdvancementIdempotency: "atomic-idempotent-v1", }, }, ingest: async () => ({ ingested: true }), assemble: async ({ messages }) => ({ messages, estimatedTokens: 0 }), compact: async () => ({ ok: true, compacted: false }), - commitTurn: async () => ({ status: "committed" as const }), - commitTurnLocal, + commitTurn, } satisfies ContextEngine; const lease = { engine, @@ -292,8 +243,8 @@ describe("context-engine turn outbox", () => { sessionTarget: target, }); - expect(commitTurnLocal).toHaveBeenCalledOnce(); - expect(commitTurnLocal).toHaveBeenCalledWith( + expect(commitTurn).toHaveBeenCalledOnce(); + expect(commitTurn).toHaveBeenCalledWith( expect.objectContaining({ advancementKey: admission.logicalTurnId, isHeartbeat: true, @@ -306,7 +257,7 @@ describe("context-engine turn outbox", () => { expect( database.db.prepare("SELECT advancement_key FROM context_engine_turn_outbox").all(), ).toHaveLength(0); - expect(commitTurnLocal.mock.calls[0]?.[0]).not.toHaveProperty("prePromptMessageCount"); + expect(commitTurn.mock.calls[0]?.[0]).not.toHaveProperty("prePromptMessageCount"); recorder.markRuntimePersisted(currentMessage, currentAdmission); const queued = database.db @@ -445,7 +396,6 @@ describe("context-engine turn outbox", () => { database, engineId: "test", isHeartbeat: false, - turnAdvancementIdempotency: "atomic-idempotent-v1", }); const warn = vi.fn(); diff --git a/src/agents/harness/context-engine-turn-outbox.ts b/src/agents/harness/context-engine-turn-outbox.ts index 4cfb2825db4f..904e357a6396 100644 --- a/src/agents/harness/context-engine-turn-outbox.ts +++ b/src/agents/harness/context-engine-turn-outbox.ts @@ -6,10 +6,7 @@ import { type TranscriptTurnBoundary, } from "../../config/sessions/session-accessor.js"; import type { TranscriptTurnAdmission } from "../../config/sessions/transcript-entry-anchor.js"; -import type { - ContextEngine, - ContextEngineTurnAdvancementIdempotency, -} from "../../context-engine/types.js"; +import type { ContextEngine } from "../../context-engine/types.js"; import { executeSqliteQuerySync, executeSqliteQueryTakeFirstSync, @@ -40,16 +37,13 @@ type AcceptedContextEngineTurnOutboxPayload = Readonly<{ boundary: TranscriptTurnBoundary; isHeartbeat: boolean; state: "accepted"; - turnAdvancementIdempotency?: ContextEngineTurnAdvancementIdempotency; }>; type ReadyContextEngineTurnOutboxPayload = Readonly<{ boundary: TranscriptTurnBoundary; isHeartbeat: boolean; messages: AgentMessage[]; - prePromptMessageCount?: number; state: "ready"; - turnAdvancementIdempotency?: ContextEngineTurnAdvancementIdempotency; }>; type ContextEngineTurnReadFailureKind = Exclude< @@ -201,7 +195,6 @@ export function acceptContextEngineTurnIntent(params: { engineId: string; isHeartbeat: boolean; ownerPluginId?: string; - turnAdvancementIdempotency: ContextEngineTurnAdvancementIdempotency; }): void { writeContextEngineTurnOutboxPayload({ ...params, @@ -209,7 +202,6 @@ export function acceptContextEngineTurnIntent(params: { boundary: params.boundary, isHeartbeat: params.isHeartbeat, state: "accepted", - turnAdvancementIdempotency: params.turnAdvancementIdempotency, }, }); } @@ -306,10 +298,6 @@ export function recoverContextEngineTurnOutbox(params: { boundary: payload.boundary, maxEvents: RECOVERED_TURN_MAX_EVENTS, maxBytes: RECOVERED_TURN_MAX_BYTES, - messageRange: - payload.turnAdvancementIdempotency === "atomic-idempotent-turn-local-v1" - ? "turn-local-v1" - : "full-transcript-v1", }); if (closedTurn.kind !== "ok") { if (isRetryableContextEngineTurnReadFailure(closedTurn.kind)) { @@ -339,11 +327,6 @@ export function recoverContextEngineTurnOutbox(params: { boundary: payload.boundary, isHeartbeat: payload.isHeartbeat, messages: closedTurn.messages, - prePromptMessageCount: - payload.turnAdvancementIdempotency === "atomic-idempotent-turn-local-v1" - ? undefined - : closedTurn.prePromptMessageCount, - turnAdvancementIdempotency: payload.turnAdvancementIdempotency, }, }); } @@ -358,10 +341,7 @@ export async function drainContextEngineTurnOutbox(params: { limit?: number; warn: (message: string) => void; }): Promise<{ pending: boolean }> { - if ( - typeof params.engine.commitTurn !== "function" && - typeof params.engine.commitTurnLocal !== "function" - ) { + if (typeof params.engine.commitTurn !== "function") { return { pending: false }; } let remaining = Math.max(0, params.limit ?? 16); @@ -463,17 +443,9 @@ async function commitPendingContextEngineTurn( }, isHeartbeat: payload.isHeartbeat, }; - const turnLocal = payload.turnAdvancementIdempotency === "atomic-idempotent-turn-local-v1"; - const result = turnLocal - ? await params.engine.commitTurnLocal?.(commonParams) - : await params.engine.commitTurn?.({ - ...commonParams, - prePromptMessageCount: payload.prePromptMessageCount ?? 0, - }); + const result = await params.engine.commitTurn?.(commonParams); if (!result) { - throw new Error( - `context engine does not implement ${turnLocal ? "commitTurnLocal" : "commitTurn"}`, - ); + throw new Error("context engine does not implement commitTurn"); } if (result.status !== "committed" && result.status !== "duplicate") { throw new Error(`invalid commitTurn result status: ${String(result.status)}`); diff --git a/src/config/sessions/session-accessor.transcript-range.ts b/src/config/sessions/session-accessor.transcript-range.ts index 20d249d04ccc..9a506a985cec 100644 --- a/src/config/sessions/session-accessor.transcript-range.ts +++ b/src/config/sessions/session-accessor.transcript-range.ts @@ -17,7 +17,6 @@ export type ClosedTranscriptTurnReadResult = | { kind: "ok"; messages: AgentMessage[]; - prePromptMessageCount: number; } | { kind: "non-descendant" | "projection-unavailable" | "session-rebound" | "stale" | "too-large"; @@ -99,7 +98,6 @@ export function readClosedTranscriptTurn(params: { boundary: TranscriptTurnBoundary; maxEvents: number; maxBytes: number; - messageRange: "full-transcript-v1" | "turn-local-v1"; }): ClosedTranscriptTurnReadResult { if (!anchorsShareTarget(params.boundary)) { return { kind: "session-rebound" }; @@ -219,13 +217,7 @@ export function readClosedTranscriptTurn(params: { .select("event.event_json") .where("active.session_id", "=", target.sessionId) .where("active.message_position", "is not", null) - .where( - "active.message_position", - ">=", - params.messageRange === "turn-local-v1" - ? params.boundary.admission.activeMessagePosition - : 0, - ) + .where("active.message_position", ">=", params.boundary.admission.activeMessagePosition) .where("active.message_position", "<=", params.boundary.terminal.activeMessagePosition) .orderBy("active.message_position", "asc") // Read one sentinel row so an oversized turn is rejected without @@ -246,10 +238,6 @@ export function readClosedTranscriptTurn(params: { return { kind: "ok", messages, - prePromptMessageCount: - params.messageRange === "turn-local-v1" - ? 0 - : params.boundary.admission.activeMessagePosition, } as const; }, { diff --git a/src/context-engine/host-compat.ts b/src/context-engine/host-compat.ts index 84ee4849923a..80f59ce913e9 100644 --- a/src/context-engine/host-compat.ts +++ b/src/context-engine/host-compat.ts @@ -15,12 +15,10 @@ export type ContextEngineHostSupport = { /** Return whether an engine implements the durable method matching its declared contract. */ export function supportsContextEngineDurableTurnAdvancement(engine: ContextEngine): boolean { - const contract = engine.info.transcriptSemantics?.turnAdvancementIdempotency; - return contract === "atomic-idempotent-v1" - ? typeof engine.commitTurn === "function" - : contract === "atomic-idempotent-turn-local-v1" && - typeof engine.commitTurn === "function" && - typeof engine.commitTurnLocal === "function"; + return ( + engine.info.transcriptSemantics?.turnAdvancementIdempotency === "atomic-idempotent-v1" && + typeof engine.commitTurn === "function" + ); } const GENERIC_CLI_CONTEXT_ENGINE_HOST_CAPABILITIES = [ diff --git a/src/context-engine/host-param-projection.test.ts b/src/context-engine/host-param-projection.test.ts index ff4406615165..2945e43a87a6 100644 --- a/src/context-engine/host-param-projection.test.ts +++ b/src/context-engine/host-param-projection.test.ts @@ -211,7 +211,6 @@ describe("context-engine host parameter projection", () => { activeMessagePosition: 1, }, messages: [message], - prePromptMessageCount: 1, sessionId: "session-1", sessionKey: "agent:main:session-1", sessionTarget: { agentId: "main", sessionId: "session-1" }, diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index 7c74d98e1648..b499974bc010 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -42,7 +42,7 @@ type RegisterContextEngineForOwnerOptions = { type GuardedContextEngineMethodName = Exclude; const GUARDED_CONTEXT_ENGINE_METHODS = new Set( - "bootstrap maintain ingest ingestBatch afterTurn commitTurn commitTurnLocal assemble compact prepareSubagentSpawn onSubagentEnded".split( + "bootstrap maintain ingest ingestBatch afterTurn commitTurn assemble compact prepareSubagentSpawn onSubagentEnded".split( " ", ), ); diff --git a/src/context-engine/types.ts b/src/context-engine/types.ts index b86ece0daf1d..d345d7b47473 100644 --- a/src/context-engine/types.ts +++ b/src/context-engine/types.ts @@ -174,10 +174,6 @@ export type BootstrapResult = { reason?: string; }; -export type ContextEngineTurnAdvancementIdempotency = - | "atomic-idempotent-v1" - | "atomic-idempotent-turn-local-v1"; - export type ContextEngineInfo = { id: string; name: string; @@ -185,7 +181,7 @@ export type ContextEngineInfo = { acceptedHostParams?: string[]; transcriptSemantics?: { currentTurnFence?: "before-current-turn-entry-v1"; - turnAdvancementIdempotency?: ContextEngineTurnAdvancementIdempotency; + turnAdvancementIdempotency?: "atomic-idempotent-v1"; }; /** True when the engine manages its own compaction lifecycle. */ ownsCompaction?: boolean; @@ -434,29 +430,10 @@ export interface ContextEngine { /** * Atomically and idempotently commit one accepted durable transcript turn. - * V1 messages contain transcript history through the accepted terminal entry. + * Messages span the admitted user entry through the accepted terminal entry. * Hosts may retry the same advancement key after process or plugin failure. */ commitTurn?(params: { - advancementKey: string; - admission: import("../config/sessions/transcript-entry-anchor.js").TranscriptTurnAdmission; - terminal: import("../config/sessions/transcript-entry-anchor.js").TranscriptEntryAnchor; - messages: AgentMessage[]; - /** Number of messages that precede the admitted user entry. */ - prePromptMessageCount: number; - sessionId: string; - sessionKey?: string; - sessionTarget?: ContextEngineSessionTarget; - runtimeSettings?: ContextEngineRuntimeSettings; - runtimeContext?: ContextEngineRuntimeContext; - isHeartbeat?: boolean; - }): Promise<{ status: "committed" | "duplicate" }>; - - /** - * Atomically and idempotently commit one accepted turn-local transcript range. - * Messages span the admitted user entry through the accepted terminal entry. - */ - commitTurnLocal?(params: { advancementKey: string; admission: import("../config/sessions/transcript-entry-anchor.js").TranscriptTurnAdmission; terminal: import("../config/sessions/transcript-entry-anchor.js").TranscriptEntryAnchor;