@lobu/worker 3.0.9 → 3.0.12

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 (49) hide show
  1. package/dist/openclaw/session-context.d.ts.map +1 -1
  2. package/dist/openclaw/session-context.js +1 -1
  3. package/dist/openclaw/session-context.js.map +1 -1
  4. package/package.json +10 -9
  5. package/USAGE.md +0 -120
  6. package/docs/custom-base-image.md +0 -88
  7. package/scripts/worker-entrypoint.sh +0 -184
  8. package/src/__tests__/audio-provider-suggestions.test.ts +0 -198
  9. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +0 -39
  10. package/src/__tests__/embedded-tools.test.ts +0 -558
  11. package/src/__tests__/instructions.test.ts +0 -59
  12. package/src/__tests__/memory-flush-runtime.test.ts +0 -138
  13. package/src/__tests__/memory-flush.test.ts +0 -64
  14. package/src/__tests__/model-resolver.test.ts +0 -156
  15. package/src/__tests__/processor.test.ts +0 -225
  16. package/src/__tests__/setup.ts +0 -109
  17. package/src/__tests__/sse-client.test.ts +0 -48
  18. package/src/__tests__/tool-policy.test.ts +0 -269
  19. package/src/__tests__/worker.test.ts +0 -89
  20. package/src/core/error-handler.ts +0 -70
  21. package/src/core/project-scanner.ts +0 -65
  22. package/src/core/types.ts +0 -125
  23. package/src/core/url-utils.ts +0 -9
  24. package/src/core/workspace.ts +0 -138
  25. package/src/embedded/just-bash-bootstrap.ts +0 -228
  26. package/src/gateway/gateway-integration.ts +0 -287
  27. package/src/gateway/message-batcher.ts +0 -128
  28. package/src/gateway/sse-client.ts +0 -955
  29. package/src/gateway/types.ts +0 -68
  30. package/src/index.ts +0 -144
  31. package/src/instructions/builder.ts +0 -80
  32. package/src/instructions/providers.ts +0 -27
  33. package/src/modules/lifecycle.ts +0 -92
  34. package/src/openclaw/custom-tools.ts +0 -290
  35. package/src/openclaw/instructions.ts +0 -38
  36. package/src/openclaw/model-resolver.ts +0 -150
  37. package/src/openclaw/plugin-loader.ts +0 -427
  38. package/src/openclaw/processor.ts +0 -216
  39. package/src/openclaw/session-context.ts +0 -277
  40. package/src/openclaw/tool-policy.ts +0 -212
  41. package/src/openclaw/tools.ts +0 -208
  42. package/src/openclaw/worker.ts +0 -1792
  43. package/src/server.ts +0 -329
  44. package/src/shared/audio-provider-suggestions.ts +0 -132
  45. package/src/shared/processor-utils.ts +0 -33
  46. package/src/shared/provider-auth-hints.ts +0 -64
  47. package/src/shared/tool-display-config.ts +0 -75
  48. package/src/shared/tool-implementations.ts +0 -768
  49. package/tsconfig.json +0 -21
