Skip to content

Commit 9583e28

Browse files
committed
fix(sessions): retire stale direct dm rows after dmscope changes
1 parent 58c7064 commit 9583e28

13 files changed

Lines changed: 283 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Docs: https://docs.openclaw.ai
102102
- Gateway/HTTP: avoid loading managed outgoing-image media handlers for unrelated requests, so disabled OpenAI-compatible routes return 404 without waiting on lazy media sidecars. Thanks @vincentkoc.
103103
- Gateway/OpenAI-compatible: send the assistant role SSE chunk as soon as streaming chat-completion headers are accepted, so cold agent setup cannot leave `/v1/chat/completions` clients with a bodyless 200 response until their idle timeout fires.
104104
- Agents/media: avoid direct generated-media completion fallback while the announce-agent run is still pending, so async video and music completions do not duplicate raw media messages. (#77754)
105+
- Sessions cleanup: add `openclaw sessions cleanup --fix-dm-scope` so operators who return `session.dmScope` to `main` can dry-run and retire stale direct-DM session rows while preserving transcripts as deleted archives. Fixes #47561 and #45554. Thanks @BunsDev.
105106
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
106107
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd.
107108
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd.

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,19 +1568,22 @@ public struct SessionsCleanupParams: Codable, Sendable {
15681568
public let enforce: Bool?
15691569
public let activekey: String?
15701570
public let fixmissing: Bool?
1571+
public let fixdmscope: Bool?
15711572

15721573
public init(
15731574
agent: String?,
15741575
allagents: Bool?,
15751576
enforce: Bool?,
15761577
activekey: String?,
1577-
fixmissing: Bool?)
1578+
fixmissing: Bool?,
1579+
fixdmscope: Bool?)
15781580
{
15791581
self.agent = agent
15801582
self.allagents = allagents
15811583
self.enforce = enforce
15821584
self.activekey = activekey
15831585
self.fixmissing = fixmissing
1586+
self.fixdmscope = fixdmscope
15841587
}
15851588

15861589
private enum CodingKeys: String, CodingKey {
@@ -1589,6 +1592,7 @@ public struct SessionsCleanupParams: Codable, Sendable {
15891592
case enforce
15901593
case activekey = "activeKey"
15911594
case fixmissing = "fixMissing"
1595+
case fixdmscope = "fixDmScope"
15921596
}
15931597
}
15941598

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,19 +1568,22 @@ public struct SessionsCleanupParams: Codable, Sendable {
15681568
public let enforce: Bool?
15691569
public let activekey: String?
15701570
public let fixmissing: Bool?
1571+
public let fixdmscope: Bool?
15711572

15721573
public init(
15731574
agent: String?,
15741575
allagents: Bool?,
15751576
enforce: Bool?,
15761577
activekey: String?,
1577-
fixmissing: Bool?)
1578+
fixmissing: Bool?,
1579+
fixdmscope: Bool?)
15781580
{
15791581
self.agent = agent
15801582
self.allagents = allagents
15811583
self.enforce = enforce
15821584
self.activekey = activekey
15831585
self.fixmissing = fixmissing
1586+
self.fixdmscope = fixdmscope
15841587
}
15851588

15861589
private enum CodingKeys: String, CodingKey {
@@ -1589,6 +1592,7 @@ public struct SessionsCleanupParams: Codable, Sendable {
15891592
case enforce
15901593
case activekey = "activeKey"
15911594
case fixmissing = "fixMissing"
1595+
case fixdmscope = "fixDmScope"
15921596
}
15931597
}
15941598

docs/cli/sessions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ openclaw sessions cleanup --agent work --dry-run
9393
openclaw sessions cleanup --all-agents --dry-run
9494
openclaw sessions cleanup --enforce
9595
openclaw sessions cleanup --enforce --active-key "agent:main:telegram:direct:123"
96+
openclaw sessions cleanup --dry-run --fix-dm-scope
9697
openclaw sessions cleanup --json
9798
```
9899

@@ -105,6 +106,7 @@ openclaw sessions cleanup --json
105106
- In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) so you can see what would be kept vs removed.
106107
- `--enforce`: apply maintenance even when `session.maintenance.mode` is `warn`.
107108
- `--fix-missing`: remove entries whose transcript files are missing, even if they would not normally age/count out yet.
109+
- `--fix-dm-scope`: when `session.dmScope` is `main`, retire stale peer-keyed direct-DM rows left behind by earlier `per-peer`, `per-channel-peer`, or `per-account-channel-peer` routing. Use `--dry-run` first; applying the cleanup removes those rows from `sessions.json` and preserves their transcripts as deleted archives.
108110
- `--active-key <key>`: protect a specific active key from disk-budget eviction. Durable external conversation pointers, such as group sessions and thread-scoped chat sessions, are also kept by age/count/disk-budget maintenance.
109111
- `--agent <id>`: run cleanup for one configured agent store.
110112
- `--all-agents`: run cleanup for all configured agent stores.
@@ -128,6 +130,8 @@ traffic. Use `--store <path>` for explicit offline repair of a store file.
128130
"storePath": "/home/user/.openclaw/agents/main/sessions/sessions.json",
129131
"beforeCount": 120,
130132
"afterCount": 80,
133+
"missing": 0,
134+
"dmScopeRetired": 0,
131135
"pruned": 40,
132136
"capped": 0
133137
},
@@ -136,6 +140,8 @@ traffic. Use `--store <path>` for explicit offline repair of a store file.
136140
"storePath": "/home/user/.openclaw/agents/work/sessions/sessions.json",
137141
"beforeCount": 18,
138142
"afterCount": 18,
143+
"missing": 0,
144+
"dmScopeRetired": 0,
139145
"pruned": 0,
140146
"capped": 0
141147
}

docs/concepts/session.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ Maintenance preserves durable external conversation pointers, including group
131131
sessions and thread-scoped chat sessions, while still allowing synthetic cron,
132132
hook, heartbeat, ACP, and sub-agent entries to age out.
133133

134+
If you previously used direct-message isolation and later returned
135+
`session.dmScope` to `main`, preview stale peer-keyed DM rows with
136+
`openclaw sessions cleanup --dry-run --fix-dm-scope`. Applying the same flag
137+
retires those old direct-DM rows and keeps their transcripts as deleted
138+
archives.
139+
134140
Preview with `openclaw sessions cleanup --dry-run`.
135141

136142
## Inspecting sessions

src/cli/program/register.status-health-sessions.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ describe("registerStatusHealthSessionsCommands", () => {
239239
"--dry-run",
240240
"--enforce",
241241
"--fix-missing",
242+
"--fix-dm-scope",
242243
"--active-key",
243244
"agent:main:main",
244245
"--json",
@@ -252,6 +253,7 @@ describe("registerStatusHealthSessionsCommands", () => {
252253
dryRun: true,
253254
enforce: true,
254255
fixMissing: true,
256+
fixDmScope: true,
255257
activeKey: "agent:main:main",
256258
json: true,
257259
}),

src/cli/program/register.status-health-sessions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ export function registerStatusHealthSessionsCommands(program: Command) {
182182
"Remove store entries whose transcript files are missing (bypasses age/count retention)",
183183
false,
184184
)
185+
.option(
186+
"--fix-dm-scope",
187+
"Retire stale direct-DM session rows that no longer match session.dmScope=main",
188+
false,
189+
)
185190
.option("--active-key <key>", "Protect this session key from budget-eviction")
186191
.option("--json", "Output JSON", false)
187192
.addHelpText(
@@ -193,6 +198,10 @@ export function registerStatusHealthSessionsCommands(program: Command) {
193198
"openclaw sessions cleanup --dry-run --fix-missing",
194199
"Also preview pruning entries with missing transcript files.",
195200
],
201+
[
202+
"openclaw sessions cleanup --dry-run --fix-dm-scope",
203+
"Preview stale direct-DM rows after returning dmScope to main.",
204+
],
196205
["openclaw sessions cleanup --enforce", "Apply maintenance now."],
197206
["openclaw sessions cleanup --agent work --dry-run", "Preview one agent store."],
198207
["openclaw sessions cleanup --all-agents --dry-run", "Preview all agent stores."],
@@ -220,6 +229,7 @@ export function registerStatusHealthSessionsCommands(program: Command) {
220229
dryRun: Boolean(opts.dryRun),
221230
enforce: Boolean(opts.enforce),
222231
fixMissing: Boolean(opts.fixMissing),
232+
fixDmScope: Boolean(opts.fixDmScope),
223233
activeKey: opts.activeKey as string | undefined,
224234
json: Boolean(opts.json || parentOpts?.json),
225235
},

src/commands/sessions-cleanup.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ describe("sessionsCleanupCommand", () => {
119119
staleKeys: Set<string>;
120120
cappedKeys: Set<string>;
121121
budgetEvictedKeys: Set<string>;
122+
dmScopeRetiredKeys: Set<string>;
122123
}) => {
124+
if (params.dmScopeRetiredKeys.has(params.key)) {
125+
return "retire-dm-scope";
126+
}
123127
if (params.missingKeys.has(params.key)) {
124128
return "prune-missing";
125129
}
@@ -181,6 +185,7 @@ describe("sessionsCleanupCommand", () => {
181185
beforeCount: 3,
182186
afterCount: 1,
183187
missing: 0,
188+
dmScopeRetired: 0,
184189
pruned: 0,
185190
capped: 2,
186191
diskBudget: {
@@ -245,6 +250,7 @@ describe("sessionsCleanupCommand", () => {
245250
beforeCount: 3,
246251
afterCount: 1,
247252
missing: 0,
253+
dmScopeRetired: 0,
248254
pruned: 2,
249255
capped: 0,
250256
diskBudget: null,
@@ -286,6 +292,7 @@ describe("sessionsCleanupCommand", () => {
286292
beforeCount: 2,
287293
afterCount: 1,
288294
missing: 0,
295+
dmScopeRetired: 0,
289296
pruned: 1,
290297
capped: 0,
291298
diskBudget: {
@@ -305,6 +312,7 @@ describe("sessionsCleanupCommand", () => {
305312
staleKeys: new Set<string>(),
306313
cappedKeys: new Set<string>(),
307314
budgetEvictedKeys: new Set<string>(),
315+
dmScopeRetiredKeys: new Set<string>(),
308316
},
309317
],
310318
appliedSummaries: [],
@@ -347,6 +355,7 @@ describe("sessionsCleanupCommand", () => {
347355
beforeCount: 1,
348356
afterCount: 0,
349357
missing: 1,
358+
dmScopeRetired: 0,
350359
pruned: 0,
351360
capped: 0,
352361
diskBudget: null,
@@ -357,6 +366,7 @@ describe("sessionsCleanupCommand", () => {
357366
staleKeys: new Set<string>(),
358367
cappedKeys: new Set<string>(),
359368
budgetEvictedKeys: new Set<string>(),
369+
dmScopeRetiredKeys: new Set<string>(),
360370
},
361371
],
362372
appliedSummaries: [],
@@ -393,6 +403,7 @@ describe("sessionsCleanupCommand", () => {
393403
beforeCount: 2,
394404
afterCount: 1,
395405
missing: 0,
406+
dmScopeRetired: 0,
396407
pruned: 1,
397408
capped: 0,
398409
unreferencedArtifacts: {
@@ -412,6 +423,7 @@ describe("sessionsCleanupCommand", () => {
412423
staleKeys: new Set(["stale"]),
413424
cappedKeys: new Set<string>(),
414425
budgetEvictedKeys: new Set<string>(),
426+
dmScopeRetiredKeys: new Set<string>(),
415427
},
416428
],
417429
appliedSummaries: [],
@@ -450,6 +462,7 @@ describe("sessionsCleanupCommand", () => {
450462
beforeCount: 1,
451463
afterCount: 0,
452464
missing: 0,
465+
dmScopeRetired: 0,
453466
pruned: 1,
454467
capped: 0,
455468
diskBudget: null,
@@ -460,6 +473,7 @@ describe("sessionsCleanupCommand", () => {
460473
staleKeys: new Set(["stale"]),
461474
cappedKeys: new Set<string>(),
462475
budgetEvictedKeys: new Set<string>(),
476+
dmScopeRetiredKeys: new Set<string>(),
463477
},
464478
{
465479
summary: {
@@ -470,6 +484,7 @@ describe("sessionsCleanupCommand", () => {
470484
beforeCount: 1,
471485
afterCount: 0,
472486
missing: 0,
487+
dmScopeRetired: 0,
473488
pruned: 1,
474489
capped: 0,
475490
diskBudget: null,
@@ -480,6 +495,7 @@ describe("sessionsCleanupCommand", () => {
480495
staleKeys: new Set(["stale"]),
481496
cappedKeys: new Set<string>(),
482497
budgetEvictedKeys: new Set<string>(),
498+
dmScopeRetiredKeys: new Set<string>(),
483499
},
484500
],
485501
appliedSummaries: [],

src/commands/sessions-cleanup.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
toSessionDisplayRows,
2626
} from "./sessions-table.js";
2727

28-
const ACTION_PAD = 12;
28+
const ACTION_PAD = 16;
2929

3030
type SessionCleanupActionRow = ReturnType<typeof toSessionDisplayRows>[number] & {
3131
action: ReturnType<typeof resolveSessionCleanupAction>;
@@ -48,6 +48,9 @@ function formatCleanupActionCell(
4848
if (action === "prune-stale") {
4949
return theme.warn(label);
5050
}
51+
if (action === "retire-dm-scope") {
52+
return theme.warn(label);
53+
}
5154
if (action === "cap-overflow") {
5255
return theme.accentBright(label);
5356
}
@@ -60,6 +63,7 @@ function buildActionRows(params: {
6063
staleKeys: Set<string>;
6164
cappedKeys: Set<string>;
6265
budgetEvictedKeys: Set<string>;
66+
dmScopeRetiredKeys: Set<string>;
6367
}): SessionCleanupActionRow[] {
6468
return toSessionDisplayRows(params.beforeStore).map((row) =>
6569
Object.assign({}, row, {
@@ -69,6 +73,7 @@ function buildActionRows(params: {
6973
staleKeys: params.staleKeys,
7074
cappedKeys: params.cappedKeys,
7175
budgetEvictedKeys: params.budgetEvictedKeys,
76+
dmScopeRetiredKeys: params.dmScopeRetiredKeys,
7277
}),
7378
}),
7479
);
@@ -91,6 +96,7 @@ function renderStoreDryRunPlan(params: {
9196
`Entries: ${params.summary.beforeCount} -> ${params.summary.afterCount} (remove ${params.summary.beforeCount - params.summary.afterCount})`,
9297
);
9398
params.runtime.log(`Would prune missing transcripts: ${params.summary.missing}`);
99+
params.runtime.log(`Would retire stale direct DM sessions: ${params.summary.dmScopeRetired}`);
94100
params.runtime.log(`Would prune stale: ${params.summary.pruned}`);
95101
params.runtime.log(`Would cap overflow: ${params.summary.capped}`);
96102
if (params.summary.unreferencedArtifacts?.scannedFiles) {
@@ -169,6 +175,7 @@ async function maybeRunGatewayCleanup(
169175
enforce: opts.enforce,
170176
activeKey: opts.activeKey,
171177
fixMissing: opts.fixMissing,
178+
fixDmScope: opts.fixDmScope,
172179
},
173180
mode: GATEWAY_CLIENT_MODES.CLI,
174181
clientName: GATEWAY_CLIENT_NAMES.CLI,

0 commit comments

Comments
 (0)