@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,47 @@
1
+ export const ACP_ERROR_CODES = [
2
+ "ACP_BACKEND_MISSING",
3
+ "ACP_BACKEND_UNAVAILABLE",
4
+ "ACP_BACKEND_UNSUPPORTED_CONTROL",
5
+ "ACP_DISPATCH_DISABLED",
6
+ "ACP_INVALID_RUNTIME_OPTION",
7
+ "ACP_SESSION_INIT_FAILED",
8
+ "ACP_TURN_FAILED",
9
+ ];
10
+ export class AcpRuntimeError extends Error {
11
+ code;
12
+ cause;
13
+ constructor(code, message, options) {
14
+ super(message);
15
+ this.name = "AcpRuntimeError";
16
+ this.code = code;
17
+ this.cause = options?.cause;
18
+ }
19
+ }
20
+ export function isAcpRuntimeError(value) {
21
+ return value instanceof AcpRuntimeError;
22
+ }
23
+ export function toAcpRuntimeError(params) {
24
+ if (params.error instanceof AcpRuntimeError) {
25
+ return params.error;
26
+ }
27
+ if (params.error instanceof Error) {
28
+ return new AcpRuntimeError(params.fallbackCode, params.error.message, {
29
+ cause: params.error,
30
+ });
31
+ }
32
+ return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, {
33
+ cause: params.error,
34
+ });
35
+ }
36
+ export async function withAcpRuntimeErrorBoundary(params) {
37
+ try {
38
+ return await params.run();
39
+ }
40
+ catch (error) {
41
+ throw toAcpRuntimeError({
42
+ error,
43
+ fallbackCode: params.fallbackCode,
44
+ fallbackMessage: params.fallbackMessage,
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,86 @@
1
+ import { AcpRuntimeError } from "./errors.js";
2
+ const ACP_RUNTIME_REGISTRY_STATE_KEY = Symbol.for("poolbot.acpRuntimeRegistryState");
3
+ function createAcpRuntimeRegistryGlobalState() {
4
+ return {
5
+ backendsById: new Map(),
6
+ };
7
+ }
8
+ function resolveAcpRuntimeRegistryGlobalState() {
9
+ const runtimeGlobal = globalThis;
10
+ if (!runtimeGlobal[ACP_RUNTIME_REGISTRY_STATE_KEY]) {
11
+ runtimeGlobal[ACP_RUNTIME_REGISTRY_STATE_KEY] = createAcpRuntimeRegistryGlobalState();
12
+ }
13
+ return runtimeGlobal[ACP_RUNTIME_REGISTRY_STATE_KEY];
14
+ }
15
+ const ACP_BACKENDS_BY_ID = resolveAcpRuntimeRegistryGlobalState().backendsById;
16
+ function normalizeBackendId(id) {
17
+ return id?.trim().toLowerCase() || "";
18
+ }
19
+ function isBackendHealthy(backend) {
20
+ if (!backend.healthy) {
21
+ return true;
22
+ }
23
+ try {
24
+ return backend.healthy();
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ export function registerAcpRuntimeBackend(backend) {
31
+ const id = normalizeBackendId(backend.id);
32
+ if (!id) {
33
+ throw new Error("ACP runtime backend id is required");
34
+ }
35
+ if (!backend.runtime) {
36
+ throw new Error(`ACP runtime backend "${id}" is missing runtime implementation`);
37
+ }
38
+ ACP_BACKENDS_BY_ID.set(id, {
39
+ ...backend,
40
+ id,
41
+ });
42
+ }
43
+ export function unregisterAcpRuntimeBackend(id) {
44
+ const normalized = normalizeBackendId(id);
45
+ if (!normalized) {
46
+ return;
47
+ }
48
+ ACP_BACKENDS_BY_ID.delete(normalized);
49
+ }
50
+ export function getAcpRuntimeBackend(id) {
51
+ const normalized = normalizeBackendId(id);
52
+ if (normalized) {
53
+ return ACP_BACKENDS_BY_ID.get(normalized) ?? null;
54
+ }
55
+ if (ACP_BACKENDS_BY_ID.size === 0) {
56
+ return null;
57
+ }
58
+ for (const backend of ACP_BACKENDS_BY_ID.values()) {
59
+ if (isBackendHealthy(backend)) {
60
+ return backend;
61
+ }
62
+ }
63
+ return ACP_BACKENDS_BY_ID.values().next().value ?? null;
64
+ }
65
+ export function requireAcpRuntimeBackend(id) {
66
+ const normalized = normalizeBackendId(id);
67
+ const backend = getAcpRuntimeBackend(normalized || undefined);
68
+ if (!backend) {
69
+ throw new AcpRuntimeError("ACP_BACKEND_MISSING", "ACP runtime backend is not configured. Install and enable the acpx runtime plugin.");
70
+ }
71
+ if (!isBackendHealthy(backend)) {
72
+ throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime backend is currently unavailable. Try again in a moment.");
73
+ }
74
+ if (normalized && backend.id !== normalized) {
75
+ throw new AcpRuntimeError("ACP_BACKEND_MISSING", `ACP runtime backend "${normalized}" is not registered.`);
76
+ }
77
+ return backend;
78
+ }
79
+ export const __testing = {
80
+ resetAcpRuntimeBackendsForTests() {
81
+ ACP_BACKENDS_BY_ID.clear();
82
+ },
83
+ getAcpRuntimeRegistryGlobalStateForTests() {
84
+ return resolveAcpRuntimeRegistryGlobalState();
85
+ },
86
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -342,3 +342,100 @@ export class AcpGatewayAgent {
342
342
  });
343
343
  }
344
344
  }
345
+ /**
346
+ * Apply prompt prefix to a prompt request
347
+ */
348
+ export function applyPromptPrefix(request, config) {
349
+ if (!config.enabled || !config.prefix) {
350
+ return request;
351
+ }
352
+ // Check session filter
353
+ if (config.sessionIds && !config.sessionIds.includes(request.sessionId)) {
354
+ return request;
355
+ }
356
+ // Apply prefix to prompt text
357
+ const modified = { ...request };
358
+ if (modified.prompt) {
359
+ const promptText = typeof modified.prompt === "string" ? modified.prompt : JSON.stringify(modified.prompt);
360
+ modified.prompt = (config.prefix + "\n\n" + promptText);
361
+ }
362
+ return modified;
363
+ }
364
+ /**
365
+ * Cancel scoping manager to prevent cascade cancels
366
+ */
367
+ export class CancelScopingManager {
368
+ cancelHistory = [];
369
+ config;
370
+ constructor(config = {}) {
371
+ this.config = {
372
+ enabled: config.enabled ?? true,
373
+ timeWindowMs: config.timeWindowMs ?? 5000, // 5 seconds default
374
+ maxCancelsInWindow: config.maxCancelsInWindow ?? 3,
375
+ sessionIds: config.sessionIds,
376
+ };
377
+ }
378
+ /**
379
+ * Check if a cancel request should be allowed
380
+ */
381
+ shouldAllowCancel(sessionId, reason) {
382
+ if (!this.config.enabled) {
383
+ return { allowed: true };
384
+ }
385
+ // Check session filter
386
+ if (this.config.sessionIds && !this.config.sessionIds.includes(sessionId)) {
387
+ return { allowed: true };
388
+ }
389
+ const now = Date.now();
390
+ const windowStart = now - (this.config.timeWindowMs ?? 5000);
391
+ // Clean old records
392
+ this.cancelHistory = this.cancelHistory.filter((r) => r.timestamp > windowStart);
393
+ // Count recent cancels for this session
394
+ const recentCancels = this.cancelHistory.filter((r) => r.sessionId === sessionId && r.timestamp > windowStart).length;
395
+ const maxAllowed = this.config.maxCancelsInWindow ?? 3;
396
+ if (recentCancels >= maxAllowed) {
397
+ return {
398
+ allowed: false,
399
+ reason: `Cancel rate limit exceeded: ${recentCancels}/${maxAllowed} cancels in the last ${this.config.timeWindowMs}ms`,
400
+ };
401
+ }
402
+ // Record this cancel
403
+ this.cancelHistory.push({
404
+ sessionId,
405
+ timestamp: now,
406
+ reason,
407
+ });
408
+ return { allowed: true };
409
+ }
410
+ /**
411
+ * Clear cancel history
412
+ */
413
+ clear() {
414
+ this.cancelHistory = [];
415
+ }
416
+ /**
417
+ * Get statistics about cancel activity
418
+ */
419
+ getStats() {
420
+ const now = Date.now();
421
+ const windowStart = now - (this.config.timeWindowMs ?? 5000);
422
+ const recentHistory = this.cancelHistory.filter((r) => r.timestamp > windowStart);
423
+ const cancelsBySession = recentHistory.reduce((acc, r) => {
424
+ acc[r.sessionId] = (acc[r.sessionId] || 0) + 1;
425
+ return acc;
426
+ }, {});
427
+ return {
428
+ totalCancels: recentHistory.length,
429
+ cancelsBySession,
430
+ oldestCancelInWindow: recentHistory.length > 0 ? Math.min(...recentHistory.map((r) => r.timestamp)) : undefined,
431
+ };
432
+ }
433
+ }
434
+ /**
435
+ * Process a cancel notification with scoping
436
+ */
437
+ export function processCancelWithScoping(notification, manager) {
438
+ const sessionId = notification.sessionId || "unknown";
439
+ const reason = notification.reason || "user_requested";
440
+ return manager.shouldAllowCancel(sessionId, reason);
441
+ }
@@ -0,0 +1,280 @@
1
+ import { streamSimple, } from "@mariozechner/pi-ai";
2
+ import { SessionManager } from "@mariozechner/pi-coding-agent";
3
+ import { resolveSessionFilePath, resolveSessionFilePathOptions, } from "../config/sessions.js";
4
+ import { diagnosticLogger as diag } from "../logging/diagnostic.js";
5
+ import { resolveSessionAuthProfileOverride } from "./auth-profiles/session-override.js";
6
+ import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
7
+ import { ensureOpenClawModelsJson } from "./models-config.js";
8
+ import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
9
+ import { resolveModelWithRegistry } from "./pi-embedded-runner/model.js";
10
+ import { getActiveEmbeddedRunSnapshot } from "./pi-embedded-runner/runs.js";
11
+ import { mapThinkingLevel } from "./pi-embedded-runner/utils.js";
12
+ import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js";
13
+ import { stripToolResultDetails } from "./session-transcript-repair.js";
14
+ function collectTextContent(content) {
15
+ return content
16
+ .filter((part) => part.type === "text")
17
+ .map((part) => part.text)
18
+ .join("");
19
+ }
20
+ function collectThinkingContent(content) {
21
+ return content
22
+ .filter((part) => part.type === "thinking")
23
+ .map((part) => part.thinking)
24
+ .join("");
25
+ }
26
+ function buildBtwSystemPrompt() {
27
+ return [
28
+ "You are answering an ephemeral /btw side question about the current conversation.",
29
+ "Use the conversation only as background context.",
30
+ "Answer only the side question in the last user message.",
31
+ "Do not continue, resume, or complete any unfinished task from the conversation.",
32
+ "Do not emit tool calls, pseudo-tool calls, shell commands, file writes, patches, or code unless the side question explicitly asks for them.",
33
+ "Do not say you will continue the main task after answering.",
34
+ "If the question can be answered briefly, answer briefly.",
35
+ ].join("\n");
36
+ }
37
+ function buildBtwQuestionPrompt(question, inFlightPrompt) {
38
+ const lines = [
39
+ "Answer this side question only.",
40
+ "Ignore any unfinished task in the conversation while answering it.",
41
+ ];
42
+ const trimmedPrompt = inFlightPrompt?.trim();
43
+ if (trimmedPrompt) {
44
+ lines.push("", "Current in-flight main task request for background context only:", "<in_flight_main_task>", trimmedPrompt, "</in_flight_main_task>", "Do not continue or complete that task while answering the side question.");
45
+ }
46
+ lines.push("", "<btw_side_question>", question.trim(), "</btw_side_question>");
47
+ return lines.join("\n");
48
+ }
49
+ function toSimpleContextMessages(messages) {
50
+ const contextMessages = messages.filter((message) => {
51
+ if (!message || typeof message !== "object") {
52
+ return false;
53
+ }
54
+ const role = message.role;
55
+ return role === "user" || role === "assistant";
56
+ });
57
+ return stripToolResultDetails(contextMessages);
58
+ }
59
+ function resolveSimpleThinkingLevel(level) {
60
+ if (!level || level === "off") {
61
+ return undefined;
62
+ }
63
+ return mapThinkingLevel(level);
64
+ }
65
+ function resolveSessionTranscriptPath(params) {
66
+ try {
67
+ const agentId = params.sessionKey?.split(":")[1];
68
+ const pathOpts = resolveSessionFilePathOptions({
69
+ agentId,
70
+ storePath: params.storePath,
71
+ });
72
+ return resolveSessionFilePath(params.sessionId, params.sessionEntry, pathOpts);
73
+ }
74
+ catch (error) {
75
+ diag.debug(`resolveSessionTranscriptPath failed: sessionId=${params.sessionId} err=${String(error)}`);
76
+ return undefined;
77
+ }
78
+ }
79
+ async function resolveRuntimeModel(params) {
80
+ await ensureOpenClawModelsJson(params.cfg, params.agentDir);
81
+ const authStorage = discoverAuthStorage(params.agentDir);
82
+ const modelRegistry = discoverModels(authStorage, params.agentDir);
83
+ const model = resolveModelWithRegistry({
84
+ provider: params.provider,
85
+ modelId: params.model,
86
+ modelRegistry,
87
+ cfg: params.cfg,
88
+ });
89
+ if (!model) {
90
+ throw new Error(`Unknown model: ${params.provider}/${params.model}`);
91
+ }
92
+ const authProfileId = await resolveSessionAuthProfileOverride({
93
+ cfg: params.cfg,
94
+ provider: params.provider,
95
+ agentDir: params.agentDir,
96
+ sessionEntry: params.sessionEntry,
97
+ sessionStore: params.sessionStore,
98
+ sessionKey: params.sessionKey,
99
+ storePath: params.storePath,
100
+ isNewSession: params.isNewSession,
101
+ });
102
+ return {
103
+ model,
104
+ authProfileId,
105
+ authProfileIdSource: params.sessionEntry?.authProfileOverrideSource,
106
+ };
107
+ }
108
+ export async function runBtwSideQuestion(params) {
109
+ const sessionId = params.sessionEntry.sessionId?.trim();
110
+ if (!sessionId) {
111
+ throw new Error("No active session context.");
112
+ }
113
+ const sessionFile = resolveSessionTranscriptPath({
114
+ sessionId,
115
+ sessionEntry: params.sessionEntry,
116
+ sessionKey: params.sessionKey,
117
+ storePath: params.storePath,
118
+ });
119
+ if (!sessionFile) {
120
+ throw new Error("No active session transcript.");
121
+ }
122
+ const sessionManager = SessionManager.open(sessionFile);
123
+ const activeRunSnapshot = getActiveEmbeddedRunSnapshot(sessionId);
124
+ let messages = [];
125
+ let inFlightPrompt;
126
+ if (Array.isArray(activeRunSnapshot?.messages) && activeRunSnapshot.messages.length > 0) {
127
+ messages = toSimpleContextMessages(activeRunSnapshot.messages);
128
+ inFlightPrompt = activeRunSnapshot.inFlightPrompt;
129
+ }
130
+ else if (activeRunSnapshot) {
131
+ inFlightPrompt = activeRunSnapshot.inFlightPrompt;
132
+ if (activeRunSnapshot.transcriptLeafId && sessionManager.branch) {
133
+ try {
134
+ sessionManager.branch(activeRunSnapshot.transcriptLeafId);
135
+ }
136
+ catch (error) {
137
+ diag.debug(`btw snapshot leaf unavailable: sessionId=${sessionId} leaf=${activeRunSnapshot.transcriptLeafId} err=${String(error)}`);
138
+ sessionManager.resetLeaf?.();
139
+ }
140
+ }
141
+ else {
142
+ sessionManager.resetLeaf?.();
143
+ }
144
+ }
145
+ else {
146
+ const leafEntry = sessionManager.getLeafEntry?.();
147
+ if (leafEntry?.type === "message" && leafEntry.message?.role === "user") {
148
+ if (leafEntry.parentId && sessionManager.branch) {
149
+ sessionManager.branch(leafEntry.parentId);
150
+ }
151
+ else {
152
+ sessionManager.resetLeaf?.();
153
+ }
154
+ }
155
+ }
156
+ if (messages.length === 0) {
157
+ const sessionContext = sessionManager.buildSessionContext();
158
+ messages = toSimpleContextMessages(Array.isArray(sessionContext.messages) ? sessionContext.messages : []);
159
+ }
160
+ if (messages.length === 0 && !inFlightPrompt?.trim()) {
161
+ throw new Error("No active session context.");
162
+ }
163
+ const { model, authProfileId } = await resolveRuntimeModel({
164
+ cfg: params.cfg,
165
+ provider: params.provider,
166
+ model: params.model,
167
+ agentDir: params.agentDir,
168
+ sessionEntry: params.sessionEntry,
169
+ sessionStore: params.sessionStore,
170
+ sessionKey: params.sessionKey,
171
+ storePath: params.storePath,
172
+ isNewSession: params.isNewSession,
173
+ });
174
+ const apiKeyInfo = await getApiKeyForModel({
175
+ model,
176
+ cfg: params.cfg,
177
+ profileId: authProfileId,
178
+ agentDir: params.agentDir,
179
+ });
180
+ const apiKey = requireApiKey(apiKeyInfo, model.provider);
181
+ const chunker = params.opts?.onBlockReply && params.blockReplyChunking
182
+ ? new EmbeddedBlockChunker(params.blockReplyChunking)
183
+ : undefined;
184
+ let emittedBlocks = 0;
185
+ let blockEmitChain = Promise.resolve();
186
+ let answerText = "";
187
+ let reasoningText = "";
188
+ let assistantStarted = false;
189
+ let sawTextEvent = false;
190
+ const emitBlockChunk = async (text) => {
191
+ const trimmed = text.trim();
192
+ if (!trimmed || !params.opts?.onBlockReply) {
193
+ return;
194
+ }
195
+ emittedBlocks += 1;
196
+ blockEmitChain = blockEmitChain.then(async () => {
197
+ await params.opts?.onBlockReply?.({
198
+ text,
199
+ btw: { question: params.question },
200
+ });
201
+ });
202
+ await blockEmitChain;
203
+ };
204
+ const stream = streamSimple(model, {
205
+ systemPrompt: buildBtwSystemPrompt(),
206
+ messages: [
207
+ ...messages,
208
+ {
209
+ role: "user",
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: buildBtwQuestionPrompt(params.question, inFlightPrompt),
214
+ },
215
+ ],
216
+ timestamp: Date.now(),
217
+ },
218
+ ],
219
+ }, {
220
+ apiKey,
221
+ reasoning: resolveSimpleThinkingLevel(params.resolvedThinkLevel),
222
+ signal: params.opts?.abortSignal,
223
+ });
224
+ let finalEvent;
225
+ for await (const event of stream) {
226
+ finalEvent = event.type === "done" || event.type === "error" ? event : finalEvent;
227
+ if (!assistantStarted && (event.type === "text_start" || event.type === "start")) {
228
+ assistantStarted = true;
229
+ await params.opts?.onAssistantMessageStart?.();
230
+ }
231
+ if (event.type === "text_delta") {
232
+ sawTextEvent = true;
233
+ answerText += event.delta;
234
+ chunker?.append(event.delta);
235
+ if (chunker && params.resolvedBlockStreamingBreak === "text_end") {
236
+ chunker.drain({ force: false, emit: (chunk) => void emitBlockChunk(chunk) });
237
+ }
238
+ continue;
239
+ }
240
+ if (event.type === "text_end" && chunker && params.resolvedBlockStreamingBreak === "text_end") {
241
+ chunker.drain({ force: true, emit: (chunk) => void emitBlockChunk(chunk) });
242
+ continue;
243
+ }
244
+ if (event.type === "thinking_delta") {
245
+ reasoningText += event.delta;
246
+ if (params.resolvedReasoningLevel !== "off") {
247
+ await params.opts?.onReasoningStream?.({ text: reasoningText, isReasoning: true });
248
+ }
249
+ continue;
250
+ }
251
+ if (event.type === "thinking_end" && params.resolvedReasoningLevel !== "off") {
252
+ await params.opts?.onReasoningEnd?.();
253
+ }
254
+ }
255
+ if (chunker && params.resolvedBlockStreamingBreak !== "text_end" && chunker.hasBuffered()) {
256
+ chunker.drain({ force: true, emit: (chunk) => void emitBlockChunk(chunk) });
257
+ }
258
+ await blockEmitChain;
259
+ if (finalEvent?.type === "error") {
260
+ const message = collectTextContent(finalEvent.error.content);
261
+ throw new Error(message || finalEvent.error.errorMessage || "BTW failed.");
262
+ }
263
+ const finalMessage = finalEvent?.type === "done" ? finalEvent.message : undefined;
264
+ if (finalMessage) {
265
+ if (!sawTextEvent) {
266
+ answerText = collectTextContent(finalMessage.content);
267
+ }
268
+ if (!reasoningText) {
269
+ reasoningText = collectThinkingContent(finalMessage.content);
270
+ }
271
+ }
272
+ const answer = answerText.trim();
273
+ if (!answer) {
274
+ throw new Error("No BTW response generated.");
275
+ }
276
+ if (emittedBlocks > 0) {
277
+ return undefined;
278
+ }
279
+ return { text: answer };
280
+ }