@lobu/worker 3.0.9 → 3.0.13
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.
- package/dist/openclaw/session-context.d.ts.map +1 -1
- package/dist/openclaw/session-context.js +1 -1
- package/dist/openclaw/session-context.js.map +1 -1
- package/package.json +10 -9
- package/USAGE.md +0 -120
- package/docs/custom-base-image.md +0 -88
- package/scripts/worker-entrypoint.sh +0 -184
- package/src/__tests__/audio-provider-suggestions.test.ts +0 -198
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +0 -39
- package/src/__tests__/embedded-tools.test.ts +0 -558
- package/src/__tests__/instructions.test.ts +0 -59
- package/src/__tests__/memory-flush-runtime.test.ts +0 -138
- package/src/__tests__/memory-flush.test.ts +0 -64
- package/src/__tests__/model-resolver.test.ts +0 -156
- package/src/__tests__/processor.test.ts +0 -225
- package/src/__tests__/setup.ts +0 -109
- package/src/__tests__/sse-client.test.ts +0 -48
- package/src/__tests__/tool-policy.test.ts +0 -269
- package/src/__tests__/worker.test.ts +0 -89
- package/src/core/error-handler.ts +0 -70
- package/src/core/project-scanner.ts +0 -65
- package/src/core/types.ts +0 -125
- package/src/core/url-utils.ts +0 -9
- package/src/core/workspace.ts +0 -138
- package/src/embedded/just-bash-bootstrap.ts +0 -228
- package/src/gateway/gateway-integration.ts +0 -287
- package/src/gateway/message-batcher.ts +0 -128
- package/src/gateway/sse-client.ts +0 -955
- package/src/gateway/types.ts +0 -68
- package/src/index.ts +0 -144
- package/src/instructions/builder.ts +0 -80
- package/src/instructions/providers.ts +0 -27
- package/src/modules/lifecycle.ts +0 -92
- package/src/openclaw/custom-tools.ts +0 -290
- package/src/openclaw/instructions.ts +0 -38
- package/src/openclaw/model-resolver.ts +0 -150
- package/src/openclaw/plugin-loader.ts +0 -427
- package/src/openclaw/processor.ts +0 -216
- package/src/openclaw/session-context.ts +0 -277
- package/src/openclaw/tool-policy.ts +0 -212
- package/src/openclaw/tools.ts +0 -208
- package/src/openclaw/worker.ts +0 -1792
- package/src/server.ts +0 -329
- package/src/shared/audio-provider-suggestions.ts +0 -132
- package/src/shared/processor-utils.ts +0 -33
- package/src/shared/provider-auth-hints.ts +0 -64
- package/src/shared/tool-display-config.ts +0 -75
- package/src/shared/tool-implementations.ts +0 -768
- package/tsconfig.json +0 -21
package/src/core/workspace.ts
DELETED
|
@@ -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
|
-
}
|