@martian-engineering/lossless-claw 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +19 -3
  2. package/dist/index.js +19240 -0
  3. package/docs/agent-tools.md +9 -4
  4. package/docs/configuration.md +24 -5
  5. package/openclaw.plugin.json +27 -3
  6. package/package.json +7 -6
  7. package/skills/lossless-claw/SKILL.md +3 -2
  8. package/skills/lossless-claw/references/architecture.md +12 -0
  9. package/skills/lossless-claw/references/config.md +37 -0
  10. package/skills/lossless-claw/references/diagnostics.md +13 -0
  11. package/index.ts +0 -2
  12. package/src/assembler.ts +0 -1188
  13. package/src/compaction.ts +0 -1756
  14. package/src/db/config.ts +0 -345
  15. package/src/db/connection.ts +0 -141
  16. package/src/db/features.ts +0 -42
  17. package/src/db/migration.ts +0 -746
  18. package/src/engine.ts +0 -4306
  19. package/src/expansion-auth.ts +0 -365
  20. package/src/expansion-policy.ts +0 -303
  21. package/src/expansion.ts +0 -383
  22. package/src/integrity.ts +0 -600
  23. package/src/large-files.ts +0 -546
  24. package/src/lcm-log.ts +0 -37
  25. package/src/openclaw-bridge.ts +0 -22
  26. package/src/plugin/index.ts +0 -1960
  27. package/src/plugin/lcm-command.ts +0 -765
  28. package/src/plugin/lcm-doctor-apply.ts +0 -542
  29. package/src/plugin/lcm-doctor-shared.ts +0 -210
  30. package/src/plugin/shared-init.ts +0 -59
  31. package/src/prune.ts +0 -391
  32. package/src/retrieval.ts +0 -363
  33. package/src/session-patterns.ts +0 -23
  34. package/src/startup-banner-log.ts +0 -49
  35. package/src/store/compaction-telemetry-store.ts +0 -156
  36. package/src/store/conversation-store.ts +0 -929
  37. package/src/store/fts5-sanitize.ts +0 -50
  38. package/src/store/full-text-fallback.ts +0 -83
  39. package/src/store/full-text-sort.ts +0 -21
  40. package/src/store/index.ts +0 -39
  41. package/src/store/parse-utc-timestamp.ts +0 -25
  42. package/src/store/summary-store.ts +0 -1519
  43. package/src/summarize.ts +0 -1511
  44. package/src/tools/common.ts +0 -53
  45. package/src/tools/lcm-conversation-scope.ts +0 -127
  46. package/src/tools/lcm-describe-tool.ts +0 -245
  47. package/src/tools/lcm-expand-query-tool.ts +0 -831
  48. package/src/tools/lcm-expand-tool.delegation.ts +0 -580
  49. package/src/tools/lcm-expand-tool.ts +0 -453
  50. package/src/tools/lcm-expansion-recursion-guard.ts +0 -373
  51. package/src/tools/lcm-grep-tool.ts +0 -228
  52. package/src/transaction-mutex.ts +0 -136
  53. package/src/transcript-repair.ts +0 -301
  54. package/src/types.ts +0 -165
