@jonathangu/openclawbrain 0.3.0

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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/bin/openclawbrain.js +15 -0
  4. package/docs/END_STATE.md +244 -0
  5. package/docs/EVIDENCE.md +128 -0
  6. package/docs/RELEASE_CONTRACT.md +91 -0
  7. package/docs/agent-tools.md +106 -0
  8. package/docs/architecture.md +224 -0
  9. package/docs/configuration.md +178 -0
  10. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
  11. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
  12. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
  13. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
  14. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
  15. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
  16. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
  17. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
  18. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
  19. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
  20. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
  21. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
  22. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
  23. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
  24. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
  25. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
  26. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
  27. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
  28. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
  29. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
  30. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
  31. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
  32. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
  33. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
  34. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
  35. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
  36. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
  37. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
  38. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
  39. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
  40. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
  41. package/docs/evidence/README.md +16 -0
  42. package/docs/fts5.md +161 -0
  43. package/docs/tui.md +506 -0
  44. package/index.ts +1372 -0
  45. package/openclaw.plugin.json +136 -0
  46. package/package.json +66 -0
  47. package/src/assembler.ts +804 -0
  48. package/src/brain-cli.ts +316 -0
  49. package/src/brain-core/decay.ts +35 -0
  50. package/src/brain-core/episode.ts +82 -0
  51. package/src/brain-core/graph.ts +321 -0
  52. package/src/brain-core/health.ts +116 -0
  53. package/src/brain-core/mutator.ts +281 -0
  54. package/src/brain-core/pack.ts +117 -0
  55. package/src/brain-core/policy.ts +153 -0
  56. package/src/brain-core/replay.ts +1 -0
  57. package/src/brain-core/teacher.ts +105 -0
  58. package/src/brain-core/trace.ts +40 -0
  59. package/src/brain-core/traverse.ts +230 -0
  60. package/src/brain-core/types.ts +405 -0
  61. package/src/brain-core/update.ts +123 -0
  62. package/src/brain-harvest/human.ts +46 -0
  63. package/src/brain-harvest/scanner.ts +98 -0
  64. package/src/brain-harvest/self.ts +147 -0
  65. package/src/brain-runtime/assembler-extension.ts +230 -0
  66. package/src/brain-runtime/evidence-detectors.ts +68 -0
  67. package/src/brain-runtime/graph-io.ts +72 -0
  68. package/src/brain-runtime/harvester-extension.ts +98 -0
  69. package/src/brain-runtime/service.ts +659 -0
  70. package/src/brain-runtime/tools.ts +109 -0
  71. package/src/brain-runtime/worker-state.ts +106 -0
  72. package/src/brain-runtime/worker-supervisor.ts +169 -0
  73. package/src/brain-store/embedding.ts +179 -0
  74. package/src/brain-store/init.ts +347 -0
  75. package/src/brain-store/migrations.ts +188 -0
  76. package/src/brain-store/store.ts +816 -0
  77. package/src/brain-worker/child-runner.ts +321 -0
  78. package/src/brain-worker/jobs.ts +12 -0
  79. package/src/brain-worker/mutation-job.ts +5 -0
  80. package/src/brain-worker/promotion-job.ts +5 -0
  81. package/src/brain-worker/protocol.ts +79 -0
  82. package/src/brain-worker/teacher-job.ts +5 -0
  83. package/src/brain-worker/update-job.ts +5 -0
  84. package/src/brain-worker/worker.ts +422 -0
  85. package/src/compaction.ts +1332 -0
  86. package/src/db/config.ts +265 -0
  87. package/src/db/connection.ts +72 -0
  88. package/src/db/features.ts +42 -0
  89. package/src/db/migration.ts +561 -0
  90. package/src/engine.ts +1995 -0
  91. package/src/expansion-auth.ts +351 -0
  92. package/src/expansion-policy.ts +303 -0
  93. package/src/expansion.ts +383 -0
  94. package/src/integrity.ts +600 -0
  95. package/src/large-files.ts +527 -0
  96. package/src/openclaw-bridge.ts +22 -0
  97. package/src/retrieval.ts +357 -0
  98. package/src/store/conversation-store.ts +748 -0
  99. package/src/store/fts5-sanitize.ts +29 -0
  100. package/src/store/full-text-fallback.ts +74 -0
  101. package/src/store/index.ts +29 -0
  102. package/src/store/summary-store.ts +918 -0
  103. package/src/summarize.ts +847 -0
  104. package/src/tools/common.ts +53 -0
  105. package/src/tools/lcm-conversation-scope.ts +76 -0
  106. package/src/tools/lcm-describe-tool.ts +234 -0
  107. package/src/tools/lcm-expand-query-tool.ts +594 -0
  108. package/src/tools/lcm-expand-tool.delegation.ts +556 -0
  109. package/src/tools/lcm-expand-tool.ts +448 -0
  110. package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
  111. package/src/tools/lcm-grep-tool.ts +200 -0
  112. package/src/transcript-repair.ts +301 -0
  113. package/src/types.ts +149 -0