@@ -1,138 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import {
3
- createLogger,
4
- sanitizeConversationId,
5
- WorkspaceError,
6
- } from "@lobu/core";
7
- import type { WorkspaceInfo, WorkspaceSetupConfig } from "./types";
8
-
9
- const logger = createLogger("workspace");
10
-
11
- // ============================================================================
12
- // WORKSPACE UTILITIES
13
- // ============================================================================
14
-
15
- /**
16
- * Get workspace directory path for a thread
17
- */
18
- function getWorkspacePathForThread(
19
- baseDirectory: string,
20
- conversationId: string
21
- ): string {
22
- // Sanitize thread ID for filesystem
23
- const sanitizedConversationId = sanitizeConversationId(conversationId);
24
- return `${baseDirectory}/${sanitizedConversationId}`;
25
- }
26
-
27
- /**
28
- * Setup workspace directory environment variable
29
- * Used by MCP process manager
30
- */
31
- export function setupWorkspaceEnv(deploymentName: string | undefined): void {
32
- const conversationId = process.env.CONVERSATION_ID;
33
-
34
- if (conversationId) {
35
- const baseDir = process.env.WORKSPACE_DIR || "/workspace";
36
- const workspaceDir = getWorkspacePathForThread(baseDir, conversationId);
37
- process.env.WORKSPACE_DIR = workspaceDir;
38
- logger.info(`📁 Set WORKSPACE_DIR for process manager: ${workspaceDir}`);
39
- } else if (deploymentName) {
40
- // deploymentName is no longer parseable (it may be hashed/collision-resistant).
41
- logger.warn("WORKSPACE_DIR not set (missing CONVERSATION_ID env var)");
42
- }
43
- }
44
-
45
- /**
46
- * Get conversation identifier from various sources
47
- * Priority: CONVERSATION_ID > sessionKey > username
48
- */
49
- function getThreadIdentifier(sessionKey?: string, username?: string): string {
50
- const conversationId =
51
- process.env.CONVERSATION_ID || sessionKey || username || "default";
52
-
53
- return conversationId;
54
- }
55
-
56
- // ============================================================================
57
- // WORKSPACE MANAGER
58
- // ============================================================================
59
-
60
- /**
61
- * Simplified WorkspaceManager - only handles directory creation
62
- * All VCS operations (git, etc.) are handled by modules via hooks
63
- */
64
- export class WorkspaceManager {
65
- private config: WorkspaceSetupConfig;
66
- private workspaceInfo?: WorkspaceInfo;
67
-
68
- constructor(config: WorkspaceSetupConfig) {
69
- this.config = config;
70
- }
71
-
72
- /**
73
- * Setup workspace directory - creates thread-specific directory only
74
- * VCS operations are handled by module hooks (e.g., GitHub module)
75
- */
76
- async setupWorkspace(
77
- username: string,
78
- sessionKey?: string
79
- ): Promise<WorkspaceInfo> {
80
- try {
81
- // Use thread-specific directory to avoid conflicts between concurrent threads
82
- const conversationId = getThreadIdentifier(sessionKey, username);
83
-
84
- logger.info(
85
- `Setting up workspace directory for ${username}, conversation: ${conversationId}...`
86
- );
87
-
88
- const userDirectory = getWorkspacePathForThread(
89
- this.config.baseDirectory,
90
- conversationId
91
- );
92
-
93
- // Ensure base directory exists
94
- await this.ensureDirectory(this.config.baseDirectory);
95
-
96
- // Ensure user directory exists
97
- await this.ensureDirectory(userDirectory);
98
-
99
- // Create workspace info
100
- this.workspaceInfo = {
101
- baseDirectory: this.config.baseDirectory,
102
- userDirectory,
103
- };
104
-
105
- logger.info(
106
- `Workspace directory setup completed for ${username} (conversation: ${conversationId}) at ${userDirectory}`
107
- );
108
-
109
- return this.workspaceInfo;
110
- } catch (error) {
111
- throw new WorkspaceError(
112
- "setupWorkspace",
113
- `Failed to setup workspace directory`,
114
- error as Error
115
- );
116
- }
117
- }
118
-
119
- /**
120
- * Ensure directory exists
121
- */
122
- private async ensureDirectory(path: string): Promise<void> {
123
- try {
124
- await mkdir(path, { recursive: true });
125
- } catch (error: any) {
126
- if (error.code !== "EEXIST") {
127
- throw error;
128
- }
129
- }
130
- }
131
-
132
- /**
133
- * Get current working directory
134
- */
135
- getCurrentWorkingDirectory(): string {
136
- return this.workspaceInfo?.userDirectory || this.config.baseDirectory;
137
- }
138
- }
@@ -1,228 +0,0 @@
1
- /**
2
- * Worker-side just-bash bootstrap for embedded deployment mode.
3
- *
4
- * Creates a just-bash Bash instance from environment variables and wraps it
5
- * as a BashOperations interface for pi-coding-agent's bash tool.
6
- *
7
- * When nix binaries are detected on PATH (via nix-shell wrapper from gateway)
8
- * or known CLI tools (e.g. owletto) are found, they are registered as
9
- * just-bash customCommands that delegate to real exec.
10
- */
11
-
12
- import { execFile } from "node:child_process";
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- import type { BashOperations } from "@mariozechner/pi-coding-agent";
16
-
17
- const EMBEDDED_BASH_LIMITS = {
18
- maxCommandCount: 50_000,
19
- maxLoopIterations: 50_000,
20
- maxCallDepth: 50,
21
- } as const;
22
-
23
- export function buildBinaryInvocation(
24
- binaryPath: string,
25
- args: string[]
26
- ): { command: string; args: string[] } {
27
- try {
28
- const firstLine =
29
- fs.readFileSync(binaryPath, "utf8").split("\n", 1)[0] || "";
30
- if (firstLine === "#!/usr/bin/env node" || firstLine.endsWith("/node")) {
31
- return { command: "node", args: [binaryPath, ...args] };
32
- }
33
- } catch {
34
- // Fall back to executing the binary directly.
35
- }
36
-
37
- return { command: binaryPath, args };
38
- }
39
-
40
- /**
41
- * Discover binaries to register as custom commands:
42
- * 1. All executables from /nix/store/ PATH directories
43
- * 2. Known CLI tools (owletto) from anywhere on PATH
44
- */
45
- function discoverBinaries(): Map<string, string> {
46
- const binaries = new Map<string, string>();
47
- const pathDirs = (process.env.PATH || "").split(":");
48
-
49
- for (const dir of pathDirs) {
50
- if (!dir.includes("/nix/store/")) continue;
51
- try {
52
- for (const entry of fs.readdirSync(dir)) {
53
- const fullPath = path.join(dir, entry);
54
- try {
55
- fs.accessSync(fullPath, fs.constants.X_OK);
56
- if (!binaries.has(entry)) binaries.set(entry, fullPath);
57
- } catch {
58
- // not executable
59
- }
60
- }
61
- } catch {
62
- // directory not readable
63
- }
64
- }
65
-
66
- // Discover known CLI tools from full PATH
67
- for (const name of ["owletto"]) {
68
- if (binaries.has(name)) continue;
69
- for (const dir of pathDirs) {
70
- const fullPath = path.join(dir, name);
71
- try {
72
- fs.accessSync(fullPath, fs.constants.X_OK);
73
- binaries.set(name, fullPath);
74
- break;
75
- } catch {
76
- // not found
77
- }
78
- }
79
- }
80
-
81
- return binaries;
82
- }
83
-
84
- /**
85
- * Create just-bash customCommands from a map of binary name → full path.
86
- * Each custom command delegates to the real binary via child_process.execFile.
87
- */
88
- async function buildCustomCommands(
89
- binaries: Map<string, string>
90
- ): Promise<ReturnType<typeof import("just-bash").defineCommand>[]> {
91
- const { defineCommand } = await import("just-bash");
92
- const commands = [];
93
-
94
- for (const [name, binaryPath] of binaries) {
95
- commands.push(
96
- defineCommand(name, async (args: string[], ctx) => {
97
- const invocation = buildBinaryInvocation(binaryPath, args);
98
-
99
- // Convert ctx.env (Map-like) to a plain Record for child_process
100
- const envRecord: Record<string, string> = { ...process.env } as Record<
101
- string,
102
- string
103
- >;
104
- if (ctx.env && typeof ctx.env.forEach === "function") {
105
- ctx.env.forEach((v: string, k: string) => {
106
- envRecord[k] = v;
107
- });
108
- } else if (ctx.env && typeof ctx.env === "object") {
109
- Object.assign(envRecord, ctx.env);
110
- }
111
-
112
- return new Promise<{
113
- stdout: string;
114
- stderr: string;
115
- exitCode: number;
116
- }>((resolve) => {
117
- execFile(
118
- invocation.command,
119
- invocation.args,
120
- {
121
- cwd: ctx.cwd,
122
- env: envRecord,
123
- maxBuffer: 10 * 1024 * 1024,
124
- },
125
- (error, stdout, stderr) => {
126
- resolve({
127
- stdout: stdout || "",
128
- stderr: stderr || (error?.message ?? ""),
129
- exitCode: error?.code ? Number(error.code) || 1 : 0,
130
- });
131
- }
132
- );
133
- });
134
- })
135
- );
136
- }
137
-
138
- return commands;
139
- }
140
-
141
- /**
142
- * Create a BashOperations adapter backed by a just-bash Bash instance.
143
- * Reads configuration from environment variables.
144
- */
145
- export async function createEmbeddedBashOps(): Promise<BashOperations> {
146
- const { Bash, ReadWriteFs } = await import("just-bash");
147
-
148
- const workspaceDir = process.env.WORKSPACE_DIR || "/workspace";
149
- const bashFs = new ReadWriteFs({ root: workspaceDir });
150
-
151
- // Parse allowed domains from env var (set by gateway)
152
- let allowedDomains: string[] = [];
153
- if (process.env.JUST_BASH_ALLOWED_DOMAINS) {
154
- try {
155
- allowedDomains = JSON.parse(process.env.JUST_BASH_ALLOWED_DOMAINS);
156
- } catch {
157
- console.error(
158
- `[embedded] Failed to parse JUST_BASH_ALLOWED_DOMAINS: ${process.env.JUST_BASH_ALLOWED_DOMAINS}`
159
- );
160
- }
161
- }
162
-
163
- const network =
164
- allowedDomains.length > 0
165
- ? {
166
- allowedUrlPrefixes: allowedDomains.flatMap((domain: string) => [
167
- `https://${domain}/`,
168
- `http://${domain}/`,
169
- ]),
170
- allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"] as (
171
- | "GET"
172
- | "HEAD"
173
- | "POST"
174
- | "PUT"
175
- | "PATCH"
176
- | "DELETE"
177
- )[],
178
- }
179
- : undefined;
180
-
181
- // Discover nix binaries and known CLI tools, register as custom commands
182
- const binaries = discoverBinaries();
183
- const customCommands =
184
- binaries.size > 0 ? await buildCustomCommands(binaries) : [];
185
-
186
- if (binaries.size > 0) {
187
- const names = [...binaries.keys()].slice(0, 20).join(", ");
188
- const suffix = binaries.size > 20 ? `, ... (${binaries.size} total)` : "";
189
- console.log(
190
- `[embedded] Registered ${binaries.size} custom commands: ${names}${suffix}`
191
- );
192
- }
193
-
194
- const bashInstance = new Bash({
195
- fs: bashFs,
196
- cwd: "/",
197
- env: Object.fromEntries(
198
- Object.entries(process.env).filter(
199
- (entry): entry is [string, string] => entry[1] !== undefined
200
- )
201
- ),
202
- executionLimits: EMBEDDED_BASH_LIMITS,
203
- ...(network && { network }),
204
- ...(customCommands.length > 0 && { customCommands }),
205
- });
206
-
207
- return {
208
- async exec(command, cwd, { onData, signal, timeout }) {
209
- const timeoutMs =
210
- timeout !== undefined && timeout > 0 ? timeout * 1000 : undefined;
211
-
212
- const result = await bashInstance.exec(command, {
213
- cwd,
214
- signal,
215
- env: { TIMEOUT_MS: timeoutMs ? String(timeoutMs) : "" },
216
- });
217
-
218
- if (result.stdout) {
219
- onData(Buffer.from(result.stdout));
220
- }
221
- if (result.stderr) {
222
- onData(Buffer.from(result.stderr));
223
- }
224
-
225
- return { exitCode: result.exitCode };
226
- },
227
- };
228
- }
@@ -1,287 +0,0 @@
1
- /**
2
- * HTTP implementation of WorkerTransport
3
- * Sends worker responses to gateway via HTTP POST requests
4
- */
5
-
6
- import {
7
- createLogger,
8
- retryWithBackoff,
9
- type WorkerTransport,
10
- type WorkerTransportConfig,
11
- } from "@lobu/core";
12
- import type { ResponseData } from "./types";
13
-
14
- const logger = createLogger("http-worker-transport");
15
-
16
- /**
17
- * HTTP transport for worker-to-gateway communication
18
- * Implements retry logic and deduplication for streaming responses
19
- */
20
- export class HttpWorkerTransport implements WorkerTransport {
21
- private gatewayUrl: string;
22
- private workerToken: string;
23
- private userId: string;
24
- private channelId: string;
25
- private conversationId: string;
26
- private originalMessageTs: string;
27
- private botResponseTs?: string;
28
- public processedMessageIds: string[] = [];
29
- private jobId?: string;
30
- private moduleData?: Record<string, unknown>;
31
- private teamId: string;
32
- private platform?: string;
33
- private platformMetadata?: Record<string, unknown>;
34
- private accumulatedStreamContent: string[] = [];
35
- private lastStreamDelta: string = "";
36
-
37
- constructor(config: WorkerTransportConfig) {
38
- this.gatewayUrl = config.gatewayUrl;
39
- this.workerToken = config.workerToken;
40
- this.userId = config.userId;
41
- this.channelId = config.channelId;
42
- this.conversationId = config.conversationId;
43
- this.originalMessageTs = config.originalMessageTs;
44
- this.botResponseTs = config.botResponseTs;
45
- this.teamId = config.teamId;
46
- this.platform = config.platform;
47
- this.platformMetadata = config.platformMetadata;
48
- this.processedMessageIds = config.processedMessageIds || [];
49
- }
50
-
51
- setJobId(jobId: string): void {
52
- this.jobId = jobId;
53
- }
54
-
55
- setModuleData(moduleData: Record<string, unknown>): void {
56
- this.moduleData = moduleData;
57
- }
58
-
59
- async signalDone(finalDelta?: string): Promise<void> {
60
- // Send final delta if there is one
61
- if (finalDelta) {
62
- await this.sendStreamDelta(finalDelta, false, true);
63
- }
64
- await this.signalCompletion();
65
- }
66
-
67
- async sendStreamDelta(
68
- delta: string,
69
- isFullReplacement: boolean = false,
70
- isFinal: boolean = false
71
- ): Promise<void> {
72
- let actualDelta = delta;
73
-
74
- // Handle final result with deduplication
75
- if (isFinal) {
76
- logger.info(`🔍 Processing final result with deduplication`);
77
- logger.info(`Final text length: ${delta.length} chars`);
78
- const accumulatedStr = this.accumulatedStreamContent.join("");
79
- const accumulatedLength = accumulatedStr.length;
80
- logger.info(`Accumulated length: ${accumulatedLength} chars`);
81
-
82
- // Check if final result is identical to what we've already sent
83
- if (delta === accumulatedStr) {
84
- logger.info(
85
- `✅ Final result is identical to accumulated content - skipping duplicate`
86
- );
87
- return;
88
- }
89
-
90
- // Check if accumulated content is a prefix of final result
91
- if (delta.startsWith(accumulatedStr)) {
92
- // Only send the missing part
93
- actualDelta = delta.slice(accumulatedLength);
94
- if (actualDelta.length === 0) {
95
- logger.info(
96
- `✅ Final result fully contained in accumulated content - skipping`
97
- );
98
- return;
99
- }
100
- logger.info(
101
- `📝 Final result has ${actualDelta.length} new chars - sending delta only`
102
- );
103
- } else if (accumulatedLength > 0) {
104
- const normalizedFinal = this.normalizeForComparison(delta);
105
- const normalizedLastDelta = this.normalizeForComparison(
106
- this.lastStreamDelta
107
- );
108
-
109
- if (
110
- normalizedFinal.length > 0 &&
111
- normalizedFinal === normalizedLastDelta
112
- ) {
113
- logger.info(
114
- `✅ Final result matches last streamed delta (normalized) - skipping duplicate`
115
- );
116
- return;
117
- }
118
-
119
- // Content differs - log warning and send full final result
120
- logger.warn(`⚠️ Final result differs from accumulated content!`);
121
- logger.warn(
122
- `First 100 chars of accumulated: ${accumulatedStr.substring(0, 100)}`
123
- );
124
- logger.warn(`First 100 chars of final: ${delta.substring(0, 100)}`);
125
- logger.info(`📤 Sending full final result (${delta.length} chars)`);
126
- }
127
- }
128
-
129
- // Track accumulated content for deduplication using array buffer (O(1) append)
130
- if (!isFullReplacement) {
131
- this.accumulatedStreamContent.push(actualDelta);
132
- } else {
133
- this.accumulatedStreamContent = [actualDelta];
134
- }
135
- this.lastStreamDelta = actualDelta;
136
-
137
- await this.sendResponse(
138
- this.buildBaseResponse({
139
- delta: actualDelta,
140
- moduleData: this.moduleData,
141
- isFullReplacement,
142
- })
143
- );
144
- }
145
-
146
- async signalCompletion(): Promise<void> {
147
- await this.sendResponse(
148
- this.buildBaseResponse({
149
- processedMessageIds: this.processedMessageIds,
150
- moduleData: this.moduleData,
151
- })
152
- );
153
- }
154
-
155
- async signalError(error: Error, errorCode?: string): Promise<void> {
156
- await this.sendResponse(
157
- this.buildBaseResponse({
158
- error: error.message,
159
- ...(errorCode && { errorCode }),
160
- })
161
- );
162
- }
163
-
164
- async sendStatusUpdate(elapsedSeconds: number, state: string): Promise<void> {
165
- await this.sendResponse(
166
- this.buildBaseResponse({
167
- statusUpdate: { elapsedSeconds, state },
168
- })
169
- );
170
- }
171
-
172
- /**
173
- * Build base response payload with common fields shared across all response types
174
- */
175
- private buildBaseResponse(
176
- additionalFields?: Partial<ResponseData>
177
- ): ResponseData {
178
- return {
179
- messageId: this.originalMessageTs,
180
- channelId: this.channelId,
181
- conversationId: this.conversationId,
182
- userId: this.userId,
183
- teamId: this.teamId,
184
- timestamp: Date.now(),
185
- originalMessageId: this.originalMessageTs,
186
- botResponseId: this.botResponseTs,
187
- ...additionalFields,
188
- };
189
- }
190
-
191
- /**
192
- * Build exec response payload with exec-specific fields
193
- */
194
- private buildExecResponse(
195
- execId: string,
196
- additionalFields: Partial<ResponseData>
197
- ): ResponseData {
198
- return this.buildBaseResponse({ execId, ...additionalFields });
199
- }
200
-
201
- /**
202
- * Send exec output (stdout/stderr) to gateway
203
- */
204
- async sendExecOutput(
205
- execId: string,
206
- stream: "stdout" | "stderr",
207
- content: string
208
- ): Promise<void> {
209
- await this.sendResponse(
210
- this.buildExecResponse(execId, { delta: content, execStream: stream })
211
- );
212
- }
213
-
214
- /**
215
- * Send exec completion to gateway
216
- */
217
- async sendExecComplete(execId: string, exitCode: number): Promise<void> {
218
- await this.sendResponse(
219
- this.buildExecResponse(execId, { execExitCode: exitCode })
220
- );
221
- }
222
-
223
- /**
224
- * Send exec error to gateway
225
- */
226
- async sendExecError(execId: string, errorMessage: string): Promise<void> {
227
- await this.sendResponse(
228
- this.buildExecResponse(execId, { error: errorMessage })
229
- );
230
- }
231
-
232
- private async sendResponse(data: ResponseData): Promise<void> {
233
- const responseUrl = `${this.gatewayUrl}/worker/response`;
234
- const basePayload = {
235
- ...data,
236
- ...(this.platform && !data.platform ? { platform: this.platform } : {}),
237
- ...(!data.platformMetadata && this.platformMetadata
238
- ? { platformMetadata: this.platformMetadata }
239
- : {}),
240
- };
241
- const payload = this.jobId
242
- ? { jobId: this.jobId, ...basePayload }
243
- : basePayload;
244
-
245
- await retryWithBackoff(
246
- async () => {
247
- logger.info(
248
- `[WORKER-HTTP] Sending to ${responseUrl}: ${JSON.stringify(payload).substring(0, 500)}`
249
- );
250
- if (payload.delta) {
251
- logger.info(
252
- `[WORKER-HTTP] Stream delta payload: deltaLength=${payload.delta?.length}`
253
- );
254
- }
255
-
256
- const response = await fetch(responseUrl, {
257
- method: "POST",
258
- headers: {
259
- Authorization: `Bearer ${this.workerToken}`,
260
- "Content-Type": "application/json",
261
- },
262
- body: JSON.stringify(payload),
263
- signal: AbortSignal.timeout(30_000),
264
- });
265
-
266
- if (!response.ok) {
267
- throw new Error(
268
- `Failed to send response to dispatcher: ${response.status} ${response.statusText}`
269
- );
270
- }
271
-
272
- logger.debug("Response sent to dispatcher successfully");
273
- },
274
- {
275
- maxRetries: 2,
276
- baseDelay: 1000,
277
- onRetry: (attempt, error) => {
278
- logger.warn(`Failed to send response (attempt ${attempt}/2):`, error);
279
- },
280
- }
281
- );
282
- }
283
-
284
- private normalizeForComparison(text: string): string {
285
- return text.replace(/\r\n/g, "\n").trim();
286
- }
287
- }