package/src/retrieval.ts DELETED
@@ -1,363 +0,0 @@
1
- import type {
2
- ConversationStore,
3
- MessageRecord,
4
- MessageSearchResult,
5
- } from "./store/conversation-store.js";
6
- import type {
7
- SummaryStore,
8
- SummaryRecord,
9
- SummarySearchResult,
10
- LargeFileRecord,
11
- } from "./store/summary-store.js";
12
- import type { SearchSort } from "./store/full-text-sort.js";
13
-
14
- // ── Public interfaces ────────────────────────────────────────────────────────
15
-
16
- export interface DescribeResult {
17
- id: string;
18
- type: "summary" | "file";
19
- /** Summary-specific fields */
20
- summary?: {
21
- conversationId: number;
22
- kind: "leaf" | "condensed";
23
- content: string;
24
- depth: number;
25
- tokenCount: number;
26
- descendantCount: number;
27
- descendantTokenCount: number;
28
- sourceMessageTokenCount: number;
29
- fileIds: string[];
30
- parentIds: string[];
31
- childIds: string[];
32
- messageIds: number[];
33
- earliestAt: Date | null;
34
- latestAt: Date | null;
35
- subtree: Array<{
36
- summaryId: string;
37
- parentSummaryId: string | null;
38
- depthFromRoot: number;
39
- kind: "leaf" | "condensed";
40
- depth: number;
41
- tokenCount: number;
42
- descendantCount: number;
43
- descendantTokenCount: number;
44
- sourceMessageTokenCount: number;
45
- earliestAt: Date | null;
46
- latestAt: Date | null;
47
- childCount: number;
48
- path: string;
49
- }>;
50
- createdAt: Date;
51
- };
52
- /** File-specific fields */
53
- file?: {
54
- conversationId: number;
55
- fileName: string | null;
56
- mimeType: string | null;
57
- byteSize: number | null;
58
- storageUri: string;
59
- explorationSummary: string | null;
60
- createdAt: Date;
61
- };
62
- }
63
-
64
- export interface GrepInput {
65
- query: string;
66
- mode: "regex" | "full_text";
67
- scope: "messages" | "summaries" | "both";
68
- conversationId?: number;
69
- since?: Date;
70
- before?: Date;
71
- limit?: number;
72
- /** Sort order for results. Default "recency" (newest first).
73
- * "relevance" sorts by FTS5 BM25 rank (full_text mode only).
74
- * "hybrid" blends relevance with recency. */
75
- sort?: SearchSort;
76
- }
77
-
78
- export interface GrepResult {
79
- messages: MessageSearchResult[];
80
- summaries: SummarySearchResult[];
81
- totalMatches: number;
82
- }
83
-
84
- export interface ExpandInput {
85
- summaryId: string;
86
- /** Max traversal depth (default 1) */
87
- depth?: number;
88
- /** Include raw source messages at leaf level */
89
- includeMessages?: boolean;
90
- /** Max tokens to return before truncating */
91
- tokenCap?: number;
92
- }
93
-
94
- export interface ExpandResult {
95
- /** Child summaries found */
96
- children: Array<{
97
- summaryId: string;
98
- kind: "leaf" | "condensed";
99
- content: string;
100
- tokenCount: number;
101
- }>;
102
- /** Source messages (only if includeMessages=true and hitting leaf summaries) */
103
- messages: Array<{
104
- messageId: number;
105
- role: string;
106
- content: string;
107
- tokenCount: number;
108
- }>;
109
- /** Total estimated tokens in result */
110
- estimatedTokens: number;
111
- /** Whether result was truncated due to tokenCap */
112
- truncated: boolean;
113
- }
114
-
115
- // ── Helpers ──────────────────────────────────────────────────────────────────
116
-
117
- /** Rough token estimate: ~4 chars per token. */
118
- function estimateTokens(content: string): number {
119
- return Math.ceil(content.length / 4);
120
- }
121
-
122
- // ── RetrievalEngine ──────────────────────────────────────────────────────────
123
-
124
- export class RetrievalEngine {
125
- constructor(
126
- private conversationStore: ConversationStore,
127
- private summaryStore: SummaryStore,
128
- ) {}
129
-
130
- // ── describe ─────────────────────────────────────────────────────────────
131
-
132
- /**
133
- * Describe an LCM item by ID.
134
- *
135
- * - IDs starting with "sum_" are looked up as summaries (with lineage).
136
- * - IDs starting with "file_" are looked up as large files.
137
- * - Returns null if the item is not found.
138
- */
139
- async describe(id: string): Promise<DescribeResult | null> {
140
- if (id.startsWith("sum_")) {
141
- return this.describeSummary(id);
142
- }
143
- if (id.startsWith("file_")) {
144
- return this.describeFile(id);
145
- }
146
- return null;
147
- }
148
-
149
- private async describeSummary(id: string): Promise<DescribeResult | null> {
150
- const summary = await this.summaryStore.getSummary(id);
151
- if (!summary) {
152
- return null;
153
- }
154
-
155
- // Fetch lineage in parallel
156
- const [parents, children, messageIds, subtree] = await Promise.all([
157
- this.summaryStore.getSummaryParents(id),
158
- this.summaryStore.getSummaryChildren(id),
159
- this.summaryStore.getSummaryMessages(id),
160
- this.summaryStore.getSummarySubtree(id),
161
- ]);
162
-
163
- return {
164
- id,
165
- type: "summary",
166
- summary: {
167
- conversationId: summary.conversationId,
168
- kind: summary.kind,
169
- content: summary.content,
170
- depth: summary.depth,
171
- tokenCount: summary.tokenCount,
172
- descendantCount: summary.descendantCount,
173
- descendantTokenCount: summary.descendantTokenCount,
174
- sourceMessageTokenCount: summary.sourceMessageTokenCount,
175
- fileIds: summary.fileIds,
176
- parentIds: parents.map((p) => p.summaryId),
177
- childIds: children.map((c) => c.summaryId),
178
- messageIds,
179
- earliestAt: summary.earliestAt,
180
- latestAt: summary.latestAt,
181
- subtree: subtree.map((node) => ({
182
- summaryId: node.summaryId,
183
- parentSummaryId: node.parentSummaryId,
184
- depthFromRoot: node.depthFromRoot,
185
- kind: node.kind,
186
- depth: node.depth,
187
- tokenCount: node.tokenCount,
188
- descendantCount: node.descendantCount,
189
- descendantTokenCount: node.descendantTokenCount,
190
- sourceMessageTokenCount: node.sourceMessageTokenCount,
191
- earliestAt: node.earliestAt,
192
- latestAt: node.latestAt,
193
- childCount: node.childCount,
194
- path: node.path,
195
- })),
196
- createdAt: summary.createdAt,
197
- },
198
- };
199
- }
200
-
201
- private async describeFile(id: string): Promise<DescribeResult | null> {
202
- const file = await this.summaryStore.getLargeFile(id);
203
- if (!file) {
204
- return null;
205
- }
206
-
207
- return {
208
- id,
209
- type: "file",
210
- file: {
211
- conversationId: file.conversationId,
212
- fileName: file.fileName,
213
- mimeType: file.mimeType,
214
- byteSize: file.byteSize,
215
- storageUri: file.storageUri,
216
- explorationSummary: file.explorationSummary,
217
- createdAt: file.createdAt,
218
- },
219
- };
220
- }
221
-
222
- // ── grep ─────────────────────────────────────────────────────────────────
223
-
224
- /**
225
- * Search compacted history using regex or full-text search.
226
- *
227
- * Depending on `scope`, searches messages, summaries, or both (in parallel).
228
- */
229
- async grep(input: GrepInput): Promise<GrepResult> {
230
- const { query, mode, scope, conversationId, since, before, limit, sort } = input;
231
-
232
- const searchInput = { query, mode, conversationId, since, before, limit, sort };
233
-
234
- let messages: MessageSearchResult[] = [];
235
- let summaries: SummarySearchResult[] = [];
236
-
237
- if (scope === "messages") {
238
- messages = await this.conversationStore.searchMessages(searchInput);
239
- } else if (scope === "summaries") {
240
- summaries = await this.summaryStore.searchSummaries(searchInput);
241
- } else {
242
- // scope === "both" — run in parallel
243
- [messages, summaries] = await Promise.all([
244
- this.conversationStore.searchMessages(searchInput),
245
- this.summaryStore.searchSummaries(searchInput),
246
- ]);
247
- }
248
-
249
- return {
250
- messages,
251
- summaries,
252
- totalMatches: messages.length + summaries.length,
253
- };
254
- }
255
-
256
- // ── expand ───────────────────────────────────────────────────────────────
257
-
258
- /**
259
- * Expand a summary to its children and/or source messages.
260
- *
261
- * - Condensed summaries: returns child summaries, recursing up to `depth`.
262
- * - Leaf summaries with `includeMessages`: fetches the source messages.
263
- * - Respects `tokenCap` and sets `truncated` when the cap is exceeded.
264
- */
265
- async expand(input: ExpandInput): Promise<ExpandResult> {
266
- const depth = input.depth ?? 1;
267
- const includeMessages = input.includeMessages ?? false;
268
- const tokenCap = input.tokenCap ?? Infinity;
269
-
270
- const result: ExpandResult = {
271
- children: [],
272
- messages: [],
273
- estimatedTokens: 0,
274
- truncated: false,
275
- };
276
-
277
- await this.expandRecursive(input.summaryId, depth, includeMessages, tokenCap, result);
278
-
279
- return result;
280
- }
281
-
282
- private async expandRecursive(
283
- summaryId: string,
284
- depth: number,
285
- includeMessages: boolean,
286
- tokenCap: number,
287
- result: ExpandResult,
288
- ): Promise<void> {
289
- if (depth <= 0) {
290
- return;
291
- }
292
- if (result.truncated) {
293
- return;
294
- }
295
-
296
- const summary = await this.summaryStore.getSummary(summaryId);
297
- if (!summary) {
298
- return;
299
- }
300
-
301
- if (summary.kind === "condensed") {
302
- // IMPORTANT: a condensed summary is linked to the summaries that were
303
- // compacted into it via summary_parents(summary_id, parent_summary_id).
304
- // For expansion/replay we need to walk those source summaries, not newer
305
- // summaries that may later derive from this node.
306
- const children = await this.summaryStore.getSummaryParents(summaryId);
307
-
308
- for (const child of children) {
309
- if (result.truncated) {
310
- break;
311
- }
312
-
313
- // Check if adding this child would exceed the token cap
314
- if (result.estimatedTokens + child.tokenCount > tokenCap) {
315
- result.truncated = true;
316
- break;
317
- }
318
-
319
- result.children.push({
320
- summaryId: child.summaryId,
321
- kind: child.kind,
322
- content: child.content,
323
- tokenCount: child.tokenCount,
324
- });
325
- result.estimatedTokens += child.tokenCount;
326
-
327
- // Recurse into children if depth allows
328
- if (depth > 1) {
329
- await this.expandRecursive(child.summaryId, depth - 1, includeMessages, tokenCap, result);
330
- }
331
- }
332
- } else if (summary.kind === "leaf" && includeMessages) {
333
- // Leaf summary — fetch source messages
334
- const messageIds = await this.summaryStore.getSummaryMessages(summaryId);
335
-
336
- for (const msgId of messageIds) {
337
- if (result.truncated) {
338
- break;
339
- }
340
-
341
- const msg = await this.conversationStore.getMessageById(msgId);
342
- if (!msg) {
343
- continue;
344
- }
345
-
346
- const tokenCount = msg.tokenCount || estimateTokens(msg.content);
347
-
348
- if (result.estimatedTokens + tokenCount > tokenCap) {
349
- result.truncated = true;
350
- break;
351
- }
352
-
353
- result.messages.push({
354
- messageId: msg.messageId,
355
- role: msg.role,
356
- content: msg.content,
357
- tokenCount,
358
- });
359
- result.estimatedTokens += tokenCount;
360
- }
361
- }
362
- }
363
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Compile a session glob into a regex.
3
- *
4
- * `*` matches any non-colon characters, while `**` can span colons.
5
- */
6
- export function compileSessionPattern(pattern: string): RegExp {
7
- const escaped = pattern
8
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
9
- .replace(/\*\*/g, "\u0000")
10
- .replace(/\*/g, "[^:]*")
11
- .replace(/\u0000/g, ".*");
12
- return new RegExp(`^${escaped}$`);
13
- }
14
-
15
- /** Compile all configured ignore patterns once at startup. */
16
- export function compileSessionPatterns(patterns: string[]): RegExp[] {
17
- return patterns.map((pattern) => compileSessionPattern(pattern));
18
- }
19
-
20
- /** Check whether a session key matches any compiled ignore pattern. */
21
- export function matchesSessionPattern(sessionKey: string, patterns: RegExp[]): boolean {
22
- return patterns.some((pattern) => pattern.test(sessionKey));
23
- }
@@ -1,49 +0,0 @@
1
- type StartupBannerKey =
2
- | "plugin-loaded"
3
- | "compaction-model"
4
- | "fallback-providers"
5
- | "ignore-session-patterns"
6
- | "stateless-session-patterns";
7
-
8
- type StartupBannerLogState = {
9
- emitted: Set<StartupBannerKey>;
10
- };
11
-
12
- const STARTUP_BANNER_LOG_STATE = Symbol.for(
13
- "@martian-engineering/lossless-claw/startup-banner-log-state",
14
- );
15
-
16
- /** Return the process-global startup banner log state. */
17
- function getStartupBannerLogState(): StartupBannerLogState {
18
- const globalState = globalThis as typeof globalThis & {
19
- [STARTUP_BANNER_LOG_STATE]?: StartupBannerLogState;
20
- };
21
-
22
- if (!globalState[STARTUP_BANNER_LOG_STATE]) {
23
- globalState[STARTUP_BANNER_LOG_STATE] = {
24
- emitted: new Set<StartupBannerKey>(),
25
- };
26
- }
27
-
28
- return globalState[STARTUP_BANNER_LOG_STATE];
29
- }
30
-
31
- /** Emit a startup/config banner only once per process. */
32
- export function logStartupBannerOnce(params: {
33
- key: StartupBannerKey;
34
- log: (message: string) => void;
35
- message: string;
36
- }): void {
37
- const state = getStartupBannerLogState();
38
- if (state.emitted.has(params.key)) {
39
- return;
40
- }
41
-
42
- state.emitted.add(params.key);
43
- params.log(params.message);
44
- }
45
-
46
- /** Reset startup/config banner dedupe state for tests. */
47
- export function resetStartupBannerLogsForTests(): void {
48
- getStartupBannerLogState().emitted.clear();
49
- }
@@ -1,156 +0,0 @@
1
- import type { DatabaseSync } from "node:sqlite";
2
- import { withDatabaseTransaction } from "../transaction-mutex.js";
3
- import { parseUtcTimestampOrNull } from "./parse-utc-timestamp.js";
4
-
5
- export type CacheState = "hot" | "cold" | "unknown";
6
- export type ActivityBand = "low" | "medium" | "high";
7
-
8
- export type ConversationCompactionTelemetryRecord = {
9
- conversationId: number;
10
- lastObservedCacheRead: number | null;
11
- lastObservedCacheWrite: number | null;
12
- lastObservedCacheHitAt: Date | null;
13
- lastObservedCacheBreakAt: Date | null;
14
- cacheState: CacheState;
15
- retention: string | null;
16
- lastLeafCompactionAt: Date | null;
17
- turnsSinceLeafCompaction: number;
18
- tokensAccumulatedSinceLeafCompaction: number;
19
- lastActivityBand: ActivityBand;
20
- updatedAt: Date;
21
- };
22
-
23
- export type UpsertConversationCompactionTelemetryInput = {
24
- conversationId: number;
25
- lastObservedCacheRead?: number | null;
26
- lastObservedCacheWrite?: number | null;
27
- lastObservedCacheHitAt?: Date | null;
28
- lastObservedCacheBreakAt?: Date | null;
29
- cacheState: CacheState;
30
- retention?: string | null;
31
- lastLeafCompactionAt?: Date | null;
32
- turnsSinceLeafCompaction?: number;
33
- tokensAccumulatedSinceLeafCompaction?: number;
34
- lastActivityBand?: ActivityBand;
35
- };
36
-
37
- type ConversationCompactionTelemetryRow = {
38
- conversation_id: number;
39
- last_observed_cache_read: number | null;
40
- last_observed_cache_write: number | null;
41
- last_observed_cache_hit_at: string | null;
42
- last_observed_cache_break_at: string | null;
43
- cache_state: CacheState;
44
- retention: string | null;
45
- last_leaf_compaction_at: string | null;
46
- turns_since_leaf_compaction: number | null;
47
- tokens_accumulated_since_leaf_compaction: number | null;
48
- last_activity_band: ActivityBand | null;
49
- updated_at: string;
50
- };
51
-
52
- function toConversationCompactionTelemetryRecord(
53
- row: ConversationCompactionTelemetryRow,
54
- ): ConversationCompactionTelemetryRecord {
55
- return {
56
- conversationId: row.conversation_id,
57
- lastObservedCacheRead: row.last_observed_cache_read,
58
- lastObservedCacheWrite: row.last_observed_cache_write,
59
- lastObservedCacheHitAt: parseUtcTimestampOrNull(row.last_observed_cache_hit_at),
60
- lastObservedCacheBreakAt: parseUtcTimestampOrNull(row.last_observed_cache_break_at),
61
- cacheState: row.cache_state,
62
- retention: row.retention,
63
- lastLeafCompactionAt: parseUtcTimestampOrNull(row.last_leaf_compaction_at),
64
- turnsSinceLeafCompaction: row.turns_since_leaf_compaction ?? 0,
65
- tokensAccumulatedSinceLeafCompaction: row.tokens_accumulated_since_leaf_compaction ?? 0,
66
- lastActivityBand: row.last_activity_band ?? "low",
67
- updatedAt: parseUtcTimestampOrNull(row.updated_at) ?? new Date(0),
68
- };
69
- }
70
-
71
- /**
72
- * Persist and query per-conversation prompt-cache telemetry used by
73
- * cache-aware incremental compaction.
74
- */
75
- export class CompactionTelemetryStore {
76
- constructor(private readonly db: DatabaseSync) {}
77
-
78
- /** Execute multiple telemetry writes atomically. */
79
- withTransaction<T>(fn: () => Promise<T>): Promise<T> {
80
- return withDatabaseTransaction(this.db, "BEGIN", fn);
81
- }
82
-
83
- /** Load the latest persisted telemetry for a conversation. */
84
- async getConversationCompactionTelemetry(
85
- conversationId: number,
86
- ): Promise<ConversationCompactionTelemetryRecord | null> {
87
- const row = this.db
88
- .prepare(
89
- `SELECT
90
- conversation_id,
91
- last_observed_cache_read,
92
- last_observed_cache_write,
93
- last_observed_cache_hit_at,
94
- last_observed_cache_break_at,
95
- cache_state,
96
- retention,
97
- last_leaf_compaction_at,
98
- turns_since_leaf_compaction,
99
- tokens_accumulated_since_leaf_compaction,
100
- last_activity_band,
101
- updated_at
102
- FROM conversation_compaction_telemetry
103
- WHERE conversation_id = ?`,
104
- )
105
- .get(conversationId) as ConversationCompactionTelemetryRow | undefined;
106
- return row ? toConversationCompactionTelemetryRecord(row) : null;
107
- }
108
-
109
- /** Upsert the current cache telemetry snapshot for a conversation. */
110
- async upsertConversationCompactionTelemetry(
111
- input: UpsertConversationCompactionTelemetryInput,
112
- ): Promise<void> {
113
- this.db
114
- .prepare(
115
- `INSERT INTO conversation_compaction_telemetry (
116
- conversation_id,
117
- last_observed_cache_read,
118
- last_observed_cache_write,
119
- last_observed_cache_hit_at,
120
- last_observed_cache_break_at,
121
- cache_state,
122
- retention,
123
- last_leaf_compaction_at,
124
- turns_since_leaf_compaction,
125
- tokens_accumulated_since_leaf_compaction,
126
- last_activity_band,
127
- updated_at
128
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
129
- ON CONFLICT(conversation_id) DO UPDATE SET
130
- last_observed_cache_read = excluded.last_observed_cache_read,
131
- last_observed_cache_write = excluded.last_observed_cache_write,
132
- last_observed_cache_hit_at = excluded.last_observed_cache_hit_at,
133
- last_observed_cache_break_at = excluded.last_observed_cache_break_at,
134
- cache_state = excluded.cache_state,
135
- retention = excluded.retention,
136
- last_leaf_compaction_at = excluded.last_leaf_compaction_at,
137
- turns_since_leaf_compaction = excluded.turns_since_leaf_compaction,
138
- tokens_accumulated_since_leaf_compaction = excluded.tokens_accumulated_since_leaf_compaction,
139
- last_activity_band = excluded.last_activity_band,
140
- updated_at = datetime('now')`,
141
- )
142
- .run(
143
- input.conversationId,
144
- input.lastObservedCacheRead ?? null,
145
- input.lastObservedCacheWrite ?? null,
146
- input.lastObservedCacheHitAt?.toISOString() ?? null,
147
- input.lastObservedCacheBreakAt?.toISOString() ?? null,
148
- input.cacheState,
149
- input.retention ?? null,
150
- input.lastLeafCompactionAt?.toISOString() ?? null,
151
- input.turnsSinceLeafCompaction ?? 0,
152
- input.tokensAccumulatedSinceLeafCompaction ?? 0,
153
- input.lastActivityBand ?? "low",
154
- );
155
- }
156
- }