Skip to content

Commit 8f9e42f

Browse files
authored
fix(cli): report honest SQLite store labels and consistent build SHA in help (#111659)
1 parent 1508a58 commit 8f9e42f

12 files changed

Lines changed: 161 additions & 50 deletions

scripts/write-cli-startup-metadata.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,27 +778,35 @@ export async function writeCliStartupMetadata(options?: {
778778
: undefined;
779779
const reusableBrowserHelpText =
780780
reusableExisting &&
781+
bundleIdentity &&
782+
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
781783
reusableExisting.browserHelpSourceSignature === browserHelpSourceSignature &&
782784
typeof reusableExisting.browserHelpText === "string" &&
783785
reusableExisting.browserHelpText.length > 0
784786
? reusableExisting.browserHelpText
785787
: undefined;
786788
const reusableSecretsHelpText =
787789
reusableExisting &&
790+
bundleIdentity &&
791+
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
788792
reusableExisting.secretsHelpSourceSignature === secretsHelpSourceSignature &&
789793
typeof reusableExisting.secretsHelpText === "string" &&
790794
reusableExisting.secretsHelpText.length > 0
791795
? reusableExisting.secretsHelpText
792796
: undefined;
793797
const reusableNodesHelpText =
794798
reusableExisting &&
799+
bundleIdentity &&
800+
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
795801
reusableExisting.nodesHelpSourceSignature === nodesHelpSourceSignature &&
796802
typeof reusableExisting.nodesHelpText === "string" &&
797803
reusableExisting.nodesHelpText.length > 0
798804
? reusableExisting.nodesHelpText
799805
: undefined;
800806
const reusableSubcommandHelpText =
801807
reusableExisting &&
808+
bundleIdentity &&
809+
reusableExisting.rootHelpBundleSignature === bundleIdentity.signature &&
802810
reusableExisting.subcommandHelpSourceSignature === subcommandHelpSourceSignature &&
803811
hasAllPrecomputedSubcommandHelpText(reusableExisting.subcommandHelpText)
804812
? (reusableExisting.subcommandHelpText as PrecomputedSubcommandHelpText)

src/commands/sessions-cleanup.test.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ describe("sessionsCleanupCommand", () => {
227227
expect(logs).toHaveLength(1);
228228
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
229229
agentId: "main",
230-
storePath: "/resolved/sessions.json",
230+
storePath: "/resolved/openclaw-agent.sqlite",
231231
mode: "enforce",
232232
dryRun: false,
233233
beforeCount: 3,
@@ -262,9 +262,10 @@ describe("sessionsCleanupCommand", () => {
262262
});
263263

264264
it("delegates non-store enforcing cleanup through the Gateway writer when reachable", async () => {
265+
const remoteStorePath = "C:\\Users\\gateway\\.openclaw\\agents\\main\\sessions\\sessions.json";
265266
mocks.callGateway.mockResolvedValue({
266267
agentId: "main",
267-
storePath: "/resolved/sessions.json",
268+
storePath: remoteStorePath,
268269
mode: "enforce",
269270
dryRun: false,
270271
beforeCount: 3,
@@ -298,7 +299,7 @@ describe("sessionsCleanupCommand", () => {
298299
expect(logs).toHaveLength(1);
299300
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
300301
agentId: "main",
301-
storePath: "/resolved/sessions.json",
302+
storePath: remoteStorePath,
302303
mode: "enforce",
303304
dryRun: false,
304305
beforeCount: 3,
@@ -315,6 +316,32 @@ describe("sessionsCleanupCommand", () => {
315316
});
316317
});
317318

319+
it("preserves a Gateway-owned store path in human output", async () => {
320+
const remoteStorePath = "C:\\Users\\gateway\\.openclaw\\openclaw-agent.sqlite";
321+
mocks.callGateway.mockResolvedValue({
322+
agentId: "main",
323+
storePath: remoteStorePath,
324+
mode: "enforce",
325+
dryRun: false,
326+
beforeCount: 3,
327+
afterCount: 1,
328+
missing: 0,
329+
dmScopeRetired: 0,
330+
modelRunPruned: 0,
331+
pruned: 2,
332+
capped: 0,
333+
diskBudget: null,
334+
wouldMutate: true,
335+
applied: true,
336+
appliedCount: 1,
337+
});
338+
339+
const { runtime, logs } = makeRuntime();
340+
await sessionsCleanupCommand({ enforce: true }, runtime);
341+
342+
expectLogsToInclude(logs, `Session store: ${remoteStorePath}`);
343+
});
344+
318345
it("returns dry-run JSON without mutating the store", async () => {
319346
mocks.runSessionsCleanup.mockResolvedValue({
320347
mode: "warn",
@@ -368,7 +395,7 @@ describe("sessionsCleanupCommand", () => {
368395
expect(logs).toHaveLength(1);
369396
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
370397
agentId: "main",
371-
storePath: "/resolved/sessions.json",
398+
storePath: "/resolved/openclaw-agent.sqlite",
372399
mode: "warn",
373400
dryRun: true,
374401
beforeCount: 2,
@@ -440,7 +467,7 @@ describe("sessionsCleanupCommand", () => {
440467
expect(logs).toHaveLength(1);
441468
expect(JSON.parse(logs[0] ?? "{}")).toEqual({
442469
agentId: "main",
443-
storePath: "/resolved/sessions.json",
470+
storePath: "/resolved/openclaw-agent.sqlite",
444471
mode: "warn",
445472
dryRun: true,
446473
beforeCount: 1,
@@ -505,6 +532,7 @@ describe("sessionsCleanupCommand", () => {
505532
runtime,
506533
);
507534

535+
expectLogsToInclude(logs, "Session store: /resolved/openclaw-agent.sqlite");
508536
expectLogsToInclude(logs, "Planned session actions:");
509537
expectLogsToInclude(logs, "Would prune unreferenced artifacts: 2");
510538
const tableHeaderLines = logs.filter((line) => line.includes("Action") && line.includes("Key"));
@@ -694,7 +722,7 @@ describe("sessionsCleanupCommand", () => {
694722
stores: [
695723
{
696724
agentId: "main",
697-
storePath: "/resolved/main-sessions.json",
725+
storePath: "/resolved/main-sessions.sqlite",
698726
mode: "warn",
699727
dryRun: true,
700728
beforeCount: 1,
@@ -709,7 +737,7 @@ describe("sessionsCleanupCommand", () => {
709737
},
710738
{
711739
agentId: "work",
712-
storePath: "/resolved/work-sessions.json",
740+
storePath: "/resolved/work-sessions.work.sqlite",
713741
mode: "warn",
714742
dryRun: true,
715743
beforeCount: 1,

src/commands/sessions-cleanup.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type SessionsCleanupOptions,
1616
type SessionsCleanupResult,
1717
} from "../config/sessions.js";
18+
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
1819
import type { OpenClawConfig } from "../config/types.openclaw.js";
1920
import { callGateway, isGatewayTransportError } from "../gateway/call.js";
2021
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
@@ -141,6 +142,15 @@ function renderLabelSummaries(params: {
141142
params.runtime.log(`Total: ${totalKept} kept, ${totalPruned} pruned`);
142143
}
143144

145+
function toDisplayedCleanupSummary(summary: SessionCleanupSummary): SessionCleanupSummary {
146+
return {
147+
...summary,
148+
storePath: resolveSqliteTargetFromSessionStorePath(summary.storePath, {
149+
agentId: summary.agentId,
150+
}).path,
151+
};
152+
}
153+
144154
function renderStoreDryRunPlan(params: {
145155
cfg: OpenClawConfig;
146156
summary: SessionCleanupSummary;
@@ -149,10 +159,11 @@ function renderStoreDryRunPlan(params: {
149159
showAgentHeader: boolean;
150160
}) {
151161
const rich = isRich();
162+
const displaySummary = toDisplayedCleanupSummary(params.summary);
152163
if (params.showAgentHeader) {
153164
params.runtime.log(`Agent: ${params.summary.agentId}`);
154165
}
155-
params.runtime.log(`Session store: ${params.summary.storePath}`);
166+
params.runtime.log(`Session store: ${displaySummary.storePath}`);
156167
params.runtime.log(`Maintenance mode: ${params.summary.mode}`);
157168
params.runtime.log(
158169
`Entries: ${params.summary.beforeCount} -> ${params.summary.afterCount} (remove ${params.summary.beforeCount - params.summary.afterCount})`,
@@ -202,6 +213,7 @@ function renderStoreDryRunPlan(params: {
202213
function renderAppliedSummaries(params: {
203214
summaries: SessionCleanupSummary[];
204215
runtime: RuntimeEnv;
216+
locallyOwned: boolean;
205217
}) {
206218
for (let i = 0; i < params.summaries.length; i += 1) {
207219
const summary = params.summaries[i];
@@ -214,7 +226,10 @@ function renderAppliedSummaries(params: {
214226
if (params.summaries.length > 1) {
215227
params.runtime.log(`Agent: ${summary.agentId}`);
216228
}
217-
params.runtime.log(`Session store: ${summary.storePath}`);
229+
const storePath = params.locallyOwned
230+
? toDisplayedCleanupSummary(summary).storePath
231+
: summary.storePath;
232+
params.runtime.log(`Session store: ${storePath}`);
218233
params.runtime.log(`Applied maintenance. Current entries: ${summary.appliedCount ?? 0}`);
219234
if (summary.unreferencedArtifacts?.removedFiles) {
220235
params.runtime.log(
@@ -226,14 +241,14 @@ function renderAppliedSummaries(params: {
226241

227242
async function maybeRunGatewayCleanup(
228243
opts: SessionsCleanupOptions,
229-
): Promise<SessionsCleanupResult | null> {
244+
): Promise<{ delegated: true; result: SessionsCleanupResult } | { delegated: false }> {
230245
if (opts.store || opts.dryRun) {
231246
// Explicit store paths and dry-runs must stay local; the gateway only owns
232247
// live in-process cleanup for default stores.
233-
return null;
248+
return { delegated: false };
234249
}
235250
try {
236-
return await callGateway<SessionsCleanupResult>({
251+
const result = await callGateway<SessionsCleanupResult>({
237252
method: "sessions.cleanup",
238253
params: {
239254
agent: opts.agent,
@@ -247,27 +262,32 @@ async function maybeRunGatewayCleanup(
247262
clientName: GATEWAY_CLIENT_NAMES.CLI,
248263
requiredMethods: ["sessions.cleanup"],
249264
});
265+
return { delegated: true, result };
250266
} catch (error) {
251267
if (isGatewayTransportError(error)) {
252268
// A stopped gateway should not block local maintenance; fall back to the
253269
// on-disk session stores when transport is unavailable.
254-
return null;
270+
return { delegated: false };
255271
}
256272
throw error;
257273
}
258274
}
259275

260276
/** Runs session cleanup, optionally using the live gateway for active stores. */
261277
export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runtime: RuntimeEnv) {
262-
const gatewayResult = await maybeRunGatewayCleanup(opts);
263-
if (gatewayResult) {
278+
const gatewayCleanup = await maybeRunGatewayCleanup(opts);
279+
if (gatewayCleanup.delegated) {
280+
// The Gateway owns this path. Preserve its syntax because resolving a remote
281+
// Windows path on a POSIX client (or vice versa) would fabricate a local path.
264282
if (opts.json) {
265-
writeRuntimeJson(runtime, gatewayResult);
283+
writeRuntimeJson(runtime, gatewayCleanup.result);
266284
return;
267285
}
268286
renderAppliedSummaries({
269-
summaries: "stores" in gatewayResult ? gatewayResult.stores : [gatewayResult],
287+
summaries:
288+
"stores" in gatewayCleanup.result ? gatewayCleanup.result.stores : [gatewayCleanup.result],
270289
runtime,
290+
locallyOwned: false,
271291
});
272292
return;
273293
}
@@ -298,7 +318,7 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
298318
serializeSessionCleanupResult({
299319
mode,
300320
dryRun: true,
301-
summaries: previewResults.map((result) => result.summary),
321+
summaries: previewResults.map((result) => toDisplayedCleanupSummary(result.summary)),
302322
}),
303323
);
304324
return;
@@ -325,11 +345,11 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
325345
serializeSessionCleanupResult({
326346
mode,
327347
dryRun: false,
328-
summaries: appliedSummaries,
348+
summaries: appliedSummaries.map(toDisplayedCleanupSummary),
329349
}),
330350
);
331351
return;
332352
}
333353

334-
renderAppliedSummaries({ summaries: appliedSummaries, runtime });
354+
renderAppliedSummaries({ summaries: appliedSummaries, runtime, locallyOwned: true });
335355
}

src/commands/sessions.default-agent-store.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,8 @@ describe("sessionsCommand default store agent selection", () => {
156156
expect(payload.count).toBe(2);
157157
expect(payload.allAgents).toBe(true);
158158
expect(payload.stores).toEqual([
159-
{ agentId: "main", path: "/tmp/shared-sessions.json" },
160-
{ agentId: "voice", path: "/tmp/shared-sessions.json" },
159+
{ agentId: "main", path: "/tmp/shared-sessions.sqlite" },
160+
{ agentId: "voice", path: "/tmp/shared-sessions.voice.sqlite" },
161161
]);
162162
expect(payload.sessions?.map((session) => session.agentId).toSorted()).toEqual([
163163
"main",
@@ -177,7 +177,7 @@ describe("sessionsCommand default store agent selection", () => {
177177
agentId: "voice",
178178
storePath: "/tmp/sessions-voice.json",
179179
});
180-
expect(logs[0]).toContain("Session store: /tmp/sessions-voice.json");
180+
expect(logs[0]).toContain("Session store: /tmp/sessions-voice.voice.sqlite");
181181
});
182182

183183
it("uses all configured agent stores with --all-agents", async () => {

src/commands/sessions.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,28 @@ describe("sessionsCommand", () => {
182182
expect(group?.totalTokensFresh).toBe(false);
183183
});
184184

185+
it("reports the SQLite database for the store and SQLite-backed sessionFile", async () => {
186+
const store = await writeStore({
187+
main: {
188+
sessionId: "abc123",
189+
sessionFile: "sqlite:main:abc123:/tmp/openclaw/agents/main/sessions/sessions.json",
190+
updatedAt: Date.now() - 10 * 60_000,
191+
model: "test:opus",
192+
},
193+
});
194+
195+
const payload = await runSessionsJson<{
196+
path?: string;
197+
sessions?: Array<{ key: string; sessionFile?: string }>;
198+
}>(sessionsCommand, store);
199+
200+
expect(payload.path).toMatch(/openclaw-agent\.sqlite$/u);
201+
expect(payload.path).not.toContain("sessions.json");
202+
expect(payload.sessions?.find((row) => row.key === "main")?.sessionFile).toBe(
203+
"sqlite:main:abc123:/tmp/openclaw/agents/main/agent/openclaw-agent.sqlite",
204+
);
205+
});
206+
185207
it("exports subagent lineage metadata in JSON output", async () => {
186208
const store = await writeStore({
187209
"agent:child:main": {

src/commands/sessions.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import { normalizeChatType } from "../channels/chat-type.js";
1717
import { getRuntimeConfig } from "../config/config.js";
1818
import { resolveSessionTotalTokens } from "../config/sessions.js";
1919
import { listSessionEntries } from "../config/sessions/session-accessor.js";
20+
import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js";
21+
import {
22+
formatSqliteSessionFileMarker,
23+
parseSqliteSessionFileMarker,
24+
} from "../config/sessions/sqlite-marker.js";
2025
import type { SessionEntry } from "../config/sessions/types.js";
2126
import type { OpenClawConfig } from "../config/types.openclaw.js";
2227
import { resolveStoredSessionKeyForAgentStore } from "../gateway/session-store-key.js";
@@ -229,9 +234,24 @@ function formatRuntimeCell(runtimeLabel: string, rich: boolean): string {
229234
return rich ? theme.info(label) : label;
230235
}
231236

237+
function resolveSessionStoreDisplayPath(target: { agentId: string; storePath: string }): string {
238+
return resolveSqliteTargetFromSessionStorePath(target.storePath, {
239+
agentId: target.agentId,
240+
}).path;
241+
}
242+
232243
function toJsonSessionRow(row: SessionRow): Omit<SessionRow, "runtimeLabel"> {
233244
const { runtimeLabel, ...jsonRow } = row;
234245
void runtimeLabel;
246+
const marker = parseSqliteSessionFileMarker(jsonRow.sessionFile);
247+
if (marker) {
248+
jsonRow.sessionFile = formatSqliteSessionFileMarker({
249+
...marker,
250+
storePath: resolveSqliteTargetFromSessionStorePath(marker.storePath, {
251+
agentId: marker.agentId,
252+
}).path,
253+
});
254+
}
235255
return jsonRow;
236256
}
237257

@@ -429,11 +449,11 @@ export async function sessionsCommand(
429449
const multi = targets.length > 1;
430450
const aggregate = aggregateAgents || multi;
431451
writeRuntimeJson(runtime, {
432-
path: aggregate ? null : (targets[0]?.storePath ?? null),
452+
path: aggregate || !targets[0] ? null : resolveSessionStoreDisplayPath(targets[0]),
433453
stores: aggregate
434454
? targets.map((target) => ({
435455
agentId: target.agentId,
436-
path: target.storePath,
456+
path: resolveSessionStoreDisplayPath(target),
437457
}))
438458
: undefined,
439459
allAgents: aggregateAgents ? true : undefined,
@@ -476,8 +496,9 @@ export async function sessionsCommand(
476496
return;
477497
}
478498

479-
if (targets.length === 1 && !aggregateAgents) {
480-
runtime.log(info(`Session store: ${targets[0]?.storePath}`));
499+
const primaryTarget = targets[0];
500+
if (primaryTarget && targets.length === 1 && !aggregateAgents) {
501+
runtime.log(info(`Session store: ${resolveSessionStoreDisplayPath(primaryTarget)}`));
481502
} else {
482503
runtime.log(
483504
info(`Session stores: ${targets.length} (${targets.map((t) => t.agentId).join(", ")})`),

src/config/sessions/session-sqlite-target.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js
44
/** SQLite database target resolved from a legacy session store path. */
55
type ResolvedSqliteStoreTarget = {
66
agentId?: string;
7-
path?: string;
7+
path: string;
88
};
99

1010
function resolveCustomStoreSqlitePath(params: {

0 commit comments

Comments
 (0)