Skip to content

Commit 7f1d82a

Browse files
committed
revert(sessions): defer session metadata sqlite
Reverts 538d36e while preserving subsequent main changes. The beta-only SQLite downgrade rescue and reverse migration remain excluded.
1 parent 9408380 commit 7f1d82a

152 files changed

Lines changed: 3995 additions & 5209 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/plugins/sdk-subpaths.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ usage endpoint failed or returned no usable usage data.
246246
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only |
247247
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
248248
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
249-
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), target discovery, legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
249+
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
250250
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
251251
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
252252
| `plugin-sdk/plugin-state-runtime` | Plugin sidecar SQLite keyed-state types |

extensions/codex/src/app-server/startup-binding.test.ts

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import fs from "node:fs/promises";
33
import os from "node:os";
44
import path from "node:path";
5-
import { saveSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
65
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
76
import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
87
import { rotateOversizedCodexAppServerStartupBinding } from "./startup-binding.js";
@@ -35,17 +34,14 @@ describe("Codex app-server startup binding", () => {
3534

3635
async function writeSessionRecord(sessionFile: string, record: Record<string, unknown>) {
3736
await fs.mkdir(path.dirname(sessionFile), { recursive: true });
38-
await saveSessionStore(
37+
await fs.writeFile(
3938
path.join(path.dirname(sessionFile), "sessions.json"),
40-
{
39+
JSON.stringify({
4140
"agent:main:session-1": {
42-
sessionId: "session-1",
4341
sessionFile,
44-
updatedAt: Date.now(),
4542
...record,
4643
},
47-
},
48-
{ skipMaintenance: true },
44+
}),
4945
);
5046
}
5147

@@ -82,33 +78,29 @@ describe("Codex app-server startup binding", () => {
8278
expect(savedBinding?.threadId).toBe("thread-existing");
8379
});
8480

85-
it("reads updated SQLite-backed session records between startup checks", async () => {
81+
it("reuses the session record cache while sessions.json is unchanged", async () => {
8682
const sessionFile = path.join(tempDir, "session.jsonl");
8783
const workspaceDir = path.join(tempDir, "workspace");
8884
const agentDir = path.join(tempDir, "agent");
8985
await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" });
9086
await writeSessionRecord(sessionFile, { totalTokens: 12_000 });
91-
92-
const firstBinding = await rotateOversizedCodexAppServerStartupBinding({
93-
binding: await readCodexAppServerBinding(sessionFile),
94-
sessionFile,
95-
agentDir,
96-
config: undefined,
97-
});
98-
expect(firstBinding?.threadId).toBe("thread-existing");
99-
100-
await writeSessionRecord(sessionFile, { totalTokens: 400_000 });
101-
102-
const secondBinding = await rotateOversizedCodexAppServerStartupBinding({
103-
binding: await readCodexAppServerBinding(sessionFile),
104-
sessionFile,
105-
agentDir,
106-
config: undefined,
107-
});
108-
109-
expect(secondBinding).toBeUndefined();
110-
const savedBinding = await readCodexAppServerBinding(sessionFile);
111-
expect(savedBinding).toBeUndefined();
87+
const sessionsJson = path.join(path.dirname(sessionFile), "sessions.json");
88+
const readFileSpy = vi.spyOn(fs, "readFile");
89+
90+
for (let i = 0; i < 2; i += 1) {
91+
const binding = await rotateOversizedCodexAppServerStartupBinding({
92+
binding: await readCodexAppServerBinding(sessionFile),
93+
sessionFile,
94+
agentDir,
95+
config: undefined,
96+
});
97+
expect(binding?.threadId).toBe("thread-existing");
98+
}
99+
100+
const sessionStoreReads = readFileSpy.mock.calls.filter(
101+
([file]) => typeof file === "string" && file === sessionsJson,
102+
);
103+
expect(sessionStoreReads).toHaveLength(1);
112104
});
113105

114106
it("checks native rollout token pressure under default compaction config", async () => {

extensions/codex/src/app-server/startup-binding.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
embeddedAgentLog,
1010
type EmbeddedRunAttemptParams,
1111
} from "openclaw/plugin-sdk/agent-harness-runtime";
12-
import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
1312
import { resolveCodexAppServerHomeDir } from "./auth-bridge.js";
1413
import { isJsonObject, type JsonValue } from "./protocol.js";
1514
import { clearCodexAppServerBinding, type CodexAppServerThreadBinding } from "./session-binding.js";
@@ -35,6 +34,15 @@ const CODEX_APP_SERVER_BYTE_UNITS: Record<string, number> = {
3534
tb: 1024 * 1024 * 1024 * 1024,
3635
tib: 1024 * 1024 * 1024 * 1024,
3736
};
37+
type CodexSessionRecordCacheEntry = {
38+
sessionsFile: string;
39+
mtimeMs: number;
40+
size: number;
41+
record: (Record<string, unknown> & { sessionKey: string }) | undefined;
42+
};
43+
44+
const codexSessionRecordCache = new Map<string, CodexSessionRecordCacheEntry>();
45+
3846
function parseCodexAppServerByteLimit(value: unknown): number | undefined {
3947
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
4048
return Math.floor(value);
@@ -115,24 +123,52 @@ async function listCodexAppServerRolloutFilesForThread(
115123
async function readCodexSessionRecordForSessionFile(
116124
sessionFile: string,
117125
): Promise<(Record<string, unknown> & { sessionKey: string }) | undefined> {
118-
const storePath = path.join(path.dirname(sessionFile), "sessions.json");
126+
const sessionsFile = path.join(path.dirname(sessionFile), "sessions.json");
119127
const resolvedSessionFile = path.resolve(sessionFile);
120-
let store: Record<string, unknown>;
128+
let stat: Awaited<ReturnType<typeof fs.stat>>;
121129
try {
122-
store = loadSessionStore(storePath, { skipCache: true }) as Record<string, unknown>;
130+
stat = await fs.stat(sessionsFile);
123131
} catch {
132+
codexSessionRecordCache.delete(resolvedSessionFile);
124133
return undefined;
125134
}
135+
const cached = codexSessionRecordCache.get(resolvedSessionFile);
136+
if (
137+
cached?.sessionsFile === sessionsFile &&
138+
cached.mtimeMs === stat.mtimeMs &&
139+
cached.size === stat.size
140+
) {
141+
return cached.record;
142+
}
143+
let store: JsonValue | undefined;
144+
try {
145+
store = JSON.parse(await fs.readFile(sessionsFile, "utf8")) as JsonValue;
146+
} catch {
147+
codexSessionRecordCache.delete(resolvedSessionFile);
148+
return undefined;
149+
}
150+
if (!isJsonObject(store)) {
151+
codexSessionRecordCache.delete(resolvedSessionFile);
152+
return undefined;
153+
}
154+
let found: (Record<string, unknown> & { sessionKey: string }) | undefined;
126155
for (const [sessionKey, record] of Object.entries(store)) {
127156
if (!isJsonObject(record) || typeof record.sessionFile !== "string") {
128157
continue;
129158
}
130159
if (path.resolve(record.sessionFile) !== resolvedSessionFile) {
131160
continue;
132161
}
133-
return { sessionKey, ...record };
162+
found = { sessionKey, ...record };
163+
break;
134164
}
135-
return undefined;
165+
codexSessionRecordCache.set(resolvedSessionFile, {
166+
sessionsFile,
167+
mtimeMs: stat.mtimeMs,
168+
size: stat.size,
169+
record: found,
170+
});
171+
return found;
136172
}
137173

138174
type CodexAppServerRolloutTokenSnapshot = {

extensions/feishu/src/doctor.test.ts

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
5-
import {
6-
loadSessionStore,
7-
saveSessionStore,
8-
type SessionEntry,
9-
} from "openclaw/plugin-sdk/session-store-runtime";
5+
import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
106
import { afterEach, beforeEach, describe, expect, it } from "vitest";
117
import type { OpenClawConfig } from "../runtime-api.js";
128
import { isFeishuSessionStoreKey, runFeishuDoctorSequence } from "./doctor.js";
@@ -63,13 +59,10 @@ function storePath(agentId = "main"): string {
6359
return path.join(sessionsDir(agentId), "sessions.json");
6460
}
6561

66-
async function writeStore(
67-
entries: Record<string, SessionEntry>,
68-
agentId = "main",
69-
): Promise<string> {
62+
function writeStore(entries: Record<string, unknown>, agentId = "main"): string {
7063
const target = storePath(agentId);
7164
fs.mkdirSync(path.dirname(target), { recursive: true });
72-
await saveSessionStore(target, entries, { skipMaintenance: true });
65+
fs.writeFileSync(target, JSON.stringify(entries, null, 2));
7366
return target;
7467
}
7568

@@ -138,7 +131,7 @@ describe("Feishu doctor state repair", () => {
138131
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), JSON.stringify({ msg1: 1 }));
139132

140133
writeTranscript("sess-ok", [sessionHeader("sess-ok"), userMessage("hello")]);
141-
await writeStore({
134+
writeStore({
142135
"agent:main:feishu:direct:ou_user": {
143136
sessionId: "sess-ok",
144137
sessionFile: "sess-ok.jsonl",
@@ -162,16 +155,15 @@ describe("Feishu doctor state repair", () => {
162155
]);
163156
const customStorePath = path.join(stateDir(), "custom-sessions", "sessions.json");
164157
fs.mkdirSync(path.dirname(customStorePath), { recursive: true });
165-
await saveSessionStore(
158+
fs.writeFileSync(
166159
customStorePath,
167-
{
160+
JSON.stringify({
168161
"agent:main:feishu:direct:ou_user": {
169162
sessionId: "sess-abs",
170163
sessionFile: transcriptPath,
171164
updatedAt: Date.now(),
172165
},
173-
},
174-
{ skipMaintenance: true },
166+
}),
175167
);
176168

177169
const result = await runFeishuDoctorSequence({
@@ -195,7 +187,7 @@ describe("Feishu doctor state repair", () => {
195187
userMessage("world"),
196188
userMessage(""),
197189
]);
198-
await writeStore({
190+
writeStore({
199191
"agent:main:feishu:direct:ou_user": {
200192
sessionId: "sess-separated-blanks",
201193
sessionFile: "sess-separated-blanks.jsonl",
@@ -238,7 +230,7 @@ describe("Feishu doctor state repair", () => {
238230
sessionHeader("sess-ok"),
239231
userMessage("hello"),
240232
]);
241-
const targetStorePath = await writeStore({
233+
const targetStorePath = writeStore({
242234
"agent:main:feishu:direct:ou_user": {
243235
sessionId: "sess-ok",
244236
sessionFile: "sess-ok.jsonl",
@@ -294,7 +286,7 @@ describe("Feishu doctor state repair", () => {
294286
userMessage(""),
295287
]);
296288

297-
const targetStorePath = await writeStore({
289+
const targetStorePath = writeStore({
298290
"agent:main:feishu:direct:ou_user": {
299291
sessionId: "sess-bad",
300292
sessionFile: "sess-bad.jsonl",
@@ -353,46 +345,14 @@ describe("Feishu doctor state repair", () => {
353345
).toBe(true);
354346
});
355347

356-
it("archives unhealthy Feishu sessions from SQLite-only retired agent stores", async () => {
357-
const retiredAgent = "retired";
358-
const transcriptPath = writeTranscript(
359-
"sess-retired-bad",
360-
[sessionHeader("sess-retired-bad"), userMessage(""), userMessage(""), userMessage("")],
361-
retiredAgent,
362-
);
363-
const targetStorePath = storePath(retiredAgent);
364-
const entries: Record<string, SessionEntry> = {
365-
"agent:retired:feishu:direct:ou_user": {
366-
sessionId: "sess-retired-bad",
367-
sessionFile: "sess-retired-bad.jsonl",
368-
updatedAt: Date.now(),
369-
},
370-
};
371-
await saveSessionStore(targetStorePath, entries, { skipMaintenance: true });
372-
expect(fs.existsSync(targetStorePath)).toBe(false);
373-
374-
const result = await runFeishuDoctorSequence({
375-
cfg: feishuConfig(),
376-
env: process.env,
377-
shouldRepair: true,
378-
});
379-
380-
expect(result.warningNotes).toEqual([]);
381-
expect(result.changeNotes.join("\n")).toContain("Removed 1 Feishu-scoped session entry");
382-
383-
const store = loadSessionStore(targetStorePath, { skipCache: true });
384-
expect(store["agent:retired:feishu:direct:ou_user"]).toBeUndefined();
385-
expect(fs.existsSync(transcriptPath)).toBe(false);
386-
});
387-
388348
it("archives unhealthy default-scope sessions when metadata identifies Feishu", async () => {
389349
const transcriptPath = writeTranscript("sess-default-feishu-bad", [
390350
sessionHeader("sess-default-feishu-bad"),
391351
userMessage(""),
392352
userMessage(""),
393353
userMessage(""),
394354
]);
395-
const targetStorePath = await writeStore({
355+
const targetStorePath = writeStore({
396356
"agent:main:main": {
397357
sessionId: "sess-default-feishu-bad",
398358
sessionFile: "sess-default-feishu-bad.jsonl",

0 commit comments

Comments
 (0)