@@ -0,0 +1,286 @@
1
+ import crypto from "node:crypto";
2
+ import { resolveDelegatedExpansionGrantId } from "../expansion-auth.js";
3
+ import type { LcmDependencies } from "../types.js";
4
+
5
+ export const EXPANSION_RECURSION_ERROR_CODE = "EXPANSION_RECURSION_BLOCKED";
6
+ const EXPANSION_DELEGATION_DEPTH_CAP = 1;
7
+
8
+ type TelemetryEvent = "start" | "block" | "timeout" | "success";
9
+
10
+ const telemetryCounters: Record<TelemetryEvent, number> = {
11
+ start: 0,
12
+ block: 0,
13
+ timeout: 0,
14
+ success: 0,
15
+ };
16
+
17
+ export type DelegatedExpansionContext = {
18
+ requestId: string;
19
+ expansionDepth: number;
20
+ originSessionKey: string;
21
+ stampedBy: string;
22
+ createdAt: string;
23
+ };
24
+
25
+ export type ExpansionRecursionBlockReason = "depth_cap" | "idempotent_reentry";
26
+
27
+ export type ExpansionRecursionGuardDecision =
28
+ | {
29
+ blocked: false;
30
+ requestId: string;
31
+ expansionDepth: number;
32
+ originSessionKey: string;
33
+ }
34
+ | {
35
+ blocked: true;
36
+ code: typeof EXPANSION_RECURSION_ERROR_CODE;
37
+ reason: ExpansionRecursionBlockReason;
38
+ message: string;
39
+ requestId: string;
40
+ expansionDepth: number;
41
+ originSessionKey: string;
42
+ };
43
+
44
+ const delegatedContextBySessionKey = new Map<string, DelegatedExpansionContext>();
45
+ const blockedRequestIdsBySessionKey = new Map<string, Set<string>>();
46
+
47
+ function normalizeSessionKey(sessionKey?: string): string {
48
+ return typeof sessionKey === "string" ? sessionKey.trim() : "";
49
+ }
50
+
51
+ function getOrInitBlockedRequestIds(sessionKey: string): Set<string> {
52
+ const existing = blockedRequestIdsBySessionKey.get(sessionKey);
53
+ if (existing) {
54
+ return existing;
55
+ }
56
+ const created = new Set<string>();
57
+ blockedRequestIdsBySessionKey.set(sessionKey, created);
58
+ return created;
59
+ }
60
+
61
+ function resolveFallbackDelegatedContext(
62
+ sessionKey: string,
63
+ requestId: string,
64
+ ): DelegatedExpansionContext | undefined {
65
+ if (!sessionKey) {
66
+ return undefined;
67
+ }
68
+ const grantId = resolveDelegatedExpansionGrantId(sessionKey);
69
+ if (!grantId) {
70
+ return undefined;
71
+ }
72
+ return {
73
+ requestId,
74
+ expansionDepth: EXPANSION_DELEGATION_DEPTH_CAP,
75
+ originSessionKey: sessionKey,
76
+ stampedBy: "delegated_grant",
77
+ createdAt: new Date().toISOString(),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Build actionable recovery guidance for recursion-blocked delegated calls.
83
+ */
84
+ function buildExpansionRecursionRecoveryGuidance(originSessionKey: string): string {
85
+ return (
86
+ "Recovery: In delegated sub-agent sessions, call `lcm_expand` directly and synthesize " +
87
+ "your answer from that result. Do NOT call `lcm_expand_query` from delegated context. " +
88
+ `If deeper delegation is required, return to the origin session (${originSessionKey}) ` +
89
+ "and call `lcm_expand_query` there."
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Create a stable request identifier for delegated expansion orchestration.
95
+ */
96
+ export function createExpansionRequestId(): string {
97
+ return crypto.randomUUID();
98
+ }
99
+
100
+ /**
101
+ * Resolve the active expansion request id for a session, inheriting from any
102
+ * stamped delegated context when present.
103
+ */
104
+ export function resolveExpansionRequestId(sessionKey?: string): string {
105
+ const key = normalizeSessionKey(sessionKey);
106
+ return delegatedContextBySessionKey.get(key)?.requestId ?? createExpansionRequestId();
107
+ }
108
+
109
+ /**
110
+ * Resolve the next delegated expansion depth to stamp onto a child session.
111
+ */
112
+ export function resolveNextExpansionDepth(sessionKey?: string): number {
113
+ const key = normalizeSessionKey(sessionKey);
114
+ if (!key) {
115
+ return 1;
116
+ }
117
+ const existing = delegatedContextBySessionKey.get(key);
118
+ if (existing) {
119
+ return existing.expansionDepth + 1;
120
+ }
121
+ return resolveDelegatedExpansionGrantId(key) ? EXPANSION_DELEGATION_DEPTH_CAP + 1 : 1;
122
+ }
123
+
124
+ /**
125
+ * Stamp delegated expansion metadata for a child session so re-entry checks can
126
+ * enforce recursion and depth policies deterministically.
127
+ */
128
+ export function stampDelegatedExpansionContext(params: {
129
+ sessionKey: string;
130
+ requestId: string;
131
+ expansionDepth: number;
132
+ originSessionKey: string;
133
+ stampedBy: string;
134
+ }): DelegatedExpansionContext {
135
+ const sessionKey = normalizeSessionKey(params.sessionKey);
136
+ const context: DelegatedExpansionContext = {
137
+ requestId: params.requestId,
138
+ expansionDepth: Math.max(0, Math.trunc(params.expansionDepth)),
139
+ originSessionKey: params.originSessionKey.trim() || "main",
140
+ stampedBy: params.stampedBy,
141
+ createdAt: new Date().toISOString(),
142
+ };
143
+ if (sessionKey) {
144
+ delegatedContextBySessionKey.set(sessionKey, context);
145
+ }
146
+ return context;
147
+ }
148
+
149
+ /**
150
+ * Remove delegated expansion metadata for a child session after cleanup.
151
+ */
152
+ export function clearDelegatedExpansionContext(sessionKey: string): void {
153
+ const key = normalizeSessionKey(sessionKey);
154
+ if (!key) {
155
+ return;
156
+ }
157
+ delegatedContextBySessionKey.delete(key);
158
+ blockedRequestIdsBySessionKey.delete(key);
159
+ }
160
+
161
+ /**
162
+ * Evaluate whether a session is allowed to delegate expansion work.
163
+ * Delegated contexts are blocked at depth >= 1, with repeated request id
164
+ * re-entry mapped to an explicit idempotency block reason.
165
+ */
166
+ export function evaluateExpansionRecursionGuard(params: {
167
+ sessionKey?: string;
168
+ requestId: string;
169
+ }): ExpansionRecursionGuardDecision {
170
+ const sessionKey = normalizeSessionKey(params.sessionKey);
171
+ const requestId = params.requestId.trim();
172
+ const delegatedContext =
173
+ delegatedContextBySessionKey.get(sessionKey) ??
174
+ resolveFallbackDelegatedContext(sessionKey, requestId || createExpansionRequestId());
175
+
176
+ if (!delegatedContext) {
177
+ return {
178
+ blocked: false,
179
+ requestId,
180
+ expansionDepth: 0,
181
+ originSessionKey: sessionKey || "main",
182
+ };
183
+ }
184
+
185
+ if (delegatedContext.expansionDepth < EXPANSION_DELEGATION_DEPTH_CAP) {
186
+ return {
187
+ blocked: false,
188
+ requestId,
189
+ expansionDepth: delegatedContext.expansionDepth,
190
+ originSessionKey: delegatedContext.originSessionKey,
191
+ };
192
+ }
193
+
194
+ const seenRequestIds = getOrInitBlockedRequestIds(sessionKey);
195
+ const isIdempotentReentry = seenRequestIds.has(requestId);
196
+ seenRequestIds.add(requestId);
197
+ const reason: ExpansionRecursionBlockReason = isIdempotentReentry
198
+ ? "idempotent_reentry"
199
+ : "depth_cap";
200
+
201
+ return {
202
+ blocked: true,
203
+ code: EXPANSION_RECURSION_ERROR_CODE,
204
+ reason,
205
+ message:
206
+ `${EXPANSION_RECURSION_ERROR_CODE}: Expansion delegation blocked at depth ` +
207
+ `${delegatedContext.expansionDepth} (${reason}; requestId=${requestId}; ` +
208
+ `origin=${delegatedContext.originSessionKey}). ` +
209
+ buildExpansionRecursionRecoveryGuidance(delegatedContext.originSessionKey),
210
+ requestId,
211
+ expansionDepth: delegatedContext.expansionDepth,
212
+ originSessionKey: delegatedContext.originSessionKey,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Emit structured delegated expansion telemetry with monotonic counters.
218
+ */
219
+ export function recordExpansionDelegationTelemetry(params: {
220
+ deps: Pick<LcmDependencies, "log">;
221
+ component: string;
222
+ event: TelemetryEvent;
223
+ requestId: string;
224
+ sessionKey?: string;
225
+ expansionDepth: number;
226
+ originSessionKey: string;
227
+ reason?: string;
228
+ runId?: string;
229
+ }): void {
230
+ telemetryCounters[params.event] += 1;
231
+ const payload = {
232
+ component: params.component,
233
+ event: params.event,
234
+ requestId: params.requestId,
235
+ sessionKey: normalizeSessionKey(params.sessionKey) || undefined,
236
+ expansionDepth: params.expansionDepth,
237
+ originSessionKey: params.originSessionKey,
238
+ reason: params.reason,
239
+ runId: params.runId,
240
+ counters: {
241
+ start: telemetryCounters.start,
242
+ block: telemetryCounters.block,
243
+ timeout: telemetryCounters.timeout,
244
+ success: telemetryCounters.success,
245
+ },
246
+ };
247
+ const line = `[lcm][expansion_delegation] ${JSON.stringify(payload)}`;
248
+ if (params.event === "start" || params.event === "success") {
249
+ params.deps.log.info(line);
250
+ return;
251
+ }
252
+ params.deps.log.warn(line);
253
+ }
254
+
255
+ /**
256
+ * Return the currently stamped delegated expansion context for test assertions.
257
+ */
258
+ export function getDelegatedExpansionContextForTests(
259
+ sessionKey: string,
260
+ ): DelegatedExpansionContext | undefined {
261
+ return delegatedContextBySessionKey.get(normalizeSessionKey(sessionKey));
262
+ }
263
+
264
+ /**
265
+ * Return the delegated expansion telemetry counters for tests.
266
+ */
267
+ export function getExpansionDelegationTelemetrySnapshotForTests(): Record<TelemetryEvent, number> {
268
+ return {
269
+ start: telemetryCounters.start,
270
+ block: telemetryCounters.block,
271
+ timeout: telemetryCounters.timeout,
272
+ success: telemetryCounters.success,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Reset delegated expansion context and telemetry state between tests.
278
+ */
279
+ export function resetExpansionDelegationGuardForTests(): void {
280
+ delegatedContextBySessionKey.clear();
281
+ blockedRequestIdsBySessionKey.clear();
282
+ telemetryCounters.start = 0;
283
+ telemetryCounters.block = 0;
284
+ telemetryCounters.timeout = 0;
285
+ telemetryCounters.success = 0;
286
+ }
@@ -0,0 +1,200 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { LcmContextEngine } from "../engine.js";
3
+ import type { LcmDependencies } from "../types.js";
4
+ import type { AnyAgentTool } from "./common.js";
5
+ import { jsonResult } from "./common.js";
6
+ import { parseIsoTimestampParam, resolveLcmConversationScope } from "./lcm-conversation-scope.js";
7
+ import { formatTimestamp } from "../compaction.js";
8
+
9
+ const MAX_RESULT_CHARS = 40_000; // ~10k tokens
10
+
11
+ const LcmGrepSchema = Type.Object({
12
+ pattern: Type.String({
13
+ description:
14
+ "Search pattern. Interpreted as regex when mode is 'regex', or as a text query for 'full_text' mode.",
15
+ }),
16
+ mode: Type.Optional(
17
+ Type.String({
18
+ description:
19
+ 'Search mode: "regex" for regular expression matching, "full_text" for text search. Default: "regex".',
20
+ enum: ["regex", "full_text"],
21
+ }),
22
+ ),
23
+ scope: Type.Optional(
24
+ Type.String({
25
+ description:
26
+ 'What to search: "messages" for raw messages, "summaries" for compacted summaries, "both" for all. Default: "both".',
27
+ enum: ["messages", "summaries", "both"],
28
+ }),
29
+ ),
30
+ conversationId: Type.Optional(
31
+ Type.Number({
32
+ description:
33
+ "Conversation ID to search within. If omitted, defaults to the current session conversation.",
34
+ }),
35
+ ),
36
+ allConversations: Type.Optional(
37
+ Type.Boolean({
38
+ description:
39
+ "Set true to explicitly search across all conversations. Ignored when conversationId is provided.",
40
+ }),
41
+ ),
42
+ since: Type.Optional(
43
+ Type.String({
44
+ description: "Only return matches created at or after this ISO timestamp.",
45
+ }),
46
+ ),
47
+ before: Type.Optional(
48
+ Type.String({
49
+ description: "Only return matches created before this ISO timestamp.",
50
+ }),
51
+ ),
52
+ limit: Type.Optional(
53
+ Type.Number({
54
+ description: "Maximum number of results to return (default: 50).",
55
+ minimum: 1,
56
+ maximum: 200,
57
+ }),
58
+ ),
59
+ });
60
+
61
+ function truncateSnippet(content: string, maxLen: number = 200): string {
62
+ const singleLine = content.replace(/\n/g, " ").trim();
63
+ if (singleLine.length <= maxLen) {
64
+ return singleLine;
65
+ }
66
+ return singleLine.substring(0, maxLen - 3) + "...";
67
+ }
68
+
69
+ export function createLcmGrepTool(input: {
70
+ deps: LcmDependencies;
71
+ lcm: LcmContextEngine;
72
+ sessionId?: string;
73
+ sessionKey?: string;
74
+ }): AnyAgentTool {
75
+ return {
76
+ name: "lcm_grep",
77
+ label: "LCM Grep",
78
+ description:
79
+ "Search compacted conversation history using regex or full-text search. " +
80
+ "Searches across messages and/or summaries stored by LCM. " +
81
+ "Use this to find specific content that may have been compacted away from " +
82
+ "active context. Returns matching snippets with their summary/message IDs " +
83
+ "for follow-up with lcm_expand or lcm_describe.",
84
+ parameters: LcmGrepSchema,
85
+ async execute(_toolCallId, params) {
86
+ const retrieval = input.lcm.getRetrieval();
87
+ const timezone = input.lcm.timezone;
88
+
89
+ const p = params as Record<string, unknown>;
90
+ const pattern = (p.pattern as string).trim();
91
+ const mode = (p.mode as "regex" | "full_text") ?? "regex";
92
+ const scope = (p.scope as "messages" | "summaries" | "both") ?? "both";
93
+ const limit = typeof p.limit === "number" ? Math.trunc(p.limit) : 50;
94
+ let since: Date | undefined;
95
+ let before: Date | undefined;
96
+ try {
97
+ since = parseIsoTimestampParam(p, "since");
98
+ before = parseIsoTimestampParam(p, "before");
99
+ } catch (error) {
100
+ return jsonResult({
101
+ error: error instanceof Error ? error.message : "Invalid timestamp filter.",
102
+ });
103
+ }
104
+ if (since && before && since.getTime() >= before.getTime()) {
105
+ return jsonResult({
106
+ error: "`since` must be earlier than `before`.",
107
+ });
108
+ }
109
+ const conversationScope = await resolveLcmConversationScope({
110
+ lcm: input.lcm,
111
+ deps: input.deps,
112
+ sessionId: input.sessionId,
113
+ sessionKey: input.sessionKey,
114
+ params: p,
115
+ });
116
+ if (!conversationScope.allConversations && conversationScope.conversationId == null) {
117
+ return jsonResult({
118
+ error:
119
+ "No LCM conversation found for this session. Provide conversationId or set allConversations=true.",
120
+ });
121
+ }
122
+
123
+ const result = await retrieval.grep({
124
+ query: pattern,
125
+ mode,
126
+ scope,
127
+ conversationId: conversationScope.conversationId,
128
+ limit,
129
+ since,
130
+ before,
131
+ });
132
+
133
+ const lines: string[] = [];
134
+ lines.push("## LCM Grep Results");
135
+ lines.push(`**Pattern:** \`${pattern}\``);
136
+ lines.push(`**Mode:** ${mode} | **Scope:** ${scope}`);
137
+ if (conversationScope.allConversations) {
138
+ lines.push("**Conversation scope:** all conversations");
139
+ } else if (conversationScope.conversationId != null) {
140
+ lines.push(`**Conversation scope:** ${conversationScope.conversationId}`);
141
+ }
142
+ if (since || before) {
143
+ lines.push(
144
+ `**Time filter:** ${since ? `since ${formatTimestamp(since, timezone)}` : "since -∞"} | ${
145
+ before ? `before ${formatTimestamp(before, timezone)}` : "before +∞"
146
+ }`,
147
+ );
148
+ }
149
+ lines.push(`**Total matches:** ${result.totalMatches}`);
150
+ lines.push("");
151
+
152
+ let currentChars = lines.join("\n").length;
153
+
154
+ if (result.messages.length > 0) {
155
+ lines.push("### Messages");
156
+ lines.push("");
157
+ for (const msg of result.messages) {
158
+ const snippet = truncateSnippet(msg.snippet);
159
+ const line = `- [msg#${msg.messageId}] (${msg.role}, ${formatTimestamp(msg.createdAt, timezone)}): ${snippet}`;
160
+ if (currentChars + line.length > MAX_RESULT_CHARS) {
161
+ lines.push("*(truncated — more results available)*");
162
+ break;
163
+ }
164
+ lines.push(line);
165
+ currentChars += line.length;
166
+ }
167
+ lines.push("");
168
+ }
169
+
170
+ if (result.summaries.length > 0) {
171
+ lines.push("### Summaries");
172
+ lines.push("");
173
+ for (const sum of result.summaries) {
174
+ const snippet = truncateSnippet(sum.snippet);
175
+ const line = `- [${sum.summaryId}] (${sum.kind}, ${formatTimestamp(sum.createdAt, timezone)}): ${snippet}`;
176
+ if (currentChars + line.length > MAX_RESULT_CHARS) {
177
+ lines.push("*(truncated — more results available)*");
178
+ break;
179
+ }
180
+ lines.push(line);
181
+ currentChars += line.length;
182
+ }
183
+ lines.push("");
184
+ }
185
+
186
+ if (result.totalMatches === 0) {
187
+ lines.push("No matches found.");
188
+ }
189
+
190
+ return {
191
+ content: [{ type: "text", text: lines.join("\n") }],
192
+ details: {
193
+ messageCount: result.messages.length,
194
+ summaryCount: result.summaries.length,
195
+ totalMatches: result.totalMatches,
196
+ },
197
+ };
198
+ },
199
+ };
200
+ }