@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,556 @@
1
+ import crypto from "node:crypto";
2
+ import type { LcmContextEngine } from "../engine.js";
3
+ import {
4
+ createDelegatedExpansionGrant,
5
+ revokeDelegatedExpansionGrantForSession,
6
+ } from "../expansion-auth.js";
7
+ import type { LcmDependencies } from "../types.js";
8
+ import {
9
+ clearDelegatedExpansionContext,
10
+ evaluateExpansionRecursionGuard,
11
+ recordExpansionDelegationTelemetry,
12
+ resolveExpansionRequestId,
13
+ stampDelegatedExpansionContext,
14
+ } from "./lcm-expansion-recursion-guard.js";
15
+
16
+ const MAX_GATEWAY_TIMEOUT_MS = 2_147_483_647;
17
+
18
+ type DelegatedPassStatus = "ok" | "timeout" | "error";
19
+
20
+ type DelegatedExpansionPassResult = {
21
+ pass: number;
22
+ status: DelegatedPassStatus;
23
+ runId: string;
24
+ childSessionKey: string;
25
+ summary: string;
26
+ citedIds: string[];
27
+ followUpSummaryIds: string[];
28
+ totalTokens: number;
29
+ truncated: boolean;
30
+ rawReply?: string;
31
+ error?: string;
32
+ };
33
+
34
+ export type DelegatedExpansionLoopResult = {
35
+ status: DelegatedPassStatus;
36
+ passes: DelegatedExpansionPassResult[];
37
+ citedIds: string[];
38
+ totalTokens: number;
39
+ truncated: boolean;
40
+ text: string;
41
+ error?: string;
42
+ };
43
+
44
+ export function normalizeSummaryIds(input: string[] | undefined): string[] {
45
+ if (!Array.isArray(input)) {
46
+ return [];
47
+ }
48
+ const seen = new Set<string>();
49
+ const normalized: string[] = [];
50
+ for (const value of input) {
51
+ if (typeof value !== "string") {
52
+ continue;
53
+ }
54
+ const trimmed = value.trim();
55
+ if (!trimmed || seen.has(trimmed)) {
56
+ continue;
57
+ }
58
+ seen.add(trimmed);
59
+ normalized.push(trimmed);
60
+ }
61
+ return normalized;
62
+ }
63
+
64
+ function parseDelegatedExpansionReply(rawReply: string | undefined): {
65
+ summary: string;
66
+ citedIds: string[];
67
+ followUpSummaryIds: string[];
68
+ totalTokens: number;
69
+ truncated: boolean;
70
+ } {
71
+ const fallback = {
72
+ summary: (rawReply ?? "").trim(),
73
+ citedIds: [] as string[],
74
+ followUpSummaryIds: [] as string[],
75
+ totalTokens: 0,
76
+ truncated: false,
77
+ };
78
+ const reply = rawReply?.trim();
79
+ if (!reply) {
80
+ return fallback;
81
+ }
82
+
83
+ const candidates: string[] = [reply];
84
+ const fenced = reply.match(/```(?:json)?\s*([\s\S]*?)```/i);
85
+ if (fenced?.[1]) {
86
+ candidates.unshift(fenced[1].trim());
87
+ }
88
+
89
+ for (const candidate of candidates) {
90
+ try {
91
+ const parsed = JSON.parse(candidate) as {
92
+ summary?: unknown;
93
+ citedIds?: unknown;
94
+ followUpSummaryIds?: unknown;
95
+ totalTokens?: unknown;
96
+ truncated?: unknown;
97
+ };
98
+ const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
99
+ const citedIds = normalizeSummaryIds(
100
+ Array.isArray(parsed.citedIds)
101
+ ? parsed.citedIds.filter((value): value is string => typeof value === "string")
102
+ : undefined,
103
+ );
104
+ const followUpSummaryIds = normalizeSummaryIds(
105
+ Array.isArray(parsed.followUpSummaryIds)
106
+ ? parsed.followUpSummaryIds.filter((value): value is string => typeof value === "string")
107
+ : undefined,
108
+ );
109
+ const totalTokens =
110
+ typeof parsed.totalTokens === "number" && Number.isFinite(parsed.totalTokens)
111
+ ? Math.max(0, Math.floor(parsed.totalTokens))
112
+ : 0;
113
+ const truncated = parsed.truncated === true;
114
+ return {
115
+ summary: summary || fallback.summary,
116
+ citedIds,
117
+ followUpSummaryIds,
118
+ totalTokens,
119
+ truncated,
120
+ };
121
+ } catch {
122
+ // Keep parsing candidates until one succeeds.
123
+ }
124
+ }
125
+
126
+ return fallback;
127
+ }
128
+
129
+ function formatDelegatedExpansionText(passes: DelegatedExpansionPassResult[]): string {
130
+ const lines: string[] = [];
131
+ const allCitedIds = new Set<string>();
132
+
133
+ for (const pass of passes) {
134
+ for (const summaryId of pass.citedIds) {
135
+ allCitedIds.add(summaryId);
136
+ }
137
+ if (!pass.summary.trim()) {
138
+ continue;
139
+ }
140
+ if (passes.length > 1) {
141
+ lines.push(`Pass ${pass.pass}: ${pass.summary.trim()}`);
142
+ } else {
143
+ lines.push(pass.summary.trim());
144
+ }
145
+ }
146
+
147
+ if (lines.length === 0) {
148
+ lines.push("Delegated expansion completed with no textual summary.");
149
+ }
150
+
151
+ if (allCitedIds.size > 0) {
152
+ lines.push("", "Cited IDs:", ...Array.from(allCitedIds).map((value) => `- ${value}`));
153
+ }
154
+
155
+ return lines.join("\n");
156
+ }
157
+
158
+ function buildDelegatedExpansionTask(params: {
159
+ summaryIds: string[];
160
+ conversationId: number;
161
+ maxDepth?: number;
162
+ tokenCap?: number;
163
+ includeMessages: boolean;
164
+ pass: number;
165
+ query?: string;
166
+ requestId: string;
167
+ expansionDepth: number;
168
+ originSessionKey: string;
169
+ }) {
170
+ const payload: {
171
+ summaryIds: string[];
172
+ conversationId: number;
173
+ maxDepth?: number;
174
+ tokenCap?: number;
175
+ includeMessages: boolean;
176
+ } = {
177
+ summaryIds: params.summaryIds,
178
+ conversationId: params.conversationId,
179
+ maxDepth: params.maxDepth,
180
+ includeMessages: params.includeMessages,
181
+ };
182
+ if (typeof params.tokenCap === "number" && Number.isFinite(params.tokenCap)) {
183
+ payload.tokenCap = params.tokenCap;
184
+ }
185
+ return [
186
+ "Run LCM expansion and report distilled findings.",
187
+ params.query ? `Original query: ${params.query}` : undefined,
188
+ `Pass ${params.pass}`,
189
+ "",
190
+ "Call `lcm_expand` using exactly this JSON payload:",
191
+ JSON.stringify(payload, null, 2),
192
+ "",
193
+ "Delegated expansion metadata (for tracing):",
194
+ `- requestId: ${params.requestId}`,
195
+ `- expansionDepth: ${params.expansionDepth}`,
196
+ `- originSessionKey: ${params.originSessionKey}`,
197
+ "",
198
+ "Then return ONLY JSON with this shape:",
199
+ "{",
200
+ ' "summary": "string concise findings",',
201
+ ' "citedIds": ["sum_xxx"],',
202
+ ' "followUpSummaryIds": ["sum_xxx"],',
203
+ ' "totalTokens": 0,',
204
+ ' "truncated": false',
205
+ "}",
206
+ "",
207
+ "Rules:",
208
+ "- In delegated context, use `lcm_expand` directly for retrieval.",
209
+ "- DO NOT call `lcm_expand_query` from this delegated session.",
210
+ "- Keep summary concise and factual.",
211
+ "- Synthesize findings from the `lcm_expand` result before returning.",
212
+ "- citedIds/followUpSummaryIds must contain unique summary IDs only.",
213
+ "- If no follow-up is needed, return an empty followUpSummaryIds array.",
214
+ ]
215
+ .filter((line): line is string => line !== undefined)
216
+ .join("\n");
217
+ }
218
+
219
+ /**
220
+ * Resolve the requester's active LCM conversation ID from the session store.
221
+ * This allows delegated expansion to stay scoped even when conversationId
222
+ * wasn't passed explicitly in the tool call.
223
+ */
224
+ export async function resolveRequesterConversationScopeId(params: {
225
+ deps: Pick<LcmDependencies, "resolveSessionIdFromSessionKey">;
226
+ requesterSessionKey: string;
227
+ lcm: LcmContextEngine;
228
+ }): Promise<number | undefined> {
229
+ const requesterSessionKey = params.requesterSessionKey.trim();
230
+ if (!requesterSessionKey) {
231
+ return undefined;
232
+ }
233
+
234
+ try {
235
+ const runtimeSessionId = await params.deps.resolveSessionIdFromSessionKey(requesterSessionKey);
236
+ if (!runtimeSessionId) {
237
+ return undefined;
238
+ }
239
+ const conversation = await params.lcm
240
+ .getConversationStore()
241
+ .getConversationBySessionId(runtimeSessionId);
242
+ return conversation?.conversationId;
243
+ } catch {
244
+ return undefined;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Execute one delegated pass via a scoped sub-agent session.
250
+ * Each pass creates its own grant/session and always performs cleanup.
251
+ */
252
+ async function runDelegatedExpansionPass(params: {
253
+ deps: Pick<
254
+ LcmDependencies,
255
+ | "callGateway"
256
+ | "parseAgentSessionKey"
257
+ | "normalizeAgentId"
258
+ | "buildSubagentSystemPrompt"
259
+ | "readLatestAssistantReply"
260
+ | "agentLaneSubagent"
261
+ | "log"
262
+ >;
263
+ requesterSessionKey: string;
264
+ conversationId: number;
265
+ summaryIds: string[];
266
+ maxDepth?: number;
267
+ tokenCap?: number;
268
+ includeMessages: boolean;
269
+ query?: string;
270
+ pass: number;
271
+ requestId: string;
272
+ parentExpansionDepth: number;
273
+ originSessionKey: string;
274
+ }): Promise<DelegatedExpansionPassResult> {
275
+ const requesterAgentId = params.deps.normalizeAgentId(
276
+ params.deps.parseAgentSessionKey(params.requesterSessionKey)?.agentId,
277
+ );
278
+ const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
279
+ let runId = "";
280
+
281
+ createDelegatedExpansionGrant({
282
+ delegatedSessionKey: childSessionKey,
283
+ issuerSessionId: params.requesterSessionKey,
284
+ allowedConversationIds: [params.conversationId],
285
+ tokenCap: params.tokenCap,
286
+ ttlMs: MAX_GATEWAY_TIMEOUT_MS,
287
+ });
288
+ stampDelegatedExpansionContext({
289
+ sessionKey: childSessionKey,
290
+ requestId: params.requestId,
291
+ expansionDepth: params.parentExpansionDepth + 1,
292
+ originSessionKey: params.originSessionKey,
293
+ stampedBy: "runDelegatedExpansionLoop",
294
+ });
295
+
296
+ try {
297
+ const message = buildDelegatedExpansionTask({
298
+ summaryIds: params.summaryIds,
299
+ conversationId: params.conversationId,
300
+ maxDepth: params.maxDepth,
301
+ tokenCap: params.tokenCap,
302
+ includeMessages: params.includeMessages,
303
+ pass: params.pass,
304
+ query: params.query,
305
+ requestId: params.requestId,
306
+ expansionDepth: params.parentExpansionDepth + 1,
307
+ originSessionKey: params.originSessionKey,
308
+ });
309
+ const response = (await params.deps.callGateway({
310
+ method: "agent",
311
+ params: {
312
+ message,
313
+ sessionKey: childSessionKey,
314
+ deliver: false,
315
+ lane: params.deps.agentLaneSubagent,
316
+ extraSystemPrompt: params.deps.buildSubagentSystemPrompt({
317
+ depth: 1,
318
+ maxDepth: 8,
319
+ taskSummary: "Run lcm_expand and return JSON findings",
320
+ }),
321
+ },
322
+ timeoutMs: 10_000,
323
+ })) as { runId?: string };
324
+ runId =
325
+ typeof response?.runId === "string" && response.runId ? response.runId : crypto.randomUUID();
326
+
327
+ const wait = (await params.deps.callGateway({
328
+ method: "agent.wait",
329
+ params: {
330
+ runId,
331
+ timeoutMs: MAX_GATEWAY_TIMEOUT_MS,
332
+ },
333
+ timeoutMs: MAX_GATEWAY_TIMEOUT_MS,
334
+ })) as { status?: string; error?: string };
335
+ const status = typeof wait?.status === "string" ? wait.status : "error";
336
+ if (status === "timeout") {
337
+ return {
338
+ pass: params.pass,
339
+ status: "timeout",
340
+ runId,
341
+ childSessionKey,
342
+ summary: "",
343
+ citedIds: [],
344
+ followUpSummaryIds: [],
345
+ totalTokens: 0,
346
+ truncated: true,
347
+ error: "delegated expansion pass timed out",
348
+ };
349
+ }
350
+ if (status !== "ok") {
351
+ return {
352
+ pass: params.pass,
353
+ status: "error",
354
+ runId,
355
+ childSessionKey,
356
+ summary: "",
357
+ citedIds: [],
358
+ followUpSummaryIds: [],
359
+ totalTokens: 0,
360
+ truncated: true,
361
+ error: typeof wait?.error === "string" ? wait.error : "delegated expansion pass failed",
362
+ };
363
+ }
364
+
365
+ const replyPayload = (await params.deps.callGateway({
366
+ method: "sessions.get",
367
+ params: { key: childSessionKey, limit: 80 },
368
+ timeoutMs: 10_000,
369
+ })) as { messages?: unknown[] };
370
+ const reply = params.deps.readLatestAssistantReply(
371
+ Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
372
+ );
373
+ const parsed = parseDelegatedExpansionReply(reply);
374
+ return {
375
+ pass: params.pass,
376
+ status: "ok",
377
+ runId,
378
+ childSessionKey,
379
+ summary: parsed.summary,
380
+ citedIds: parsed.citedIds,
381
+ followUpSummaryIds: parsed.followUpSummaryIds,
382
+ totalTokens: parsed.totalTokens,
383
+ truncated: parsed.truncated,
384
+ rawReply: reply,
385
+ };
386
+ } catch (err) {
387
+ return {
388
+ pass: params.pass,
389
+ status: "error",
390
+ runId: runId || crypto.randomUUID(),
391
+ childSessionKey,
392
+ summary: "",
393
+ citedIds: [],
394
+ followUpSummaryIds: [],
395
+ totalTokens: 0,
396
+ truncated: true,
397
+ error: err instanceof Error ? err.message : String(err),
398
+ };
399
+ } finally {
400
+ try {
401
+ await params.deps.callGateway({
402
+ method: "sessions.delete",
403
+ params: { key: childSessionKey, deleteTranscript: true },
404
+ timeoutMs: 10_000,
405
+ });
406
+ } catch {
407
+ // Cleanup is best-effort.
408
+ }
409
+ revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
410
+ clearDelegatedExpansionContext(childSessionKey);
411
+ }
412
+ }
413
+
414
+ export async function runDelegatedExpansionLoop(params: {
415
+ deps: Pick<
416
+ LcmDependencies,
417
+ | "callGateway"
418
+ | "parseAgentSessionKey"
419
+ | "normalizeAgentId"
420
+ | "buildSubagentSystemPrompt"
421
+ | "readLatestAssistantReply"
422
+ | "agentLaneSubagent"
423
+ | "log"
424
+ >;
425
+ requesterSessionKey: string;
426
+ conversationId: number;
427
+ summaryIds: string[];
428
+ maxDepth?: number;
429
+ tokenCap?: number;
430
+ includeMessages: boolean;
431
+ query?: string;
432
+ requestId?: string;
433
+ }): Promise<DelegatedExpansionLoopResult> {
434
+ const requestId = params.requestId?.trim() || resolveExpansionRequestId(params.requesterSessionKey);
435
+ const recursionCheck = evaluateExpansionRecursionGuard({
436
+ sessionKey: params.requesterSessionKey,
437
+ requestId,
438
+ });
439
+ recordExpansionDelegationTelemetry({
440
+ deps: params.deps,
441
+ component: "runDelegatedExpansionLoop",
442
+ event: "start",
443
+ requestId,
444
+ sessionKey: params.requesterSessionKey,
445
+ expansionDepth: recursionCheck.expansionDepth,
446
+ originSessionKey: recursionCheck.originSessionKey,
447
+ });
448
+ if (recursionCheck.blocked) {
449
+ recordExpansionDelegationTelemetry({
450
+ deps: params.deps,
451
+ component: "runDelegatedExpansionLoop",
452
+ event: "block",
453
+ requestId,
454
+ sessionKey: params.requesterSessionKey,
455
+ expansionDepth: recursionCheck.expansionDepth,
456
+ originSessionKey: recursionCheck.originSessionKey,
457
+ reason: recursionCheck.reason,
458
+ });
459
+ return {
460
+ status: "error",
461
+ passes: [],
462
+ citedIds: [],
463
+ totalTokens: 0,
464
+ truncated: true,
465
+ text: "Delegated expansion blocked by recursion guard.",
466
+ error: recursionCheck.message,
467
+ };
468
+ }
469
+
470
+ const passes: DelegatedExpansionPassResult[] = [];
471
+ const visited = new Set<string>();
472
+ const cited = new Set<string>();
473
+ let queue = normalizeSummaryIds(params.summaryIds);
474
+
475
+ let pass = 1;
476
+ while (queue.length > 0) {
477
+ for (const summaryId of queue) {
478
+ visited.add(summaryId);
479
+ }
480
+ const result = await runDelegatedExpansionPass({
481
+ deps: params.deps,
482
+ requesterSessionKey: params.requesterSessionKey,
483
+ conversationId: params.conversationId,
484
+ summaryIds: queue,
485
+ maxDepth: params.maxDepth,
486
+ tokenCap: params.tokenCap,
487
+ includeMessages: params.includeMessages,
488
+ query: params.query,
489
+ pass,
490
+ requestId,
491
+ parentExpansionDepth: recursionCheck.expansionDepth,
492
+ originSessionKey: recursionCheck.originSessionKey,
493
+ });
494
+ passes.push(result);
495
+
496
+ if (result.status !== "ok") {
497
+ if (result.status === "timeout") {
498
+ recordExpansionDelegationTelemetry({
499
+ deps: params.deps,
500
+ component: "runDelegatedExpansionLoop",
501
+ event: "timeout",
502
+ requestId,
503
+ sessionKey: params.requesterSessionKey,
504
+ expansionDepth: recursionCheck.expansionDepth,
505
+ originSessionKey: recursionCheck.originSessionKey,
506
+ runId: result.runId,
507
+ });
508
+ }
509
+ const okPasses = passes.filter((entry) => entry.status === "ok");
510
+ for (const okPass of okPasses) {
511
+ for (const summaryId of okPass.citedIds) {
512
+ cited.add(summaryId);
513
+ }
514
+ }
515
+ const text =
516
+ okPasses.length > 0
517
+ ? formatDelegatedExpansionText(okPasses)
518
+ : "Delegated expansion failed before any pass completed.";
519
+ return {
520
+ status: result.status,
521
+ passes,
522
+ citedIds: Array.from(cited),
523
+ totalTokens: okPasses.reduce((sum, entry) => sum + entry.totalTokens, 0),
524
+ truncated: true,
525
+ text,
526
+ error: result.error,
527
+ };
528
+ }
529
+
530
+ for (const summaryId of result.citedIds) {
531
+ cited.add(summaryId);
532
+ }
533
+
534
+ const nextQueue = result.followUpSummaryIds.filter((summaryId) => !visited.has(summaryId));
535
+ queue = nextQueue;
536
+ pass += 1;
537
+ }
538
+
539
+ recordExpansionDelegationTelemetry({
540
+ deps: params.deps,
541
+ component: "runDelegatedExpansionLoop",
542
+ event: "success",
543
+ requestId,
544
+ sessionKey: params.requesterSessionKey,
545
+ expansionDepth: recursionCheck.expansionDepth,
546
+ originSessionKey: recursionCheck.originSessionKey,
547
+ });
548
+ return {
549
+ status: "ok",
550
+ passes,
551
+ citedIds: Array.from(cited),
552
+ totalTokens: passes.reduce((sum, entry) => sum + entry.totalTokens, 0),
553
+ truncated: passes.some((entry) => entry.truncated),
554
+ text: formatDelegatedExpansionText(passes),
555
+ };
556
+ }