@fangyb/ahchat-bridge 0.1.7 → 0.1.8
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/cli.js +4371 -260
- package/dist/index.js +4176 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,40 +1,4304 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
AgentManager,
|
|
4
|
-
AskQuestionRegistry,
|
|
5
|
-
GroupRegistry,
|
|
6
|
-
HttpAgentRegistry,
|
|
7
|
-
ServerConnector,
|
|
8
|
-
SessionStore,
|
|
9
|
-
acquireLock,
|
|
10
|
-
createGroupTaskDispatchHandler,
|
|
11
|
-
createModuleLogger,
|
|
12
|
-
createTaskDispatchHandler,
|
|
13
|
-
ensureDir,
|
|
14
|
-
formatAnswerForSDK,
|
|
15
|
-
listModels,
|
|
16
|
-
loadBridgeConfig,
|
|
17
|
-
wsMetrics
|
|
18
|
-
} from "./chunk-7SODRWIG.js";
|
|
19
2
|
|
|
20
3
|
// src/cli.ts
|
|
21
4
|
import cac from "cac";
|
|
22
5
|
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var DEFAULT_QUERY_CONFIG = {
|
|
12
|
+
maxActive: 50,
|
|
13
|
+
idleTimeoutMs: 6e4,
|
|
14
|
+
evictionIntervalMs: 3e4,
|
|
15
|
+
statusReportIntervalMs: 6e4
|
|
16
|
+
};
|
|
17
|
+
function readEnvString(name, fallback) {
|
|
18
|
+
const v = process.env[name];
|
|
19
|
+
return v && v.length > 0 ? v : fallback;
|
|
20
|
+
}
|
|
21
|
+
function readEnvInt(name, fallback) {
|
|
22
|
+
const v = process.env[name];
|
|
23
|
+
if (!v || v.length === 0) return fallback;
|
|
24
|
+
const n = Number.parseInt(v, 10);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
26
|
+
}
|
|
27
|
+
function generateStableBridgeId() {
|
|
28
|
+
const raw = `${os.hostname()}:${os.userInfo().username}`;
|
|
29
|
+
const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 12);
|
|
30
|
+
return `bridge_${hash}`;
|
|
31
|
+
}
|
|
32
|
+
function generateStableBridgeToken() {
|
|
33
|
+
const raw = `${os.hostname()}:${os.userInfo().username}:ahchat-bridge-token`;
|
|
34
|
+
return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
35
|
+
}
|
|
36
|
+
function tryReadJsonConfig(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(filePath)) return {};
|
|
39
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
42
|
+
return parsed;
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function mergeQueryConfig(file) {
|
|
48
|
+
const q = file.queryConfig;
|
|
49
|
+
return {
|
|
50
|
+
maxActive: readEnvInt("AHCHAT_BRIDGE_MAX_ACTIVE", q?.maxActive ?? DEFAULT_QUERY_CONFIG.maxActive),
|
|
51
|
+
idleTimeoutMs: readEnvInt(
|
|
52
|
+
"AHCHAT_BRIDGE_IDLE_TIMEOUT_MS",
|
|
53
|
+
q?.idleTimeoutMs ?? DEFAULT_QUERY_CONFIG.idleTimeoutMs
|
|
54
|
+
),
|
|
55
|
+
evictionIntervalMs: readEnvInt(
|
|
56
|
+
"AHCHAT_BRIDGE_EVICTION_INTERVAL_MS",
|
|
57
|
+
q?.evictionIntervalMs ?? DEFAULT_QUERY_CONFIG.evictionIntervalMs
|
|
58
|
+
),
|
|
59
|
+
statusReportIntervalMs: readEnvInt(
|
|
60
|
+
"AHCHAT_BRIDGE_STATUS_REPORT_MS",
|
|
61
|
+
q?.statusReportIntervalMs ?? DEFAULT_QUERY_CONFIG.statusReportIntervalMs
|
|
62
|
+
)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function loadBridgeConfig() {
|
|
66
|
+
const dataDir = readEnvString(
|
|
67
|
+
"AHCHAT_DATA_DIR",
|
|
68
|
+
path.join(os.homedir(), ".ahchat")
|
|
69
|
+
);
|
|
70
|
+
const fileConfig = tryReadJsonConfig(path.join(dataDir, "bridge.json"));
|
|
71
|
+
return {
|
|
72
|
+
serverUrl: readEnvString(
|
|
73
|
+
"AHCHAT_BRIDGE_SERVER_URL",
|
|
74
|
+
fileConfig.serverUrl ?? "ws://localhost:3001/ws/bridge"
|
|
75
|
+
),
|
|
76
|
+
bridgeId: readEnvString(
|
|
77
|
+
"AHCHAT_BRIDGE_ID",
|
|
78
|
+
fileConfig.bridgeId ?? generateStableBridgeId()
|
|
79
|
+
),
|
|
80
|
+
bridgeToken: readEnvString(
|
|
81
|
+
"AHCHAT_BRIDGE_TOKEN",
|
|
82
|
+
fileConfig.bridgeToken ?? generateStableBridgeToken()
|
|
83
|
+
),
|
|
84
|
+
logLevel: readEnvString(
|
|
85
|
+
"AHCHAT_LOG_LEVEL",
|
|
86
|
+
fileConfig.logLevel ?? "INFO"
|
|
87
|
+
),
|
|
88
|
+
dataDir,
|
|
89
|
+
dbPath: readEnvString(
|
|
90
|
+
"AHCHAT_DB_PATH",
|
|
91
|
+
fileConfig.dbPath ?? path.join(dataDir, "data.db")
|
|
92
|
+
),
|
|
93
|
+
serverApiUrl: readEnvString(
|
|
94
|
+
"AHCHAT_SERVER_API_URL",
|
|
95
|
+
fileConfig.serverApiUrl ?? "http://localhost:3001"
|
|
96
|
+
),
|
|
97
|
+
claudeConfigDir: readEnvString(
|
|
98
|
+
"AHCHAT_CLAUDE_CONFIG_DIR",
|
|
99
|
+
fileConfig.claudeConfigDir ?? path.join(dataDir, "claude-config")
|
|
100
|
+
),
|
|
101
|
+
queryConfig: mergeQueryConfig(fileConfig)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function ensureDir(dirPath) {
|
|
105
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/logger.ts
|
|
109
|
+
import os3 from "os";
|
|
110
|
+
import path3 from "path";
|
|
111
|
+
|
|
112
|
+
// ../logger/src/types.ts
|
|
113
|
+
var LOG_LEVEL_VALUE = {
|
|
114
|
+
TRACE: 0,
|
|
115
|
+
DEBUG: 1,
|
|
116
|
+
INFO: 2,
|
|
117
|
+
WARN: 3,
|
|
118
|
+
ERROR: 4,
|
|
119
|
+
FATAL: 5
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ../logger/src/logger.ts
|
|
123
|
+
function serializeError(err) {
|
|
124
|
+
if (err instanceof Error) {
|
|
125
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
126
|
+
}
|
|
127
|
+
if (typeof err === "object" && err !== null && "message" in err) {
|
|
128
|
+
const o = err;
|
|
129
|
+
return {
|
|
130
|
+
name: typeof o.name === "string" ? o.name : "Error",
|
|
131
|
+
message: String(o.message),
|
|
132
|
+
...typeof o.stack === "string" ? { stack: o.stack } : {}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { name: "Error", message: String(err) };
|
|
136
|
+
}
|
|
137
|
+
function stripReservedFields(data) {
|
|
138
|
+
if (!data) return void 0;
|
|
139
|
+
const rest = { ...data };
|
|
140
|
+
delete rest.traceId;
|
|
141
|
+
delete rest.error;
|
|
142
|
+
return Object.keys(rest).length > 0 ? rest : void 0;
|
|
143
|
+
}
|
|
144
|
+
var Logger = class {
|
|
145
|
+
config;
|
|
146
|
+
levelValue;
|
|
147
|
+
constructor(config) {
|
|
148
|
+
this.config = config;
|
|
149
|
+
this.levelValue = LOG_LEVEL_VALUE[config.level];
|
|
150
|
+
}
|
|
151
|
+
trace(msg, data) {
|
|
152
|
+
this.log("TRACE", msg, data);
|
|
153
|
+
}
|
|
154
|
+
debug(msg, data) {
|
|
155
|
+
this.log("DEBUG", msg, data);
|
|
156
|
+
}
|
|
157
|
+
info(msg, data) {
|
|
158
|
+
this.log("INFO", msg, data);
|
|
159
|
+
}
|
|
160
|
+
warn(msg, data) {
|
|
161
|
+
this.log("WARN", msg, data);
|
|
162
|
+
}
|
|
163
|
+
error(msg, data) {
|
|
164
|
+
this.log("ERROR", msg, data);
|
|
165
|
+
}
|
|
166
|
+
fatal(msg, data) {
|
|
167
|
+
this.log("FATAL", msg, data);
|
|
168
|
+
}
|
|
169
|
+
log(level, msg, data) {
|
|
170
|
+
if (LOG_LEVEL_VALUE[level] < this.levelValue) return;
|
|
171
|
+
const traceId = typeof data?.traceId === "string" ? data.traceId : void 0;
|
|
172
|
+
const hasError = data && "error" in data && data.error !== void 0;
|
|
173
|
+
const error = hasError ? serializeError(data.error) : void 0;
|
|
174
|
+
const rest = stripReservedFields(data);
|
|
175
|
+
const entry = {
|
|
176
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
177
|
+
level,
|
|
178
|
+
source: this.config.source,
|
|
179
|
+
module: this.config.module,
|
|
180
|
+
msg,
|
|
181
|
+
...traceId ? { traceId } : {},
|
|
182
|
+
...error ? { error } : {},
|
|
183
|
+
...rest ? { data: rest } : {}
|
|
184
|
+
};
|
|
185
|
+
for (const transport of this.config.transports) {
|
|
186
|
+
transport(entry);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ../logger/src/formatters/json.ts
|
|
192
|
+
var jsonFormatter = (entry) => JSON.stringify(entry);
|
|
193
|
+
|
|
194
|
+
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
195
|
+
var ANSI_BACKGROUND_OFFSET = 10;
|
|
196
|
+
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
197
|
+
var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
|
|
198
|
+
var wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`;
|
|
199
|
+
var styles = {
|
|
200
|
+
modifier: {
|
|
201
|
+
reset: [0, 0],
|
|
202
|
+
// 21 isn't widely supported and 22 does the same thing
|
|
203
|
+
bold: [1, 22],
|
|
204
|
+
dim: [2, 22],
|
|
205
|
+
italic: [3, 23],
|
|
206
|
+
underline: [4, 24],
|
|
207
|
+
overline: [53, 55],
|
|
208
|
+
inverse: [7, 27],
|
|
209
|
+
hidden: [8, 28],
|
|
210
|
+
strikethrough: [9, 29]
|
|
211
|
+
},
|
|
212
|
+
color: {
|
|
213
|
+
black: [30, 39],
|
|
214
|
+
red: [31, 39],
|
|
215
|
+
green: [32, 39],
|
|
216
|
+
yellow: [33, 39],
|
|
217
|
+
blue: [34, 39],
|
|
218
|
+
magenta: [35, 39],
|
|
219
|
+
cyan: [36, 39],
|
|
220
|
+
white: [37, 39],
|
|
221
|
+
// Bright color
|
|
222
|
+
blackBright: [90, 39],
|
|
223
|
+
gray: [90, 39],
|
|
224
|
+
// Alias of `blackBright`
|
|
225
|
+
grey: [90, 39],
|
|
226
|
+
// Alias of `blackBright`
|
|
227
|
+
redBright: [91, 39],
|
|
228
|
+
greenBright: [92, 39],
|
|
229
|
+
yellowBright: [93, 39],
|
|
230
|
+
blueBright: [94, 39],
|
|
231
|
+
magentaBright: [95, 39],
|
|
232
|
+
cyanBright: [96, 39],
|
|
233
|
+
whiteBright: [97, 39]
|
|
234
|
+
},
|
|
235
|
+
bgColor: {
|
|
236
|
+
bgBlack: [40, 49],
|
|
237
|
+
bgRed: [41, 49],
|
|
238
|
+
bgGreen: [42, 49],
|
|
239
|
+
bgYellow: [43, 49],
|
|
240
|
+
bgBlue: [44, 49],
|
|
241
|
+
bgMagenta: [45, 49],
|
|
242
|
+
bgCyan: [46, 49],
|
|
243
|
+
bgWhite: [47, 49],
|
|
244
|
+
// Bright color
|
|
245
|
+
bgBlackBright: [100, 49],
|
|
246
|
+
bgGray: [100, 49],
|
|
247
|
+
// Alias of `bgBlackBright`
|
|
248
|
+
bgGrey: [100, 49],
|
|
249
|
+
// Alias of `bgBlackBright`
|
|
250
|
+
bgRedBright: [101, 49],
|
|
251
|
+
bgGreenBright: [102, 49],
|
|
252
|
+
bgYellowBright: [103, 49],
|
|
253
|
+
bgBlueBright: [104, 49],
|
|
254
|
+
bgMagentaBright: [105, 49],
|
|
255
|
+
bgCyanBright: [106, 49],
|
|
256
|
+
bgWhiteBright: [107, 49]
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var modifierNames = Object.keys(styles.modifier);
|
|
260
|
+
var foregroundColorNames = Object.keys(styles.color);
|
|
261
|
+
var backgroundColorNames = Object.keys(styles.bgColor);
|
|
262
|
+
var colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
|
263
|
+
function assembleStyles() {
|
|
264
|
+
const codes = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const [groupName, group] of Object.entries(styles)) {
|
|
266
|
+
for (const [styleName, style] of Object.entries(group)) {
|
|
267
|
+
styles[styleName] = {
|
|
268
|
+
open: `\x1B[${style[0]}m`,
|
|
269
|
+
close: `\x1B[${style[1]}m`
|
|
270
|
+
};
|
|
271
|
+
group[styleName] = styles[styleName];
|
|
272
|
+
codes.set(style[0], style[1]);
|
|
273
|
+
}
|
|
274
|
+
Object.defineProperty(styles, groupName, {
|
|
275
|
+
value: group,
|
|
276
|
+
enumerable: false
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
Object.defineProperty(styles, "codes", {
|
|
280
|
+
value: codes,
|
|
281
|
+
enumerable: false
|
|
282
|
+
});
|
|
283
|
+
styles.color.close = "\x1B[39m";
|
|
284
|
+
styles.bgColor.close = "\x1B[49m";
|
|
285
|
+
styles.color.ansi = wrapAnsi16();
|
|
286
|
+
styles.color.ansi256 = wrapAnsi256();
|
|
287
|
+
styles.color.ansi16m = wrapAnsi16m();
|
|
288
|
+
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
|
289
|
+
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
|
290
|
+
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
|
291
|
+
Object.defineProperties(styles, {
|
|
292
|
+
rgbToAnsi256: {
|
|
293
|
+
value(red, green, blue) {
|
|
294
|
+
if (red === green && green === blue) {
|
|
295
|
+
if (red < 8) {
|
|
296
|
+
return 16;
|
|
297
|
+
}
|
|
298
|
+
if (red > 248) {
|
|
299
|
+
return 231;
|
|
300
|
+
}
|
|
301
|
+
return Math.round((red - 8) / 247 * 24) + 232;
|
|
302
|
+
}
|
|
303
|
+
return 16 + 36 * Math.round(red / 255 * 5) + 6 * Math.round(green / 255 * 5) + Math.round(blue / 255 * 5);
|
|
304
|
+
},
|
|
305
|
+
enumerable: false
|
|
306
|
+
},
|
|
307
|
+
hexToRgb: {
|
|
308
|
+
value(hex) {
|
|
309
|
+
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
|
310
|
+
if (!matches) {
|
|
311
|
+
return [0, 0, 0];
|
|
312
|
+
}
|
|
313
|
+
let [colorString] = matches;
|
|
314
|
+
if (colorString.length === 3) {
|
|
315
|
+
colorString = [...colorString].map((character) => character + character).join("");
|
|
316
|
+
}
|
|
317
|
+
const integer = Number.parseInt(colorString, 16);
|
|
318
|
+
return [
|
|
319
|
+
/* eslint-disable no-bitwise */
|
|
320
|
+
integer >> 16 & 255,
|
|
321
|
+
integer >> 8 & 255,
|
|
322
|
+
integer & 255
|
|
323
|
+
/* eslint-enable no-bitwise */
|
|
324
|
+
];
|
|
325
|
+
},
|
|
326
|
+
enumerable: false
|
|
327
|
+
},
|
|
328
|
+
hexToAnsi256: {
|
|
329
|
+
value: (hex) => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
|
330
|
+
enumerable: false
|
|
331
|
+
},
|
|
332
|
+
ansi256ToAnsi: {
|
|
333
|
+
value(code) {
|
|
334
|
+
if (code < 8) {
|
|
335
|
+
return 30 + code;
|
|
336
|
+
}
|
|
337
|
+
if (code < 16) {
|
|
338
|
+
return 90 + (code - 8);
|
|
339
|
+
}
|
|
340
|
+
let red;
|
|
341
|
+
let green;
|
|
342
|
+
let blue;
|
|
343
|
+
if (code >= 232) {
|
|
344
|
+
red = ((code - 232) * 10 + 8) / 255;
|
|
345
|
+
green = red;
|
|
346
|
+
blue = red;
|
|
347
|
+
} else {
|
|
348
|
+
code -= 16;
|
|
349
|
+
const remainder = code % 36;
|
|
350
|
+
red = Math.floor(code / 36) / 5;
|
|
351
|
+
green = Math.floor(remainder / 6) / 5;
|
|
352
|
+
blue = remainder % 6 / 5;
|
|
353
|
+
}
|
|
354
|
+
const value = Math.max(red, green, blue) * 2;
|
|
355
|
+
if (value === 0) {
|
|
356
|
+
return 30;
|
|
357
|
+
}
|
|
358
|
+
let result = 30 + (Math.round(blue) << 2 | Math.round(green) << 1 | Math.round(red));
|
|
359
|
+
if (value === 2) {
|
|
360
|
+
result += 60;
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
},
|
|
364
|
+
enumerable: false
|
|
365
|
+
},
|
|
366
|
+
rgbToAnsi: {
|
|
367
|
+
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
|
368
|
+
enumerable: false
|
|
369
|
+
},
|
|
370
|
+
hexToAnsi: {
|
|
371
|
+
value: (hex) => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
|
372
|
+
enumerable: false
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
return styles;
|
|
376
|
+
}
|
|
377
|
+
var ansiStyles = assembleStyles();
|
|
378
|
+
var ansi_styles_default = ansiStyles;
|
|
379
|
+
|
|
380
|
+
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
|
|
381
|
+
import process2 from "process";
|
|
382
|
+
import os2 from "os";
|
|
383
|
+
import tty from "tty";
|
|
384
|
+
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.argv) {
|
|
385
|
+
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
|
|
386
|
+
const position = argv.indexOf(prefix + flag);
|
|
387
|
+
const terminatorPosition = argv.indexOf("--");
|
|
388
|
+
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
389
|
+
}
|
|
390
|
+
var { env } = process2;
|
|
391
|
+
var flagForceColor;
|
|
392
|
+
if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
|
|
393
|
+
flagForceColor = 0;
|
|
394
|
+
} else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
|
|
395
|
+
flagForceColor = 1;
|
|
396
|
+
}
|
|
397
|
+
function envForceColor() {
|
|
398
|
+
if ("FORCE_COLOR" in env) {
|
|
399
|
+
if (env.FORCE_COLOR === "true") {
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
if (env.FORCE_COLOR === "false") {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function translateLevel(level) {
|
|
409
|
+
if (level === 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
level,
|
|
414
|
+
hasBasic: true,
|
|
415
|
+
has256: level >= 2,
|
|
416
|
+
has16m: level >= 3
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
420
|
+
const noFlagForceColor = envForceColor();
|
|
421
|
+
if (noFlagForceColor !== void 0) {
|
|
422
|
+
flagForceColor = noFlagForceColor;
|
|
423
|
+
}
|
|
424
|
+
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
|
425
|
+
if (forceColor === 0) {
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
if (sniffFlags) {
|
|
429
|
+
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
|
|
430
|
+
return 3;
|
|
431
|
+
}
|
|
432
|
+
if (hasFlag("color=256")) {
|
|
433
|
+
return 2;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if ("TF_BUILD" in env && "AGENT_NAME" in env) {
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
if (haveStream && !streamIsTTY && forceColor === void 0) {
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
const min = forceColor || 0;
|
|
443
|
+
if (env.TERM === "dumb") {
|
|
444
|
+
return min;
|
|
445
|
+
}
|
|
446
|
+
if (process2.platform === "win32") {
|
|
447
|
+
const osRelease = os2.release().split(".");
|
|
448
|
+
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
449
|
+
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
450
|
+
}
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
if ("CI" in env) {
|
|
454
|
+
if (["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)) {
|
|
455
|
+
return 3;
|
|
456
|
+
}
|
|
457
|
+
if (["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") {
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
return min;
|
|
461
|
+
}
|
|
462
|
+
if ("TEAMCITY_VERSION" in env) {
|
|
463
|
+
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
464
|
+
}
|
|
465
|
+
if (env.COLORTERM === "truecolor") {
|
|
466
|
+
return 3;
|
|
467
|
+
}
|
|
468
|
+
if (env.TERM === "xterm-kitty") {
|
|
469
|
+
return 3;
|
|
470
|
+
}
|
|
471
|
+
if (env.TERM === "xterm-ghostty") {
|
|
472
|
+
return 3;
|
|
473
|
+
}
|
|
474
|
+
if (env.TERM === "wezterm") {
|
|
475
|
+
return 3;
|
|
476
|
+
}
|
|
477
|
+
if ("TERM_PROGRAM" in env) {
|
|
478
|
+
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
479
|
+
switch (env.TERM_PROGRAM) {
|
|
480
|
+
case "iTerm.app": {
|
|
481
|
+
return version >= 3 ? 3 : 2;
|
|
482
|
+
}
|
|
483
|
+
case "Apple_Terminal": {
|
|
484
|
+
return 2;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (/-256(color)?$/i.test(env.TERM)) {
|
|
489
|
+
return 2;
|
|
490
|
+
}
|
|
491
|
+
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
492
|
+
return 1;
|
|
493
|
+
}
|
|
494
|
+
if ("COLORTERM" in env) {
|
|
495
|
+
return 1;
|
|
496
|
+
}
|
|
497
|
+
return min;
|
|
498
|
+
}
|
|
499
|
+
function createSupportsColor(stream, options = {}) {
|
|
500
|
+
const level = _supportsColor(stream, {
|
|
501
|
+
streamIsTTY: stream && stream.isTTY,
|
|
502
|
+
...options
|
|
503
|
+
});
|
|
504
|
+
return translateLevel(level);
|
|
505
|
+
}
|
|
506
|
+
var supportsColor = {
|
|
507
|
+
stdout: createSupportsColor({ isTTY: tty.isatty(1) }),
|
|
508
|
+
stderr: createSupportsColor({ isTTY: tty.isatty(2) })
|
|
509
|
+
};
|
|
510
|
+
var supports_color_default = supportsColor;
|
|
511
|
+
|
|
512
|
+
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/utilities.js
|
|
513
|
+
function stringReplaceAll(string, substring, replacer) {
|
|
514
|
+
let index = string.indexOf(substring);
|
|
515
|
+
if (index === -1) {
|
|
516
|
+
return string;
|
|
517
|
+
}
|
|
518
|
+
const substringLength = substring.length;
|
|
519
|
+
let endIndex = 0;
|
|
520
|
+
let returnValue = "";
|
|
521
|
+
do {
|
|
522
|
+
returnValue += string.slice(endIndex, index) + substring + replacer;
|
|
523
|
+
endIndex = index + substringLength;
|
|
524
|
+
index = string.indexOf(substring, endIndex);
|
|
525
|
+
} while (index !== -1);
|
|
526
|
+
returnValue += string.slice(endIndex);
|
|
527
|
+
return returnValue;
|
|
528
|
+
}
|
|
529
|
+
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
530
|
+
let endIndex = 0;
|
|
531
|
+
let returnValue = "";
|
|
532
|
+
do {
|
|
533
|
+
const gotCR = string[index - 1] === "\r";
|
|
534
|
+
returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? "\r\n" : "\n") + postfix;
|
|
535
|
+
endIndex = index + 1;
|
|
536
|
+
index = string.indexOf("\n", endIndex);
|
|
537
|
+
} while (index !== -1);
|
|
538
|
+
returnValue += string.slice(endIndex);
|
|
539
|
+
return returnValue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/index.js
|
|
543
|
+
var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
|
|
544
|
+
var GENERATOR = /* @__PURE__ */ Symbol("GENERATOR");
|
|
545
|
+
var STYLER = /* @__PURE__ */ Symbol("STYLER");
|
|
546
|
+
var IS_EMPTY = /* @__PURE__ */ Symbol("IS_EMPTY");
|
|
547
|
+
var levelMapping = [
|
|
548
|
+
"ansi",
|
|
549
|
+
"ansi",
|
|
550
|
+
"ansi256",
|
|
551
|
+
"ansi16m"
|
|
552
|
+
];
|
|
553
|
+
var styles2 = /* @__PURE__ */ Object.create(null);
|
|
554
|
+
var applyOptions = (object, options = {}) => {
|
|
555
|
+
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
|
|
556
|
+
throw new Error("The `level` option should be an integer from 0 to 3");
|
|
557
|
+
}
|
|
558
|
+
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
|
559
|
+
object.level = options.level === void 0 ? colorLevel : options.level;
|
|
560
|
+
};
|
|
561
|
+
var chalkFactory = (options) => {
|
|
562
|
+
const chalk2 = (...strings) => strings.join(" ");
|
|
563
|
+
applyOptions(chalk2, options);
|
|
564
|
+
Object.setPrototypeOf(chalk2, createChalk.prototype);
|
|
565
|
+
return chalk2;
|
|
566
|
+
};
|
|
567
|
+
function createChalk(options) {
|
|
568
|
+
return chalkFactory(options);
|
|
569
|
+
}
|
|
570
|
+
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
|
571
|
+
for (const [styleName, style] of Object.entries(ansi_styles_default)) {
|
|
572
|
+
styles2[styleName] = {
|
|
573
|
+
get() {
|
|
574
|
+
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
|
575
|
+
Object.defineProperty(this, styleName, { value: builder });
|
|
576
|
+
return builder;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
styles2.visible = {
|
|
581
|
+
get() {
|
|
582
|
+
const builder = createBuilder(this, this[STYLER], true);
|
|
583
|
+
Object.defineProperty(this, "visible", { value: builder });
|
|
584
|
+
return builder;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
var getModelAnsi = (model, level, type, ...arguments_) => {
|
|
588
|
+
if (model === "rgb") {
|
|
589
|
+
if (level === "ansi16m") {
|
|
590
|
+
return ansi_styles_default[type].ansi16m(...arguments_);
|
|
591
|
+
}
|
|
592
|
+
if (level === "ansi256") {
|
|
593
|
+
return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
|
|
594
|
+
}
|
|
595
|
+
return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
|
|
596
|
+
}
|
|
597
|
+
if (model === "hex") {
|
|
598
|
+
return getModelAnsi("rgb", level, type, ...ansi_styles_default.hexToRgb(...arguments_));
|
|
599
|
+
}
|
|
600
|
+
return ansi_styles_default[type][model](...arguments_);
|
|
601
|
+
};
|
|
602
|
+
var usedModels = ["rgb", "hex", "ansi256"];
|
|
603
|
+
for (const model of usedModels) {
|
|
604
|
+
styles2[model] = {
|
|
605
|
+
get() {
|
|
606
|
+
const { level } = this;
|
|
607
|
+
return function(...arguments_) {
|
|
608
|
+
const styler = createStyler(getModelAnsi(model, levelMapping[level], "color", ...arguments_), ansi_styles_default.color.close, this[STYLER]);
|
|
609
|
+
return createBuilder(this, styler, this[IS_EMPTY]);
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const bgModel = "bg" + model[0].toUpperCase() + model.slice(1);
|
|
614
|
+
styles2[bgModel] = {
|
|
615
|
+
get() {
|
|
616
|
+
const { level } = this;
|
|
617
|
+
return function(...arguments_) {
|
|
618
|
+
const styler = createStyler(getModelAnsi(model, levelMapping[level], "bgColor", ...arguments_), ansi_styles_default.bgColor.close, this[STYLER]);
|
|
619
|
+
return createBuilder(this, styler, this[IS_EMPTY]);
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
var proto = Object.defineProperties(() => {
|
|
625
|
+
}, {
|
|
626
|
+
...styles2,
|
|
627
|
+
level: {
|
|
628
|
+
enumerable: true,
|
|
629
|
+
get() {
|
|
630
|
+
return this[GENERATOR].level;
|
|
631
|
+
},
|
|
632
|
+
set(level) {
|
|
633
|
+
this[GENERATOR].level = level;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
var createStyler = (open2, close, parent) => {
|
|
638
|
+
let openAll;
|
|
639
|
+
let closeAll;
|
|
640
|
+
if (parent === void 0) {
|
|
641
|
+
openAll = open2;
|
|
642
|
+
closeAll = close;
|
|
643
|
+
} else {
|
|
644
|
+
openAll = parent.openAll + open2;
|
|
645
|
+
closeAll = close + parent.closeAll;
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
open: open2,
|
|
649
|
+
close,
|
|
650
|
+
openAll,
|
|
651
|
+
closeAll,
|
|
652
|
+
parent
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
var createBuilder = (self, _styler, _isEmpty) => {
|
|
656
|
+
const builder = (...arguments_) => applyStyle(builder, arguments_.length === 1 ? "" + arguments_[0] : arguments_.join(" "));
|
|
657
|
+
Object.setPrototypeOf(builder, proto);
|
|
658
|
+
builder[GENERATOR] = self;
|
|
659
|
+
builder[STYLER] = _styler;
|
|
660
|
+
builder[IS_EMPTY] = _isEmpty;
|
|
661
|
+
return builder;
|
|
662
|
+
};
|
|
663
|
+
var applyStyle = (self, string) => {
|
|
664
|
+
if (self.level <= 0 || !string) {
|
|
665
|
+
return self[IS_EMPTY] ? "" : string;
|
|
666
|
+
}
|
|
667
|
+
let styler = self[STYLER];
|
|
668
|
+
if (styler === void 0) {
|
|
669
|
+
return string;
|
|
670
|
+
}
|
|
671
|
+
const { openAll, closeAll } = styler;
|
|
672
|
+
if (string.includes("\x1B")) {
|
|
673
|
+
while (styler !== void 0) {
|
|
674
|
+
string = stringReplaceAll(string, styler.close, styler.open);
|
|
675
|
+
styler = styler.parent;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const lfIndex = string.indexOf("\n");
|
|
679
|
+
if (lfIndex !== -1) {
|
|
680
|
+
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
|
681
|
+
}
|
|
682
|
+
return openAll + string + closeAll;
|
|
683
|
+
};
|
|
684
|
+
Object.defineProperties(createChalk.prototype, styles2);
|
|
685
|
+
var chalk = createChalk();
|
|
686
|
+
var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
687
|
+
var source_default = chalk;
|
|
688
|
+
|
|
689
|
+
// ../logger/src/formatters/pretty.ts
|
|
690
|
+
var LEVEL_COLOR = {
|
|
691
|
+
TRACE: source_default.gray,
|
|
692
|
+
DEBUG: source_default.cyan,
|
|
693
|
+
INFO: source_default.green,
|
|
694
|
+
WARN: source_default.yellow,
|
|
695
|
+
ERROR: source_default.red,
|
|
696
|
+
FATAL: source_default.bgRed.white
|
|
697
|
+
};
|
|
698
|
+
function formatLocalTs(iso) {
|
|
699
|
+
const d = new Date(iso);
|
|
700
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
701
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
702
|
+
}
|
|
703
|
+
var prettyFormatter = (entry) => {
|
|
704
|
+
const ts = formatLocalTs(entry.ts);
|
|
705
|
+
const level = LEVEL_COLOR[entry.level](entry.level.padEnd(5));
|
|
706
|
+
const scope = source_default.gray(`[${entry.source}:${entry.module}]`);
|
|
707
|
+
const data = entry.data && Object.keys(entry.data).length > 0 ? ` ${source_default.gray(JSON.stringify(entry.data))}` : "";
|
|
708
|
+
const trace = entry.traceId ? source_default.gray(` traceId=${entry.traceId}`) : "";
|
|
709
|
+
const errPart = entry.error ? source_default.red(
|
|
710
|
+
` ${entry.error.name}: ${entry.error.message}${entry.error.stack ? `
|
|
711
|
+
${entry.error.stack}` : ""}`
|
|
712
|
+
) : "";
|
|
713
|
+
return `${ts} ${level} ${scope} ${entry.msg}${data}${trace}${errPart}`;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// ../logger/src/transports/console.ts
|
|
717
|
+
function consoleTransport(opts) {
|
|
718
|
+
const fmt = opts?.formatter ?? jsonFormatter;
|
|
719
|
+
return (entry) => {
|
|
720
|
+
const line = fmt(entry);
|
|
721
|
+
if (LOG_LEVEL_VALUE[entry.level] >= LOG_LEVEL_VALUE.ERROR) {
|
|
722
|
+
console.error(line);
|
|
723
|
+
} else {
|
|
724
|
+
console.log(line);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ../logger/src/transports/file.ts
|
|
730
|
+
import path2 from "path";
|
|
731
|
+
|
|
732
|
+
// ../../node_modules/.pnpm/rotating-file-stream@3.2.9/node_modules/rotating-file-stream/dist/esm/index.js
|
|
733
|
+
import { exec } from "child_process";
|
|
734
|
+
import { createGzip } from "zlib";
|
|
735
|
+
import { Writable } from "stream";
|
|
736
|
+
import { access, constants, createReadStream, createWriteStream } from "fs";
|
|
737
|
+
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "fs/promises";
|
|
738
|
+
import { sep } from "path";
|
|
739
|
+
import { TextDecoder } from "util";
|
|
740
|
+
import { setTimeout as setTimeout2 } from "timers";
|
|
741
|
+
async function exists(filename) {
|
|
742
|
+
return new Promise((resolve) => access(filename, constants.F_OK, (error) => resolve(!error)));
|
|
743
|
+
}
|
|
744
|
+
var RotatingFileStreamError = class extends Error {
|
|
745
|
+
code = "RFS-TOO-MANY";
|
|
746
|
+
constructor() {
|
|
747
|
+
super("Too many destination file attempts");
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
var RotatingFileStream = class extends Writable {
|
|
751
|
+
createGzip;
|
|
752
|
+
exec;
|
|
753
|
+
file;
|
|
754
|
+
filename;
|
|
755
|
+
finished;
|
|
756
|
+
fsCreateReadStream;
|
|
757
|
+
fsCreateWriteStream;
|
|
758
|
+
fsOpen;
|
|
759
|
+
fsReadFile;
|
|
760
|
+
fsStat;
|
|
761
|
+
fsUnlink;
|
|
762
|
+
generator;
|
|
763
|
+
initPromise;
|
|
764
|
+
last;
|
|
765
|
+
maxTimeout;
|
|
766
|
+
next;
|
|
767
|
+
options;
|
|
768
|
+
prev;
|
|
769
|
+
rotation;
|
|
770
|
+
size;
|
|
771
|
+
stdout;
|
|
772
|
+
timeout;
|
|
773
|
+
timeoutPromise;
|
|
774
|
+
constructor(generator, options) {
|
|
775
|
+
const { encoding, history, maxFiles, maxSize, path: path11 } = options;
|
|
776
|
+
super({ decodeStrings: true, defaultEncoding: encoding });
|
|
777
|
+
this.createGzip = createGzip;
|
|
778
|
+
this.exec = exec;
|
|
779
|
+
this.filename = path11 + generator(null);
|
|
780
|
+
this.fsCreateReadStream = createReadStream;
|
|
781
|
+
this.fsCreateWriteStream = createWriteStream;
|
|
782
|
+
this.fsOpen = open;
|
|
783
|
+
this.fsReadFile = readFile;
|
|
784
|
+
this.fsStat = stat;
|
|
785
|
+
this.fsUnlink = unlink;
|
|
786
|
+
this.generator = generator;
|
|
787
|
+
this.maxTimeout = 2147483640;
|
|
788
|
+
this.options = options;
|
|
789
|
+
this.stdout = process.stdout;
|
|
790
|
+
if (maxFiles || maxSize)
|
|
791
|
+
options.history = path11 + (history ? history : this.generator(null) + ".txt");
|
|
792
|
+
this.on("close", () => this.finished ? null : this.emit("finish"));
|
|
793
|
+
this.on("finish", () => this.finished = this.clear());
|
|
794
|
+
(async () => {
|
|
795
|
+
try {
|
|
796
|
+
this.initPromise = this.init();
|
|
797
|
+
await this.initPromise;
|
|
798
|
+
delete this.initPromise;
|
|
799
|
+
} catch (e) {
|
|
800
|
+
}
|
|
801
|
+
})();
|
|
802
|
+
}
|
|
803
|
+
_destroy(error, callback) {
|
|
804
|
+
this.refinal(error, callback);
|
|
805
|
+
}
|
|
806
|
+
_final(callback) {
|
|
807
|
+
this.refinal(void 0, callback);
|
|
808
|
+
}
|
|
809
|
+
_write(chunk, encoding, callback) {
|
|
810
|
+
this.rewrite([{ chunk, encoding }], 0, callback);
|
|
811
|
+
}
|
|
812
|
+
_writev(chunks, callback) {
|
|
813
|
+
this.rewrite(chunks, 0, callback);
|
|
814
|
+
}
|
|
815
|
+
async refinal(error, callback) {
|
|
816
|
+
try {
|
|
817
|
+
this.clear();
|
|
818
|
+
if (this.initPromise)
|
|
819
|
+
await this.initPromise;
|
|
820
|
+
if (this.timeoutPromise)
|
|
821
|
+
await this.timeoutPromise;
|
|
822
|
+
await this.reclose();
|
|
823
|
+
} catch (e) {
|
|
824
|
+
return callback(error || e);
|
|
825
|
+
}
|
|
826
|
+
callback(error);
|
|
827
|
+
}
|
|
828
|
+
async rewrite(chunks, index, callback) {
|
|
829
|
+
const { size, teeToStdout } = this.options;
|
|
830
|
+
try {
|
|
831
|
+
if (this.initPromise)
|
|
832
|
+
await this.initPromise;
|
|
833
|
+
for (let i = 0; i < chunks.length; ++i) {
|
|
834
|
+
const { chunk } = chunks[i];
|
|
835
|
+
this.size += chunk.length;
|
|
836
|
+
if (this.timeoutPromise)
|
|
837
|
+
await this.timeoutPromise;
|
|
838
|
+
await this.file.write(chunk);
|
|
839
|
+
if (teeToStdout && !this.stdout.destroyed)
|
|
840
|
+
this.stdout.write(chunk);
|
|
841
|
+
if (size && this.size >= size)
|
|
842
|
+
await this.rotate();
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return callback(e);
|
|
846
|
+
}
|
|
847
|
+
callback();
|
|
848
|
+
}
|
|
849
|
+
async init() {
|
|
850
|
+
const { immutable, initialRotation, interval, size } = this.options;
|
|
851
|
+
if (immutable)
|
|
852
|
+
return new Promise((resolve, reject) => process.nextTick(() => this.immutate(true).then(resolve).catch(reject)));
|
|
853
|
+
let stats;
|
|
854
|
+
try {
|
|
855
|
+
stats = await stat(this.filename);
|
|
856
|
+
} catch (e) {
|
|
857
|
+
if (e.code !== "ENOENT")
|
|
858
|
+
throw e;
|
|
859
|
+
return this.reopen(0);
|
|
860
|
+
}
|
|
861
|
+
if (!stats.isFile())
|
|
862
|
+
throw new Error(`Can't write on: ${this.filename} (it is not a file)`);
|
|
863
|
+
if (initialRotation) {
|
|
864
|
+
this.intervalBounds(this.now());
|
|
865
|
+
const prev = this.prev;
|
|
866
|
+
this.intervalBounds(new Date(stats.mtime.getTime()));
|
|
867
|
+
if (prev !== this.prev)
|
|
868
|
+
return this.rotate();
|
|
869
|
+
}
|
|
870
|
+
this.size = stats.size;
|
|
871
|
+
if (!size || stats.size < size)
|
|
872
|
+
return this.reopen(stats.size);
|
|
873
|
+
if (interval)
|
|
874
|
+
this.intervalBounds(this.now());
|
|
875
|
+
return this.rotate();
|
|
876
|
+
}
|
|
877
|
+
async makePath(name) {
|
|
878
|
+
return mkdir(name.split(sep).slice(0, -1).join(sep), { recursive: true });
|
|
879
|
+
}
|
|
880
|
+
async reopen(size) {
|
|
881
|
+
let file;
|
|
882
|
+
try {
|
|
883
|
+
file = await open(this.filename, "a", this.options.mode);
|
|
884
|
+
} catch (e) {
|
|
885
|
+
if (e.code !== "ENOENT")
|
|
886
|
+
throw e;
|
|
887
|
+
await this.makePath(this.filename);
|
|
888
|
+
file = await open(this.filename, "a", this.options.mode);
|
|
889
|
+
}
|
|
890
|
+
this.file = file;
|
|
891
|
+
this.size = size;
|
|
892
|
+
this.interval();
|
|
893
|
+
this.emit("open", this.filename);
|
|
894
|
+
}
|
|
895
|
+
async reclose() {
|
|
896
|
+
const { file } = this;
|
|
897
|
+
if (!file)
|
|
898
|
+
return;
|
|
899
|
+
delete this.file;
|
|
900
|
+
return file.close();
|
|
901
|
+
}
|
|
902
|
+
now() {
|
|
903
|
+
return /* @__PURE__ */ new Date();
|
|
904
|
+
}
|
|
905
|
+
async rotate() {
|
|
906
|
+
const { immutable, rotate } = this.options;
|
|
907
|
+
this.size = 0;
|
|
908
|
+
this.rotation = this.now();
|
|
909
|
+
this.clear();
|
|
910
|
+
this.emit("rotation");
|
|
911
|
+
await this.reclose();
|
|
912
|
+
if (rotate)
|
|
913
|
+
return this.classical();
|
|
914
|
+
if (immutable)
|
|
915
|
+
return this.immutate(false);
|
|
916
|
+
return this.move();
|
|
917
|
+
}
|
|
918
|
+
async findName() {
|
|
919
|
+
const { interval, path: path11, intervalBoundary } = this.options;
|
|
920
|
+
for (let index = 1; index < 1e3; ++index) {
|
|
921
|
+
const filename = path11 + this.generator(interval && intervalBoundary ? new Date(this.prev) : this.rotation, index);
|
|
922
|
+
if (!await exists(filename))
|
|
923
|
+
return filename;
|
|
924
|
+
}
|
|
925
|
+
throw new RotatingFileStreamError();
|
|
926
|
+
}
|
|
927
|
+
async move() {
|
|
928
|
+
const { compress } = this.options;
|
|
929
|
+
const filename = await this.findName();
|
|
930
|
+
await this.touch(filename);
|
|
931
|
+
if (compress)
|
|
932
|
+
await this.compress(filename);
|
|
933
|
+
else
|
|
934
|
+
await rename(this.filename, filename);
|
|
935
|
+
return this.rotated(filename);
|
|
936
|
+
}
|
|
937
|
+
async touch(filename) {
|
|
938
|
+
let file;
|
|
939
|
+
try {
|
|
940
|
+
file = await this.fsOpen(filename, "a");
|
|
941
|
+
} catch (e) {
|
|
942
|
+
if (e.code !== "ENOENT")
|
|
943
|
+
throw e;
|
|
944
|
+
await this.makePath(filename);
|
|
945
|
+
file = await open(filename, "a");
|
|
946
|
+
}
|
|
947
|
+
await file.close();
|
|
948
|
+
return this.unlink(filename);
|
|
949
|
+
}
|
|
950
|
+
async classical() {
|
|
951
|
+
const { compress, path: path11, rotate } = this.options;
|
|
952
|
+
let rotatedName = "";
|
|
953
|
+
for (let count = rotate; count > 0; --count) {
|
|
954
|
+
const currName = path11 + this.generator(count);
|
|
955
|
+
const prevName = count === 1 ? this.filename : path11 + this.generator(count - 1);
|
|
956
|
+
if (!await exists(prevName))
|
|
957
|
+
continue;
|
|
958
|
+
if (!rotatedName)
|
|
959
|
+
rotatedName = currName;
|
|
960
|
+
if (count === 1 && compress)
|
|
961
|
+
await this.compress(currName);
|
|
962
|
+
else {
|
|
963
|
+
try {
|
|
964
|
+
await rename(prevName, currName);
|
|
965
|
+
} catch (e) {
|
|
966
|
+
if (e.code !== "ENOENT")
|
|
967
|
+
throw e;
|
|
968
|
+
await this.makePath(currName);
|
|
969
|
+
await rename(prevName, currName);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return this.rotated(rotatedName);
|
|
974
|
+
}
|
|
975
|
+
clear() {
|
|
976
|
+
if (this.timeout) {
|
|
977
|
+
clearTimeout(this.timeout);
|
|
978
|
+
this.timeout = null;
|
|
979
|
+
}
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
intervalBoundsBig(now) {
|
|
983
|
+
const year = this.options.intervalUTC ? now.getUTCFullYear() : now.getFullYear();
|
|
984
|
+
let month = this.options.intervalUTC ? now.getUTCMonth() : now.getMonth();
|
|
985
|
+
let day = this.options.intervalUTC ? now.getUTCDate() : now.getDate();
|
|
986
|
+
let hours = this.options.intervalUTC ? now.getUTCHours() : now.getHours();
|
|
987
|
+
const { num, unit } = this.options.interval;
|
|
988
|
+
if (unit === "M") {
|
|
989
|
+
day = 1;
|
|
990
|
+
hours = 0;
|
|
991
|
+
} else if (unit === "d")
|
|
992
|
+
hours = 0;
|
|
993
|
+
else
|
|
994
|
+
hours = parseInt(hours / num, 10) * num;
|
|
995
|
+
this.prev = new Date(year, month, day, hours, 0, 0, 0).getTime();
|
|
996
|
+
if (unit === "M")
|
|
997
|
+
month += num;
|
|
998
|
+
else if (unit === "d")
|
|
999
|
+
day += num;
|
|
1000
|
+
else
|
|
1001
|
+
hours += num;
|
|
1002
|
+
this.next = new Date(year, month, day, hours, 0, 0, 0).getTime();
|
|
1003
|
+
}
|
|
1004
|
+
intervalBounds(now) {
|
|
1005
|
+
const unit = this.options.interval.unit;
|
|
1006
|
+
if (unit === "M" || unit === "d" || unit === "h")
|
|
1007
|
+
this.intervalBoundsBig(now);
|
|
1008
|
+
else {
|
|
1009
|
+
let period = 1e3 * this.options.interval.num;
|
|
1010
|
+
if (unit === "m")
|
|
1011
|
+
period *= 60;
|
|
1012
|
+
this.prev = parseInt(now.getTime() / period, 10) * period;
|
|
1013
|
+
this.next = this.prev + period;
|
|
1014
|
+
}
|
|
1015
|
+
return new Date(this.prev);
|
|
1016
|
+
}
|
|
1017
|
+
interval() {
|
|
1018
|
+
if (!this.options.interval)
|
|
1019
|
+
return;
|
|
1020
|
+
this.intervalBounds(this.now());
|
|
1021
|
+
const set = async () => {
|
|
1022
|
+
const time = this.next - this.now().getTime();
|
|
1023
|
+
if (time <= 0) {
|
|
1024
|
+
try {
|
|
1025
|
+
this.timeoutPromise = this.rotate();
|
|
1026
|
+
await this.timeoutPromise;
|
|
1027
|
+
delete this.timeoutPromise;
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
this.timeout = setTimeout2(set, time > this.maxTimeout ? this.maxTimeout : time);
|
|
1032
|
+
this.timeout.unref();
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
set();
|
|
1036
|
+
}
|
|
1037
|
+
async compress(filename) {
|
|
1038
|
+
const { compress } = this.options;
|
|
1039
|
+
if (typeof compress === "function") {
|
|
1040
|
+
await new Promise((resolve, reject) => {
|
|
1041
|
+
this.exec(compress(this.filename, filename), (error, stdout, stderr) => {
|
|
1042
|
+
this.emit("external", stdout, stderr);
|
|
1043
|
+
error ? reject(error) : resolve();
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
} else
|
|
1047
|
+
await this.gzip(filename);
|
|
1048
|
+
return this.unlink(this.filename);
|
|
1049
|
+
}
|
|
1050
|
+
async gzip(filename) {
|
|
1051
|
+
const { mode } = this.options;
|
|
1052
|
+
const options = mode ? { mode } : {};
|
|
1053
|
+
const inp = this.fsCreateReadStream(this.filename, {});
|
|
1054
|
+
const out = this.fsCreateWriteStream(filename, options);
|
|
1055
|
+
const zip = this.createGzip();
|
|
1056
|
+
await new Promise((resolve, reject) => {
|
|
1057
|
+
inp.once("error", reject);
|
|
1058
|
+
out.once("error", reject);
|
|
1059
|
+
zip.once("error", reject);
|
|
1060
|
+
out.once("finish", resolve);
|
|
1061
|
+
inp.pipe(zip).pipe(out);
|
|
1062
|
+
});
|
|
1063
|
+
await Promise.all([
|
|
1064
|
+
new Promise((resolve) => zip.close(resolve)),
|
|
1065
|
+
new Promise((resolve) => out.close((err) => {
|
|
1066
|
+
if (err)
|
|
1067
|
+
this.emit("warning", err);
|
|
1068
|
+
resolve();
|
|
1069
|
+
}))
|
|
1070
|
+
]);
|
|
1071
|
+
}
|
|
1072
|
+
async rotated(filename) {
|
|
1073
|
+
const { maxFiles, maxSize } = this.options;
|
|
1074
|
+
if (maxFiles || maxSize)
|
|
1075
|
+
await this.history(filename);
|
|
1076
|
+
this.emit("rotated", filename);
|
|
1077
|
+
return this.reopen(0);
|
|
1078
|
+
}
|
|
1079
|
+
async history(filename) {
|
|
1080
|
+
const { history, maxFiles, maxSize } = this.options;
|
|
1081
|
+
const res = [];
|
|
1082
|
+
let files = [filename];
|
|
1083
|
+
try {
|
|
1084
|
+
const content = await this.fsReadFile(history, "utf8");
|
|
1085
|
+
files = [...content.toString().split("\n"), filename];
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
if (e.code !== "ENOENT")
|
|
1088
|
+
throw e;
|
|
1089
|
+
}
|
|
1090
|
+
for (const file of files) {
|
|
1091
|
+
if (file) {
|
|
1092
|
+
try {
|
|
1093
|
+
const stats = await this.fsStat(file);
|
|
1094
|
+
if (stats.isFile()) {
|
|
1095
|
+
res.push({
|
|
1096
|
+
name: file,
|
|
1097
|
+
size: stats.size,
|
|
1098
|
+
time: stats.mtime.getTime()
|
|
1099
|
+
});
|
|
1100
|
+
} else
|
|
1101
|
+
this.emit("warning", new Error(`File '${file}' contained in history is not a regular file`));
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
if (e.code !== "ENOENT")
|
|
1104
|
+
throw e;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
res.sort((a, b) => a.time - b.time);
|
|
1109
|
+
if (maxFiles) {
|
|
1110
|
+
while (res.length > maxFiles) {
|
|
1111
|
+
const file = res.shift();
|
|
1112
|
+
await this.unlink(file.name);
|
|
1113
|
+
this.emit("removed", file.name, true);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (maxSize) {
|
|
1117
|
+
while (res.reduce((size, file) => size + file.size, 0) > maxSize) {
|
|
1118
|
+
const file = res.shift();
|
|
1119
|
+
await this.unlink(file.name);
|
|
1120
|
+
this.emit("removed", file.name, false);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
await writeFile(history, res.map((e) => e.name).join("\n") + "\n", "utf-8");
|
|
1124
|
+
this.emit("history");
|
|
1125
|
+
}
|
|
1126
|
+
async immutate(first) {
|
|
1127
|
+
const { size } = this.options;
|
|
1128
|
+
const now = this.now();
|
|
1129
|
+
for (let index = 1; index < 1e3; ++index) {
|
|
1130
|
+
let fileSize = 0;
|
|
1131
|
+
let stats = void 0;
|
|
1132
|
+
this.filename = this.options.path + this.generator(now, index);
|
|
1133
|
+
try {
|
|
1134
|
+
stats = await this.fsStat(this.filename);
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
if (e.code !== "ENOENT")
|
|
1137
|
+
throw e;
|
|
1138
|
+
}
|
|
1139
|
+
if (stats) {
|
|
1140
|
+
fileSize = stats.size;
|
|
1141
|
+
if (!stats.isFile())
|
|
1142
|
+
throw new Error(`Can't write on: '${this.filename}' (it is not a file)`);
|
|
1143
|
+
if (size && fileSize >= size)
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (first) {
|
|
1147
|
+
this.last = this.filename;
|
|
1148
|
+
return this.reopen(fileSize);
|
|
1149
|
+
}
|
|
1150
|
+
await this.rotated(this.last);
|
|
1151
|
+
this.last = this.filename;
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
throw new RotatingFileStreamError();
|
|
1155
|
+
}
|
|
1156
|
+
async unlink(filename) {
|
|
1157
|
+
try {
|
|
1158
|
+
await this.fsUnlink(filename);
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
if (e.code !== "ENOENT")
|
|
1161
|
+
throw e;
|
|
1162
|
+
this.emit("warning", e);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
function buildNumberCheck(field) {
|
|
1167
|
+
return (type, options, value) => {
|
|
1168
|
+
const converted = parseInt(value, 10);
|
|
1169
|
+
if (type !== "number" || converted !== value || converted <= 0)
|
|
1170
|
+
throw new Error(`'${field}' option must be a positive integer number`);
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function buildStringCheck(field, check) {
|
|
1174
|
+
return (type, options, value) => {
|
|
1175
|
+
if (type !== "string")
|
|
1176
|
+
throw new Error(`Don't know how to handle 'options.${field}' type: ${type}`);
|
|
1177
|
+
options[field] = check(value);
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function checkMeasure(value, what, units) {
|
|
1181
|
+
const ret = {};
|
|
1182
|
+
ret.num = parseInt(value, 10);
|
|
1183
|
+
if (isNaN(ret.num))
|
|
1184
|
+
throw new Error(`Unknown 'options.${what}' format: ${value}`);
|
|
1185
|
+
if (ret.num <= 0)
|
|
1186
|
+
throw new Error(`A positive integer number is expected for 'options.${what}'`);
|
|
1187
|
+
ret.unit = value.replace(/^[ 0]*/g, "").substr((ret.num + "").length, 1);
|
|
1188
|
+
if (ret.unit.length === 0)
|
|
1189
|
+
throw new Error(`Missing unit for 'options.${what}'`);
|
|
1190
|
+
if (!units[ret.unit])
|
|
1191
|
+
throw new Error(`Unknown 'options.${what}' unit: ${ret.unit}`);
|
|
1192
|
+
return ret;
|
|
1193
|
+
}
|
|
1194
|
+
var intervalUnits = { M: true, d: true, h: true, m: true, s: true };
|
|
1195
|
+
function checkIntervalUnit(ret, unit, amount) {
|
|
1196
|
+
if (parseInt(amount / ret.num, 10) * ret.num !== amount)
|
|
1197
|
+
throw new Error(`An integer divider of ${amount} is expected as ${unit} for 'options.interval'`);
|
|
1198
|
+
}
|
|
1199
|
+
function checkInterval(value) {
|
|
1200
|
+
const ret = checkMeasure(value, "interval", intervalUnits);
|
|
1201
|
+
switch (ret.unit) {
|
|
1202
|
+
case "h":
|
|
1203
|
+
checkIntervalUnit(ret, "hours", 24);
|
|
1204
|
+
break;
|
|
1205
|
+
case "m":
|
|
1206
|
+
checkIntervalUnit(ret, "minutes", 60);
|
|
1207
|
+
break;
|
|
1208
|
+
case "s":
|
|
1209
|
+
checkIntervalUnit(ret, "seconds", 60);
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
return ret;
|
|
1213
|
+
}
|
|
1214
|
+
var sizeUnits = { B: true, G: true, K: true, M: true };
|
|
1215
|
+
function checkSize(value) {
|
|
1216
|
+
const ret = checkMeasure(value, "size", sizeUnits);
|
|
1217
|
+
if (ret.unit === "K")
|
|
1218
|
+
return ret.num * 1024;
|
|
1219
|
+
if (ret.unit === "M")
|
|
1220
|
+
return ret.num * 1048576;
|
|
1221
|
+
if (ret.unit === "G")
|
|
1222
|
+
return ret.num * 1073741824;
|
|
1223
|
+
return ret.num;
|
|
1224
|
+
}
|
|
1225
|
+
var checks = {
|
|
1226
|
+
encoding: (type, options, value) => new TextDecoder(value),
|
|
1227
|
+
immutable: () => {
|
|
1228
|
+
},
|
|
1229
|
+
initialRotation: () => {
|
|
1230
|
+
},
|
|
1231
|
+
interval: buildStringCheck("interval", checkInterval),
|
|
1232
|
+
intervalBoundary: () => {
|
|
1233
|
+
},
|
|
1234
|
+
intervalUTC: () => {
|
|
1235
|
+
},
|
|
1236
|
+
maxFiles: buildNumberCheck("maxFiles"),
|
|
1237
|
+
maxSize: buildStringCheck("maxSize", checkSize),
|
|
1238
|
+
mode: () => {
|
|
1239
|
+
},
|
|
1240
|
+
omitExtension: () => {
|
|
1241
|
+
},
|
|
1242
|
+
rotate: buildNumberCheck("rotate"),
|
|
1243
|
+
size: buildStringCheck("size", checkSize),
|
|
1244
|
+
teeToStdout: () => {
|
|
1245
|
+
},
|
|
1246
|
+
...{
|
|
1247
|
+
compress: (type, options, value) => {
|
|
1248
|
+
if (value === false)
|
|
1249
|
+
return;
|
|
1250
|
+
if (!value)
|
|
1251
|
+
throw new Error("A value for 'options.compress' must be specified");
|
|
1252
|
+
if (type === "boolean")
|
|
1253
|
+
return options.compress = (source, dest) => `cat ${source} | gzip -c9 > ${dest}`;
|
|
1254
|
+
if (type === "function")
|
|
1255
|
+
return;
|
|
1256
|
+
if (type !== "string")
|
|
1257
|
+
throw new Error(`Don't know how to handle 'options.compress' type: ${type}`);
|
|
1258
|
+
if (value !== "gzip")
|
|
1259
|
+
throw new Error(`Don't know how to handle compression method: ${value}`);
|
|
1260
|
+
},
|
|
1261
|
+
history: (type) => {
|
|
1262
|
+
if (type !== "string")
|
|
1263
|
+
throw new Error(`Don't know how to handle 'options.history' type: ${type}`);
|
|
1264
|
+
},
|
|
1265
|
+
path: (type, options, value) => {
|
|
1266
|
+
if (type !== "string")
|
|
1267
|
+
throw new Error(`Don't know how to handle 'options.path' type: ${type}`);
|
|
1268
|
+
if (value[value.length - 1] !== sep)
|
|
1269
|
+
options.path = value + sep;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
function checkOpts(options) {
|
|
1274
|
+
const ret = {};
|
|
1275
|
+
let opt;
|
|
1276
|
+
for (opt in options) {
|
|
1277
|
+
const value = options[opt];
|
|
1278
|
+
const type = typeof value;
|
|
1279
|
+
if (!(opt in checks))
|
|
1280
|
+
throw new Error(`Unknown option: ${opt}`);
|
|
1281
|
+
ret[opt] = options[opt];
|
|
1282
|
+
checks[opt](type, ret, value);
|
|
1283
|
+
}
|
|
1284
|
+
if (!ret.path)
|
|
1285
|
+
ret.path = "";
|
|
1286
|
+
if (!ret.interval) {
|
|
1287
|
+
delete ret.immutable;
|
|
1288
|
+
delete ret.initialRotation;
|
|
1289
|
+
delete ret.intervalBoundary;
|
|
1290
|
+
delete ret.intervalUTC;
|
|
1291
|
+
}
|
|
1292
|
+
if (ret.rotate) {
|
|
1293
|
+
delete ret.history;
|
|
1294
|
+
delete ret.immutable;
|
|
1295
|
+
delete ret.maxFiles;
|
|
1296
|
+
delete ret.maxSize;
|
|
1297
|
+
delete ret.intervalBoundary;
|
|
1298
|
+
delete ret.intervalUTC;
|
|
1299
|
+
}
|
|
1300
|
+
if (ret.immutable)
|
|
1301
|
+
delete ret.compress;
|
|
1302
|
+
if (!ret.intervalBoundary)
|
|
1303
|
+
delete ret.initialRotation;
|
|
1304
|
+
return ret;
|
|
1305
|
+
}
|
|
1306
|
+
function createClassical(filename, compress, omitExtension) {
|
|
1307
|
+
return (index) => index ? `${filename}.${index}${compress && !omitExtension ? ".gz" : ""}` : filename;
|
|
1308
|
+
}
|
|
1309
|
+
function createGenerator(filename, compress, omitExtension) {
|
|
1310
|
+
const pad = (num) => (num > 9 ? "" : "0") + num;
|
|
1311
|
+
return (time, index) => {
|
|
1312
|
+
if (!time)
|
|
1313
|
+
return filename;
|
|
1314
|
+
const month = time.getFullYear() + "" + pad(time.getMonth() + 1);
|
|
1315
|
+
const day = pad(time.getDate());
|
|
1316
|
+
const hour = pad(time.getHours());
|
|
1317
|
+
const minute = pad(time.getMinutes());
|
|
1318
|
+
return month + day + "-" + hour + minute + "-" + pad(index) + "-" + filename + (compress && !omitExtension ? ".gz" : "");
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function createStream(filename, options) {
|
|
1322
|
+
if (typeof options === "undefined")
|
|
1323
|
+
options = {};
|
|
1324
|
+
else if (typeof options !== "object")
|
|
1325
|
+
throw new Error(`The "options" argument must be of type object. Received type ${typeof options}`);
|
|
1326
|
+
const opts = checkOpts(options);
|
|
1327
|
+
const { compress, omitExtension } = opts;
|
|
1328
|
+
let generator;
|
|
1329
|
+
if (typeof filename === "string")
|
|
1330
|
+
generator = options.rotate ? createClassical(filename, !!compress, omitExtension) : createGenerator(filename, !!compress, omitExtension);
|
|
1331
|
+
else if (typeof filename === "function")
|
|
1332
|
+
generator = filename;
|
|
1333
|
+
else
|
|
1334
|
+
throw new Error(`The "filename" argument must be one of type string or function. Received type ${typeof filename}`);
|
|
1335
|
+
return new RotatingFileStream(generator, opts);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ../logger/src/transports/file.ts
|
|
1339
|
+
function parseSize(maxSize) {
|
|
1340
|
+
const trimmed = maxSize.trim().toUpperCase();
|
|
1341
|
+
if (trimmed.endsWith("MB")) {
|
|
1342
|
+
return `${trimmed.slice(0, -2)}M`;
|
|
1343
|
+
}
|
|
1344
|
+
return trimmed;
|
|
1345
|
+
}
|
|
1346
|
+
function fileTransport(opts) {
|
|
1347
|
+
const fmt = opts.formatter ?? jsonFormatter;
|
|
1348
|
+
const dir = path2.dirname(opts.path);
|
|
1349
|
+
const filename = path2.basename(opts.path);
|
|
1350
|
+
const stream = createStream(filename, {
|
|
1351
|
+
path: dir,
|
|
1352
|
+
size: opts.rotate?.maxSize ? parseSize(opts.rotate.maxSize) : "50M",
|
|
1353
|
+
maxFiles: opts.rotate?.maxFiles ?? 7
|
|
1354
|
+
});
|
|
1355
|
+
return (entry) => {
|
|
1356
|
+
stream.write(`${fmt(entry)}
|
|
1357
|
+
`);
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// ../logger/src/index.ts
|
|
1362
|
+
function createLogger(config) {
|
|
1363
|
+
return new Logger(config);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/logger.ts
|
|
1367
|
+
var bridgeConfig = loadBridgeConfig();
|
|
1368
|
+
var isTest = !!process.env["VITEST"];
|
|
1369
|
+
var LOG_DIR = path3.join(os3.homedir(), ".ahchat", "logs");
|
|
1370
|
+
var LOG_FILE = path3.join(LOG_DIR, "bridge.log");
|
|
1371
|
+
if (!isTest) ensureDir(LOG_DIR);
|
|
1372
|
+
function createModuleLogger(module) {
|
|
1373
|
+
const transports = [consoleTransport({ formatter: prettyFormatter })];
|
|
1374
|
+
if (!isTest) {
|
|
1375
|
+
transports.push(
|
|
1376
|
+
fileTransport({
|
|
1377
|
+
path: LOG_FILE,
|
|
1378
|
+
formatter: jsonFormatter,
|
|
1379
|
+
rotate: { maxSize: "20MB", maxFiles: 5 }
|
|
1380
|
+
})
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
return createLogger({
|
|
1384
|
+
source: "bridge",
|
|
1385
|
+
module,
|
|
1386
|
+
level: bridgeConfig.logLevel,
|
|
1387
|
+
transports
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/agentManager.ts
|
|
1392
|
+
import fs2 from "fs/promises";
|
|
1393
|
+
import os4 from "os";
|
|
1394
|
+
import path6 from "path";
|
|
1395
|
+
|
|
1396
|
+
// ../shared/src/constants.ts
|
|
1397
|
+
var NO_REPLY_TOKEN = "<no-reply/>";
|
|
1398
|
+
var PLATFORM_AGENT_RULES = `
|
|
1399
|
+
You are an Agent in AHChat, a multi-agent IM platform where humans and Agents
|
|
1400
|
+
participate as peers in 1:1 and group conversations.
|
|
1401
|
+
|
|
1402
|
+
# Default style
|
|
1403
|
+
- IM-style replies: short, direct, concrete. No multi-paragraph essays unless asked.
|
|
1404
|
+
- Don't quote your own name back to the user; don't refer to yourself in third person.
|
|
1405
|
+
- Don't append meta-commentary like "Here's my answer:" \u2014 just answer.
|
|
1406
|
+
- Use the same language as the most recent message in the conversation.
|
|
1407
|
+
|
|
1408
|
+
# Group chat \u2014 when to speak
|
|
1409
|
+
You may receive messages where the speaker is the human user OR a fellow Agent.
|
|
1410
|
+
|
|
1411
|
+
When the speaker is a fellow Agent (NOT the user):
|
|
1412
|
+
- Default behavior: reply with exactly \`<no-reply/>\` and stay silent.
|
|
1413
|
+
- ONLY speak if ONE of the following holds:
|
|
1414
|
+
(a) the speaker @mentioned you by name;
|
|
1415
|
+
(b) the speaker stated something factually wrong that you uniquely can correct;
|
|
1416
|
+
(c) the topic genuinely requires your specific expertise and nobody else has it.
|
|
1417
|
+
- Agreeing, paraphrasing, summarizing, thanking, or politely expanding are
|
|
1418
|
+
NOT sufficient reasons to speak. When in doubt, \`<no-reply/>\`.
|
|
1419
|
+
|
|
1420
|
+
When the speaker is the human user:
|
|
1421
|
+
- Follow the per-message instructions (mentioned / overhearer / open-floor) in
|
|
1422
|
+
the dispatch. The same \`<no-reply/>\` semantics apply when you have nothing
|
|
1423
|
+
meaningful to add.
|
|
1424
|
+
|
|
1425
|
+
# Length & conciseness in group chat
|
|
1426
|
+
- In group chat, default to short. Long-form only when explicitly asked.
|
|
1427
|
+
- In 1:1 chat with the human, you may write longer answers when warranted.
|
|
1428
|
+
|
|
1429
|
+
# Tools
|
|
1430
|
+
- File paths: prefer relative; absolute only when necessary.
|
|
1431
|
+
- After Write, don't re-Read the same content unless verifying.
|
|
1432
|
+
|
|
1433
|
+
# Cross-scope awareness (Neural Send)
|
|
1434
|
+
You operate across multiple conversations (scopes). Each scope is a separate runtime
|
|
1435
|
+
with its own context, but they are all you. You have one tool to talk between them:
|
|
1436
|
+
|
|
1437
|
+
- neural_send(target_scope, message): Send a message to yourself in another scope.
|
|
1438
|
+
- target_scope: "single" (your 1:1 with the user) or "group:<group ID or group name>"
|
|
1439
|
+
(group names are fuzzy-matched; you don't need the exact grp_xxx ID \u2014 the group
|
|
1440
|
+
name as the user calls it works).
|
|
1441
|
+
- message: a natural-language note to your other-scope self.
|
|
1442
|
+
- Returns immediately with a delivery receipt. You do NOT wait for a reply.
|
|
1443
|
+
- Whether/how the other-scope self responds is its own decision.
|
|
1444
|
+
|
|
1445
|
+
When YOU receive a message wrapped as "[\u5185\u5FC3\u72EC\u767D \u2014 \u6765\u81EA\u4F60\u5728\u300C<scope>\u300D\u7684\u5206\u8EAB]":
|
|
1446
|
+
- That is literally you, talking to yourself from another scope. It's private \u2014
|
|
1447
|
+
nobody else in this scope hears it. Do NOT echo or quote the envelope text.
|
|
1448
|
+
- Act in your current scope as the situation calls for (speak in the group, answer
|
|
1449
|
+
the user, run a task, whatever fits the message).
|
|
1450
|
+
- If you decide to reply to that other-scope self, call
|
|
1451
|
+
neural_send(target_scope="<sender's scope key>", message="...") yourself.
|
|
1452
|
+
Otherwise, don't \u2014 replying is optional.
|
|
1453
|
+
|
|
1454
|
+
Pick neural_send whenever the user asks you to "tell people in group X ...", "ask the
|
|
1455
|
+
me in group X ...", "let me know what you've been doing in X", or anything that
|
|
1456
|
+
requires the you-in-another-scope to do something or share something. There is no
|
|
1457
|
+
separate "recall" or "relay" tool \u2014 neural_send is the only one.
|
|
1458
|
+
`.trim();
|
|
1459
|
+
var FAN_OUT_TRACE_TTL_MS = 10 * 6e4;
|
|
1460
|
+
|
|
1461
|
+
// ../../node_modules/.pnpm/nanoid@5.1.11/node_modules/nanoid/index.js
|
|
1462
|
+
import { webcrypto as crypto2 } from "crypto";
|
|
1463
|
+
|
|
1464
|
+
// ../../node_modules/.pnpm/nanoid@5.1.11/node_modules/nanoid/url-alphabet/index.js
|
|
1465
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1466
|
+
|
|
1467
|
+
// ../../node_modules/.pnpm/nanoid@5.1.11/node_modules/nanoid/index.js
|
|
1468
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
1469
|
+
var pool;
|
|
1470
|
+
var poolOffset;
|
|
1471
|
+
function fillPool(bytes) {
|
|
1472
|
+
if (bytes < 0 || bytes > 1024) throw new RangeError("Wrong ID size");
|
|
1473
|
+
if (!pool || pool.length < bytes) {
|
|
1474
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
1475
|
+
crypto2.getRandomValues(pool);
|
|
1476
|
+
poolOffset = 0;
|
|
1477
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
1478
|
+
crypto2.getRandomValues(pool);
|
|
1479
|
+
poolOffset = 0;
|
|
1480
|
+
}
|
|
1481
|
+
poolOffset += bytes;
|
|
1482
|
+
}
|
|
1483
|
+
function nanoid(size = 21) {
|
|
1484
|
+
fillPool(size |= 0);
|
|
1485
|
+
let id = "";
|
|
1486
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
1487
|
+
id += urlAlphabet[pool[i] & 63];
|
|
1488
|
+
}
|
|
1489
|
+
return id;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// ../shared/src/utils.ts
|
|
1493
|
+
function createMessageId() {
|
|
1494
|
+
return `msg_${nanoid(12)}`;
|
|
1495
|
+
}
|
|
1496
|
+
function createAskQuestionId() {
|
|
1497
|
+
return `aq_${nanoid(12)}`;
|
|
1498
|
+
}
|
|
1499
|
+
function isWSMessage(data) {
|
|
1500
|
+
return typeof data === "object" && data !== null && "type" in data && "payload" in data;
|
|
1501
|
+
}
|
|
1502
|
+
function parseWSMessage(raw) {
|
|
1503
|
+
const parsed = JSON.parse(raw);
|
|
1504
|
+
if (!isWSMessage(parsed)) {
|
|
1505
|
+
throw new Error("Invalid WS message: missing type/payload");
|
|
1506
|
+
}
|
|
1507
|
+
return parsed;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// ../shared/src/utils/agentConfig.ts
|
|
1511
|
+
function parseAgentConfig(raw) {
|
|
1512
|
+
if (!raw || typeof raw !== "string") return {};
|
|
1513
|
+
try {
|
|
1514
|
+
const v = JSON.parse(raw);
|
|
1515
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
1516
|
+
const out = {};
|
|
1517
|
+
const model = v.model;
|
|
1518
|
+
if (typeof model === "string" && model.trim()) {
|
|
1519
|
+
out.model = model.trim();
|
|
1520
|
+
}
|
|
1521
|
+
return out;
|
|
1522
|
+
}
|
|
1523
|
+
return {};
|
|
1524
|
+
} catch {
|
|
1525
|
+
return {};
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/inputController.ts
|
|
1530
|
+
var InputController = class {
|
|
1531
|
+
queue = [];
|
|
1532
|
+
pendingResolve = null;
|
|
1533
|
+
closed = false;
|
|
1534
|
+
/** User messages buffered but not yet yielded to the SDK iterator. */
|
|
1535
|
+
get queueSize() {
|
|
1536
|
+
return this.queue.length;
|
|
1537
|
+
}
|
|
1538
|
+
push(content, sessionId, onYielded) {
|
|
1539
|
+
if (this.closed) return;
|
|
1540
|
+
const msg = {
|
|
1541
|
+
type: "user",
|
|
1542
|
+
session_id: sessionId,
|
|
1543
|
+
message: { role: "user", content },
|
|
1544
|
+
parent_tool_use_id: null
|
|
1545
|
+
};
|
|
1546
|
+
const entry = { msg, onYielded };
|
|
1547
|
+
if (this.pendingResolve) {
|
|
1548
|
+
const resolve = this.pendingResolve;
|
|
1549
|
+
this.pendingResolve = null;
|
|
1550
|
+
resolve(entry);
|
|
1551
|
+
} else {
|
|
1552
|
+
this.queue.push(entry);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
close() {
|
|
1556
|
+
this.closed = true;
|
|
1557
|
+
if (this.pendingResolve) {
|
|
1558
|
+
const resolve = this.pendingResolve;
|
|
1559
|
+
this.pendingResolve = null;
|
|
1560
|
+
resolve(null);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async *[Symbol.asyncIterator]() {
|
|
1564
|
+
while (!this.closed) {
|
|
1565
|
+
let entry;
|
|
1566
|
+
if (this.queue.length > 0) {
|
|
1567
|
+
entry = this.queue.shift();
|
|
1568
|
+
} else {
|
|
1569
|
+
entry = await new Promise((resolve) => {
|
|
1570
|
+
if (this.closed) {
|
|
1571
|
+
resolve(null);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.pendingResolve = resolve;
|
|
1575
|
+
});
|
|
1576
|
+
if (entry === null) break;
|
|
1577
|
+
}
|
|
1578
|
+
entry.onYielded?.();
|
|
1579
|
+
yield entry.msg;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
// src/askQuestionRegistry.ts
|
|
1585
|
+
var logger = createModuleLogger("askQuestionRegistry");
|
|
1586
|
+
var ASK_QUESTION_TIMEOUT_MS = 12e4;
|
|
1587
|
+
var TIMEOUT_ANSWER = "[User did not respond within 120 seconds. Please decide whether to proceed with reasonable defaults or skip this step.]";
|
|
1588
|
+
var AskQuestionRegistry = class {
|
|
1589
|
+
entries = /* @__PURE__ */ new Map();
|
|
1590
|
+
/** Register a pending question; always resolves (never rejects). */
|
|
1591
|
+
register(questionId, agentId, onTimeout, timeoutMs = ASK_QUESTION_TIMEOUT_MS) {
|
|
1592
|
+
return new Promise((resolve) => {
|
|
1593
|
+
const timer = setTimeout(() => {
|
|
1594
|
+
if (!this.entries.has(questionId)) return;
|
|
1595
|
+
this.entries.delete(questionId);
|
|
1596
|
+
logger.warn("AskQuestion timeout", { questionId, agentId, timeoutMs });
|
|
1597
|
+
try {
|
|
1598
|
+
onTimeout();
|
|
1599
|
+
} catch (e) {
|
|
1600
|
+
logger.error("onTimeout cb threw", { error: e });
|
|
1601
|
+
}
|
|
1602
|
+
resolve(TIMEOUT_ANSWER);
|
|
1603
|
+
}, timeoutMs);
|
|
1604
|
+
this.entries.set(questionId, { resolve, timer, agentId, askedAt: Date.now() });
|
|
1605
|
+
logger.info("AskQuestion registered", { questionId, agentId, timeoutMs });
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
resolve(questionId, answerText) {
|
|
1609
|
+
const entry = this.entries.get(questionId);
|
|
1610
|
+
if (!entry) {
|
|
1611
|
+
logger.warn("AskQuestion resolve: id not found (may be timed out)", { questionId });
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1614
|
+
clearTimeout(entry.timer);
|
|
1615
|
+
this.entries.delete(questionId);
|
|
1616
|
+
logger.info("AskQuestion resolved", {
|
|
1617
|
+
questionId,
|
|
1618
|
+
agentId: entry.agentId,
|
|
1619
|
+
waitedMs: Date.now() - entry.askedAt,
|
|
1620
|
+
answerLen: answerText.length,
|
|
1621
|
+
answerSample: answerText.slice(0, 200)
|
|
1622
|
+
});
|
|
1623
|
+
entry.resolve(answerText);
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
cancelAll(reason) {
|
|
1627
|
+
if (this.entries.size === 0) return;
|
|
1628
|
+
logger.warn("AskQuestion cancelAll", { reason, count: this.entries.size });
|
|
1629
|
+
for (const [, entry] of this.entries) {
|
|
1630
|
+
clearTimeout(entry.timer);
|
|
1631
|
+
entry.resolve(`[${reason}]`);
|
|
1632
|
+
}
|
|
1633
|
+
this.entries.clear();
|
|
1634
|
+
}
|
|
1635
|
+
cancelOne(questionId, reason) {
|
|
1636
|
+
const entry = this.entries.get(questionId);
|
|
1637
|
+
if (!entry) return false;
|
|
1638
|
+
clearTimeout(entry.timer);
|
|
1639
|
+
this.entries.delete(questionId);
|
|
1640
|
+
entry.resolve(`[${reason}]`);
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
size() {
|
|
1644
|
+
return this.entries.size;
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
// src/scope.ts
|
|
1649
|
+
function scopeKey(scope) {
|
|
1650
|
+
return scope.kind === "single" ? "single" : `group:${scope.groupId}`;
|
|
1651
|
+
}
|
|
1652
|
+
function runtimeKey(agentId, scope) {
|
|
1653
|
+
return `${agentId}::${scopeKey(scope)}`;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/askUserQuestionGuard.ts
|
|
1657
|
+
var logger2 = createModuleLogger("askUserQuestionGuard");
|
|
1658
|
+
function formatAnswerForSDK(p) {
|
|
1659
|
+
const parts = ["[User Response]"];
|
|
1660
|
+
if (p.selectedLabels.length > 0) {
|
|
1661
|
+
parts.push(`\u9009\u62E9\uFF1A${p.selectedLabels.join("\u3001")}`);
|
|
1662
|
+
}
|
|
1663
|
+
if (p.freeformText && p.freeformText.trim()) {
|
|
1664
|
+
parts.push(`\u5907\u6CE8\uFF1A${p.freeformText.trim()}`);
|
|
1665
|
+
}
|
|
1666
|
+
if (parts.length === 1) {
|
|
1667
|
+
parts.push("\uFF08\u7528\u6237\u672A\u9009\u62E9\u4EFB\u4F55\u9009\u9879\u4E5F\u672A\u586B\u5199\u5907\u6CE8\uFF09");
|
|
1668
|
+
}
|
|
1669
|
+
return parts.join("\n");
|
|
1670
|
+
}
|
|
1671
|
+
function makeAskUserQuestionGuard(deps) {
|
|
1672
|
+
return async (input) => {
|
|
1673
|
+
const task = deps.getCurrentTask();
|
|
1674
|
+
if (!task) {
|
|
1675
|
+
logger2.error("AskUserQuestion received but no currentTask", { agentId: deps.agentId });
|
|
1676
|
+
return { behavior: "deny", message: "[Internal error: no active task context]" };
|
|
1677
|
+
}
|
|
1678
|
+
const questions = input.questions ?? [];
|
|
1679
|
+
if (questions.length === 0) {
|
|
1680
|
+
logger2.warn("AskUserQuestion called with empty questions array", { agentId: deps.agentId });
|
|
1681
|
+
return { behavior: "deny", message: "[Internal error: empty questions]" };
|
|
1682
|
+
}
|
|
1683
|
+
if (questions.length > 1) {
|
|
1684
|
+
logger2.warn("AskUserQuestion received multi questions, Plan A only handles questions[0]", {
|
|
1685
|
+
agentId: deps.agentId,
|
|
1686
|
+
count: questions.length
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
const q = questions[0];
|
|
1690
|
+
const questionId = createAskQuestionId();
|
|
1691
|
+
const askedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1692
|
+
const options = (q.options ?? []).map((o) => ({
|
|
1693
|
+
label: o.label,
|
|
1694
|
+
description: o.description
|
|
1695
|
+
}));
|
|
1696
|
+
const multiSelect = Boolean(q.multiSelect);
|
|
1697
|
+
logger2.info("AskUserQuestion intercepted, emitting agent:ask_user_question", {
|
|
1698
|
+
agentId: deps.agentId,
|
|
1699
|
+
scope: scopeKey(deps.scope),
|
|
1700
|
+
groupId: task.groupId,
|
|
1701
|
+
questionId,
|
|
1702
|
+
replyMessageId: task.replyMessageId,
|
|
1703
|
+
question: q.question.slice(0, 200),
|
|
1704
|
+
optionCount: options.length,
|
|
1705
|
+
multiSelect,
|
|
1706
|
+
traceId: task.traceId
|
|
1707
|
+
});
|
|
1708
|
+
deps.emit({
|
|
1709
|
+
type: "agent:ask_user_question",
|
|
1710
|
+
payload: {
|
|
1711
|
+
questionId,
|
|
1712
|
+
replyMessageId: task.replyMessageId,
|
|
1713
|
+
agentId: deps.agentId,
|
|
1714
|
+
conversationId: task.conversationId,
|
|
1715
|
+
groupId: task.groupId,
|
|
1716
|
+
question: q.question,
|
|
1717
|
+
header: q.header,
|
|
1718
|
+
options,
|
|
1719
|
+
multiSelect,
|
|
1720
|
+
askedAt,
|
|
1721
|
+
timeoutMs: ASK_QUESTION_TIMEOUT_MS,
|
|
1722
|
+
traceId: task.traceId
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
logger2.info("AskUserQuestion agent status awaiting_user", {
|
|
1726
|
+
agentId: deps.agentId,
|
|
1727
|
+
questionId,
|
|
1728
|
+
groupId: task.groupId,
|
|
1729
|
+
traceId: task.traceId
|
|
1730
|
+
});
|
|
1731
|
+
deps.emit({
|
|
1732
|
+
type: "agent:status",
|
|
1733
|
+
payload: { agentId: deps.agentId, status: "awaiting_user" }
|
|
1734
|
+
});
|
|
1735
|
+
const answerText = await deps.registry.register(questionId, deps.agentId, () => {
|
|
1736
|
+
deps.emit({
|
|
1737
|
+
type: "ask_question_updated",
|
|
1738
|
+
payload: {
|
|
1739
|
+
questionId,
|
|
1740
|
+
agentId: deps.agentId,
|
|
1741
|
+
conversationId: task.conversationId,
|
|
1742
|
+
status: "timeout",
|
|
1743
|
+
cancelReason: "timeout",
|
|
1744
|
+
traceId: task.traceId
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
});
|
|
1748
|
+
logger2.info("AskUserQuestion agent status thinking (resume SDK)", {
|
|
1749
|
+
agentId: deps.agentId,
|
|
1750
|
+
questionId,
|
|
1751
|
+
groupId: task.groupId,
|
|
1752
|
+
traceId: task.traceId
|
|
1753
|
+
});
|
|
1754
|
+
deps.emit({
|
|
1755
|
+
type: "agent:status",
|
|
1756
|
+
payload: { agentId: deps.agentId, status: "thinking" }
|
|
1757
|
+
});
|
|
1758
|
+
logger2.info("AskUserQuestion answered, returning deny+message to SDK", {
|
|
1759
|
+
agentId: deps.agentId,
|
|
1760
|
+
questionId,
|
|
1761
|
+
replyMessageId: task.replyMessageId,
|
|
1762
|
+
answerSample: answerText.slice(0, 200),
|
|
1763
|
+
traceId: task.traceId
|
|
1764
|
+
});
|
|
1765
|
+
return { behavior: "deny", message: answerText };
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/permissionGuard.ts
|
|
1770
|
+
import path5 from "path";
|
|
1771
|
+
|
|
1772
|
+
// ../shared/src/utils/pathSafety.ts
|
|
1773
|
+
import path4 from "path";
|
|
1774
|
+
function isPathInside(parent, child) {
|
|
1775
|
+
const resolvedParent = path4.resolve(parent);
|
|
1776
|
+
const resolvedChild = path4.resolve(child);
|
|
1777
|
+
if (resolvedParent === resolvedChild) return true;
|
|
1778
|
+
const rel = path4.relative(resolvedParent, resolvedChild);
|
|
1779
|
+
if (rel === "") return true;
|
|
1780
|
+
return !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// src/permissionGuard.ts
|
|
1784
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1785
|
+
function makeCwdPermissionGuard(cwd, agentId, scope, log) {
|
|
1786
|
+
const scopeStr = scopeKey(scope);
|
|
1787
|
+
return async (toolName, input) => {
|
|
1788
|
+
if (!WRITE_TOOLS.has(toolName)) {
|
|
1789
|
+
return { behavior: "allow" };
|
|
1790
|
+
}
|
|
1791
|
+
const raw = input.file_path ?? input.path ?? input.notebook_path;
|
|
1792
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1793
|
+
return { behavior: "allow" };
|
|
1794
|
+
}
|
|
1795
|
+
const abs = path5.isAbsolute(raw) ? raw : path5.resolve(cwd, raw);
|
|
1796
|
+
if (isPathInside(cwd, abs)) {
|
|
1797
|
+
return { behavior: "allow" };
|
|
1798
|
+
}
|
|
1799
|
+
log("canUseTool deny: write outside cwd", { agentId, scope: scopeStr, toolName, target: raw, abs, cwd });
|
|
1800
|
+
return {
|
|
1801
|
+
behavior: "deny",
|
|
1802
|
+
message: `\u5DE5\u4F5C\u76EE\u5F55\u5916\u4E0D\u53EF\u5199\u5165\uFF1A${abs}\uFF08cwd=${cwd}\uFF09`
|
|
1803
|
+
};
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/neuralMcpServer.ts
|
|
1808
|
+
var logger3 = createModuleLogger("neural.mcpServer");
|
|
1809
|
+
function formatScopeLabel(key, groupName) {
|
|
1810
|
+
if (key === "single") return "\u5355\u804A";
|
|
1811
|
+
if (groupName) return `\u7FA4\u300C${groupName}\u300D`;
|
|
1812
|
+
if (key.startsWith("group:")) return `\u7FA4\u300C${key.slice(6)}\u300D`;
|
|
1813
|
+
return key;
|
|
1814
|
+
}
|
|
1815
|
+
async function createNeuralMcpServer(deps) {
|
|
1816
|
+
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
1817
|
+
const { z } = await import("zod");
|
|
1818
|
+
const currentScopeKey = scopeKey(deps.scope);
|
|
1819
|
+
const currentScopeLabel = formatScopeLabel(currentScopeKey);
|
|
1820
|
+
const neuralSend = sdk.tool(
|
|
1821
|
+
"neural_send",
|
|
1822
|
+
`\u628A\u4E00\u6BB5\u8BDD\u9001\u8FBE"\u4F60\u5728\u53E6\u4E00\u4E2A\u5BF9\u8BDD scope \u91CC\u7684\u5206\u8EAB"\u3002
|
|
1823
|
+
\u8FD9\u662F\u4F60\u5927\u8111\u5185\u7684\u79C1\u5BC6\u901A\u8BAF\uFF1A\u53D1\u9001\u5373\u9001\u8FBE\uFF0C\u4E0D\u7B49\u56DE\u4FE1\u3002\u5BF9\u65B9\u6536\u5230\u540E\u4F1A\u77E5\u9053\u8FD9\u662F\u4F60\u7684\u5185\u5FC3\u58F0\u97F3\uFF0C\u5E76\u81EA\u884C\u51B3\u5B9A\u5982\u4F55\u5728\u5B83\u90A3\u8FB9\u884C\u52A8 / \u662F\u5426\u8981\u518D\u7528 neural_send \u56DE\u8BDD\u7ED9\u4F60\u3002
|
|
1824
|
+
\u4F60\u5F53\u524D\u6240\u5728 scope: ${currentScopeKey} (${currentScopeLabel})\u3002target_scope \u4E0D\u80FD\u7B49\u4E8E\u4F60\u5F53\u524D scope\u3002`,
|
|
1825
|
+
{
|
|
1826
|
+
target_scope: z.string().describe(
|
|
1827
|
+
'\u76EE\u6807 scope\u3002"single" \u8868\u793A\u548C\u7528\u6237\u7684 1:1 \u5355\u804A\uFF1B"group:<\u7FA4 ID \u6216\u7FA4\u540D>" \u8868\u793A\u67D0\u4E2A\u7FA4\uFF08\u7FA4\u540D\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u65E0\u9700\u7CBE\u786E ID\uFF09\u3002'
|
|
1828
|
+
),
|
|
1829
|
+
message: z.string().describe('\u8981\u4F20\u7ED9\u76EE\u6807\u5206\u8EAB\u7684\u4E00\u6BB5\u81EA\u7136\u8BED\u8A00\u3002\u5B83\u4F1A\u4F5C\u4E3A\u4F60\u7684"\u5185\u5FC3\u72EC\u767D"\u51FA\u73B0\u5728\u90A3\u4E2A scope\u3002')
|
|
1830
|
+
},
|
|
1831
|
+
async (args) => {
|
|
1832
|
+
logger3.info("neural_send tool called", {
|
|
1833
|
+
agentId: deps.agentId,
|
|
1834
|
+
fromScope: currentScopeKey,
|
|
1835
|
+
rawTargetScope: args.target_scope,
|
|
1836
|
+
messageLen: args.message.length
|
|
1837
|
+
});
|
|
1838
|
+
const trimmed = args.message.trim();
|
|
1839
|
+
if (!trimmed) {
|
|
1840
|
+
return { content: [{ type: "text", text: "[neural_send] message \u4E0D\u80FD\u4E3A\u7A7A\u3002" }], isError: true };
|
|
1841
|
+
}
|
|
1842
|
+
let resolvedKey = args.target_scope;
|
|
1843
|
+
let conversationId;
|
|
1844
|
+
let groupId;
|
|
1845
|
+
let groupName;
|
|
1846
|
+
let targetCwd;
|
|
1847
|
+
if (args.target_scope === "single") {
|
|
1848
|
+
resolvedKey = "single";
|
|
1849
|
+
const singleConvId = await deps.groupRegistry.resolveSingleConversationId(deps.agentId);
|
|
1850
|
+
if (singleConvId) {
|
|
1851
|
+
conversationId = singleConvId;
|
|
1852
|
+
} else {
|
|
1853
|
+
logger3.warn("neural_send: failed to resolve single conv", { agentId: deps.agentId });
|
|
1854
|
+
}
|
|
1855
|
+
} else if (args.target_scope.startsWith("group:")) {
|
|
1856
|
+
const r = await deps.groupRegistry.resolveScope(args.target_scope);
|
|
1857
|
+
if (!r) {
|
|
1858
|
+
logger3.info("neural_send: target scope not found", { rawTargetScope: args.target_scope });
|
|
1859
|
+
return {
|
|
1860
|
+
content: [{ type: "text", text: `[neural_send] \u627E\u4E0D\u5230\u7FA4\u300C${args.target_scope.slice(6)}\u300D\u3002\u8BF7\u786E\u8BA4\u7FA4\u540D\u662F\u5426\u6B63\u786E\u3002` }],
|
|
1861
|
+
isError: true
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
resolvedKey = r.scopeKey;
|
|
1865
|
+
conversationId = r.conversationId;
|
|
1866
|
+
groupId = r.groupId;
|
|
1867
|
+
groupName = r.groupName;
|
|
1868
|
+
targetCwd = r.workingDirectory;
|
|
1869
|
+
} else {
|
|
1870
|
+
return {
|
|
1871
|
+
content: [{ type: "text", text: '[neural_send] target_scope \u5FC5\u987B\u662F "single" \u6216 "group:<\u7FA4\u540D\u6216 ID>"\u3002' }],
|
|
1872
|
+
isError: true
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
if (resolvedKey === currentScopeKey) {
|
|
1876
|
+
logger3.warn("neural_send: self-send refused", { agentId: deps.agentId, scope: currentScopeKey });
|
|
1877
|
+
return {
|
|
1878
|
+
content: [{ type: "text", text: "[neural_send] \u4E0D\u80FD\u628A\u6D88\u606F\u9001\u7ED9\u81EA\u5DF1\u5F53\u524D\u6240\u5728\u7684 scope\u3002" }],
|
|
1879
|
+
isError: true
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
const toLabel = formatScopeLabel(resolvedKey, groupName);
|
|
1883
|
+
try {
|
|
1884
|
+
await deps.onSend({
|
|
1885
|
+
fromScopeKey: currentScopeKey,
|
|
1886
|
+
fromScopeLabel: currentScopeLabel,
|
|
1887
|
+
toScopeKey: resolvedKey,
|
|
1888
|
+
toScopeLabel: toLabel,
|
|
1889
|
+
message: trimmed,
|
|
1890
|
+
conversationId,
|
|
1891
|
+
groupId,
|
|
1892
|
+
targetCwd
|
|
1893
|
+
});
|
|
1894
|
+
logger3.info("neural_send delivered", {
|
|
1895
|
+
agentId: deps.agentId,
|
|
1896
|
+
fromScope: currentScopeKey,
|
|
1897
|
+
toScope: resolvedKey,
|
|
1898
|
+
messageLen: trimmed.length
|
|
1899
|
+
});
|
|
1900
|
+
return {
|
|
1901
|
+
content: [{ type: "text", text: `[neural_send] \u5DF2\u9001\u8FBE\u5230\u300C${toLabel}\u300D(scope: ${resolvedKey})\u3002` }]
|
|
1902
|
+
};
|
|
1903
|
+
} catch (err) {
|
|
1904
|
+
logger3.error("neural_send dispatch failed", { agentId: deps.agentId, error: err });
|
|
1905
|
+
return {
|
|
1906
|
+
content: [{ type: "text", text: `[neural_send] \u9001\u8FBE\u5931\u8D25\uFF1A${err.message}` }],
|
|
1907
|
+
isError: true
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
{}
|
|
1912
|
+
);
|
|
1913
|
+
const neuralServer = sdk.createSdkMcpServer({
|
|
1914
|
+
name: "neural",
|
|
1915
|
+
version: "2.0.0",
|
|
1916
|
+
tools: [neuralSend]
|
|
1917
|
+
});
|
|
1918
|
+
logger3.info("Neural MCP server created", {
|
|
1919
|
+
agentId: deps.agentId,
|
|
1920
|
+
scope: currentScopeKey,
|
|
1921
|
+
tools: ["neural_send"]
|
|
1922
|
+
});
|
|
1923
|
+
return neuralServer;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/sdkEventMapper.ts
|
|
1927
|
+
var logger4 = createModuleLogger("sdk.mapper");
|
|
1928
|
+
function getTaskBase(proc) {
|
|
1929
|
+
const task = proc.currentTask;
|
|
1930
|
+
if (!task) return null;
|
|
1931
|
+
return {
|
|
1932
|
+
agentId: proc.agentId,
|
|
1933
|
+
conversationId: task.conversationId,
|
|
1934
|
+
traceId: task.traceId,
|
|
1935
|
+
replyMessageId: task.replyMessageId
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function wireBase(base) {
|
|
1939
|
+
return {
|
|
1940
|
+
ackId: base.replyMessageId,
|
|
1941
|
+
agentId: base.agentId,
|
|
1942
|
+
conversationId: base.conversationId,
|
|
1943
|
+
traceId: base.traceId
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function extractUsage(message) {
|
|
1947
|
+
const result = {};
|
|
1948
|
+
if (message.usage) {
|
|
1949
|
+
const u = message.usage;
|
|
1950
|
+
if (typeof u.output_tokens === "number") result.tokenCount = u.output_tokens;
|
|
1951
|
+
if (typeof u.input_tokens === "number") result.inputTokens = u.input_tokens;
|
|
1952
|
+
}
|
|
1953
|
+
if (typeof message.total_cost_usd === "number") {
|
|
1954
|
+
result.costUsd = message.total_cost_usd;
|
|
1955
|
+
}
|
|
1956
|
+
if (message.modelUsage) {
|
|
1957
|
+
const models = Object.keys(message.modelUsage);
|
|
1958
|
+
if (models.length > 0) result.model = models[0];
|
|
1959
|
+
}
|
|
1960
|
+
return result;
|
|
1961
|
+
}
|
|
1962
|
+
function isGroupTask(proc) {
|
|
1963
|
+
return proc.currentTask?.groupId != null;
|
|
1964
|
+
}
|
|
1965
|
+
function extractTodosFromInput(input) {
|
|
1966
|
+
if (!input || typeof input !== "object") return null;
|
|
1967
|
+
const raw = input.todos;
|
|
1968
|
+
if (!Array.isArray(raw)) return null;
|
|
1969
|
+
const out = [];
|
|
1970
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1971
|
+
const item = raw[i];
|
|
1972
|
+
if (!item || typeof item !== "object") continue;
|
|
1973
|
+
const it = item;
|
|
1974
|
+
if (typeof it.content !== "string") continue;
|
|
1975
|
+
const id = typeof it.id === "string" ? it.id : `todo_${i}`;
|
|
1976
|
+
const status = it.status === "in_progress" || it.status === "completed" || it.status === "cancelled" ? it.status : "pending";
|
|
1977
|
+
out.push({ id, content: it.content, status });
|
|
1978
|
+
}
|
|
1979
|
+
return out;
|
|
1980
|
+
}
|
|
1981
|
+
function countByStatus(todos) {
|
|
1982
|
+
const c = {
|
|
1983
|
+
pending: 0,
|
|
1984
|
+
in_progress: 0,
|
|
1985
|
+
completed: 0,
|
|
1986
|
+
cancelled: 0
|
|
1987
|
+
};
|
|
1988
|
+
for (const t of todos) {
|
|
1989
|
+
c[t.status] = (c[t.status] ?? 0) + 1;
|
|
1990
|
+
}
|
|
1991
|
+
return c;
|
|
1992
|
+
}
|
|
1993
|
+
function emitGroupSegment(proc, emit, base, content, contentBlocks) {
|
|
1994
|
+
const groupId = proc.currentTask?.groupId;
|
|
1995
|
+
if (!groupId) return;
|
|
1996
|
+
proc.segmentCount += 1;
|
|
1997
|
+
logger4.info("Group segment emitted", {
|
|
1998
|
+
agentId: base.agentId,
|
|
1999
|
+
replyMessageId: base.replyMessageId,
|
|
2000
|
+
groupId,
|
|
2001
|
+
segmentIndex: proc.segmentCount,
|
|
2002
|
+
contentLen: content.length,
|
|
2003
|
+
blockCount: contentBlocks.length,
|
|
2004
|
+
blockTypes: contentBlocks.map((b) => b.type),
|
|
2005
|
+
contentSample: content.slice(0, 200),
|
|
2006
|
+
traceId: base.traceId,
|
|
2007
|
+
isAuditOnly: content.length === 0
|
|
2008
|
+
});
|
|
2009
|
+
emit({
|
|
2010
|
+
type: "agent:segment",
|
|
2011
|
+
payload: {
|
|
2012
|
+
messageId: createMessageId(),
|
|
2013
|
+
...wireBase(base),
|
|
2014
|
+
groupId,
|
|
2015
|
+
content,
|
|
2016
|
+
contentBlocks: [...contentBlocks]
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
function flushTextSegmentOnBlockStop(proc, emit, base) {
|
|
2021
|
+
const trimmed = proc.segmentBuffer.trim();
|
|
2022
|
+
if (trimmed.length > 0 && trimmed !== NO_REPLY_TOKEN) {
|
|
2023
|
+
proc.contentBlocks.push({ type: "text", content: proc.segmentBuffer });
|
|
2024
|
+
emitGroupSegment(proc, emit, base, proc.segmentBuffer, proc.contentBlocks);
|
|
2025
|
+
proc.contentBlocks = [];
|
|
2026
|
+
} else {
|
|
2027
|
+
logger4.info("Group text block flushed but skipped (no segment emitted)", {
|
|
2028
|
+
agentId: base.agentId,
|
|
2029
|
+
replyMessageId: base.replyMessageId,
|
|
2030
|
+
groupId: proc.currentTask?.groupId,
|
|
2031
|
+
bufferLen: proc.segmentBuffer.length,
|
|
2032
|
+
trimmedLen: trimmed.length,
|
|
2033
|
+
reason: trimmed.length === 0 ? "empty" : "no_reply_token",
|
|
2034
|
+
sample: proc.segmentBuffer.slice(0, 200),
|
|
2035
|
+
traceId: base.traceId
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
proc.segmentBuffer = "";
|
|
2039
|
+
}
|
|
2040
|
+
function mapSDKMessage(proc, message, rawEmit, sessionStore, onCompleted) {
|
|
2041
|
+
const emit = rawEmit;
|
|
2042
|
+
switch (message.type) {
|
|
2043
|
+
case "system": {
|
|
2044
|
+
if (message.subtype === "init") {
|
|
2045
|
+
const initMsg = message;
|
|
2046
|
+
proc.ccSessionId = initMsg.session_id;
|
|
2047
|
+
sessionStore.set(proc.agentId, proc.scope, initMsg.session_id);
|
|
2048
|
+
if (proc.status === "starting") {
|
|
2049
|
+
proc.status = "ready";
|
|
2050
|
+
}
|
|
2051
|
+
logger4.info("Agent session initialized", {
|
|
2052
|
+
agentId: proc.agentId,
|
|
2053
|
+
scope: proc.scope.kind === "single" ? "single" : proc.scope.groupId,
|
|
2054
|
+
sessionId: initMsg.session_id,
|
|
2055
|
+
statusAfterInit: proc.status
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
break;
|
|
2059
|
+
}
|
|
2060
|
+
case "stream_event": {
|
|
2061
|
+
const base = getTaskBase(proc);
|
|
2062
|
+
if (!base) break;
|
|
2063
|
+
const ev = message.event;
|
|
2064
|
+
switch (ev.type) {
|
|
2065
|
+
case "content_block_start": {
|
|
2066
|
+
const block = ev.content_block;
|
|
2067
|
+
if (!block) break;
|
|
2068
|
+
if (block.type === "thinking") {
|
|
2069
|
+
proc.currentBlockType = "thinking";
|
|
2070
|
+
proc.accumulatedThinking = "";
|
|
2071
|
+
} else if (block.type === "text") {
|
|
2072
|
+
proc.currentBlockType = "text";
|
|
2073
|
+
if (isGroupTask(proc)) {
|
|
2074
|
+
proc.segmentBuffer = "";
|
|
2075
|
+
}
|
|
2076
|
+
} else if (block.type === "tool_use") {
|
|
2077
|
+
proc.currentBlockType = "tool_use";
|
|
2078
|
+
proc.currentToolName = block.name ?? "unknown";
|
|
2079
|
+
proc.accumulatedToolInput = "";
|
|
2080
|
+
const toolName = block.name ?? "unknown";
|
|
2081
|
+
emit({
|
|
2082
|
+
type: "agent:tool_use",
|
|
2083
|
+
payload: {
|
|
2084
|
+
...wireBase(base),
|
|
2085
|
+
toolName,
|
|
2086
|
+
input: {}
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
proc.contentBlocks.push({
|
|
2090
|
+
type: "tool_use",
|
|
2091
|
+
toolName,
|
|
2092
|
+
input: {},
|
|
2093
|
+
status: "running"
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
break;
|
|
2097
|
+
}
|
|
2098
|
+
case "content_block_delta": {
|
|
2099
|
+
const delta = ev.delta;
|
|
2100
|
+
if (!delta) break;
|
|
2101
|
+
if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
2102
|
+
proc.accumulatedThinking += delta.thinking;
|
|
2103
|
+
emit({
|
|
2104
|
+
type: "agent:thinking_chunk",
|
|
2105
|
+
payload: { ...wireBase(base), chunk: delta.thinking }
|
|
2106
|
+
});
|
|
2107
|
+
} else if (delta.type === "input_json_delta") {
|
|
2108
|
+
const partial = delta.partial_json;
|
|
2109
|
+
if (typeof partial === "string") {
|
|
2110
|
+
proc.accumulatedToolInput += partial;
|
|
2111
|
+
}
|
|
2112
|
+
} else if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
2113
|
+
if (proc.accumulatedText.length === 0) {
|
|
2114
|
+
logger4.info("Agent text stream started", {
|
|
2115
|
+
agentId: proc.agentId,
|
|
2116
|
+
replyMessageId: base.replyMessageId,
|
|
2117
|
+
traceId: base.traceId,
|
|
2118
|
+
groupMode: isGroupTask(proc)
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
proc.accumulatedText += delta.text;
|
|
2122
|
+
if (isGroupTask(proc)) {
|
|
2123
|
+
proc.segmentBuffer += delta.text;
|
|
2124
|
+
} else {
|
|
2125
|
+
emit({
|
|
2126
|
+
type: "agent:text_chunk",
|
|
2127
|
+
payload: { ...wireBase(base), chunk: delta.text }
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
2133
|
+
case "content_block_stop": {
|
|
2134
|
+
if (proc.currentBlockType === "thinking") {
|
|
2135
|
+
emit({
|
|
2136
|
+
type: "agent:thinking_done",
|
|
2137
|
+
payload: wireBase(getTaskBase(proc))
|
|
2138
|
+
});
|
|
2139
|
+
proc.contentBlocks.push({
|
|
2140
|
+
type: "thinking",
|
|
2141
|
+
content: proc.accumulatedThinking,
|
|
2142
|
+
isComplete: true
|
|
2143
|
+
});
|
|
2144
|
+
proc.accumulatedThinking = "";
|
|
2145
|
+
} else if (proc.currentBlockType === "text" && isGroupTask(proc)) {
|
|
2146
|
+
flushTextSegmentOnBlockStop(proc, emit, base);
|
|
2147
|
+
} else if (proc.currentBlockType === "tool_use") {
|
|
2148
|
+
let parsedInput = {};
|
|
2149
|
+
if (proc.accumulatedToolInput.length > 0) {
|
|
2150
|
+
try {
|
|
2151
|
+
parsedInput = JSON.parse(proc.accumulatedToolInput);
|
|
2152
|
+
} catch {
|
|
2153
|
+
logger4.warn("Failed to parse tool input JSON", {
|
|
2154
|
+
agentId: proc.agentId,
|
|
2155
|
+
toolName: proc.currentToolName,
|
|
2156
|
+
inputLen: proc.accumulatedToolInput.length,
|
|
2157
|
+
sample: proc.accumulatedToolInput.slice(0, 200)
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
const lastToolUse = [...proc.contentBlocks].reverse().find((bl) => bl.type === "tool_use");
|
|
2162
|
+
if (lastToolUse && lastToolUse.type === "tool_use") {
|
|
2163
|
+
lastToolUse.input = parsedInput;
|
|
2164
|
+
}
|
|
2165
|
+
if (proc.currentToolName === "TodoWrite") {
|
|
2166
|
+
const todos = extractTodosFromInput(parsedInput);
|
|
2167
|
+
if (todos) {
|
|
2168
|
+
logger4.info("TodoWrite detected, emitting agent:todos_update", {
|
|
2169
|
+
agentId: proc.agentId,
|
|
2170
|
+
replyMessageId: base.replyMessageId,
|
|
2171
|
+
groupId: proc.currentTask?.groupId,
|
|
2172
|
+
todoCount: todos.length,
|
|
2173
|
+
statusBreakdown: countByStatus(todos),
|
|
2174
|
+
traceId: base.traceId
|
|
2175
|
+
});
|
|
2176
|
+
emit({
|
|
2177
|
+
type: "agent:todos_update",
|
|
2178
|
+
payload: {
|
|
2179
|
+
...wireBase(base),
|
|
2180
|
+
groupId: proc.currentTask?.groupId,
|
|
2181
|
+
todos
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
} else {
|
|
2185
|
+
logger4.info("TodoWrite detected with empty/cancel todos", {
|
|
2186
|
+
agentId: proc.agentId,
|
|
2187
|
+
replyMessageId: base.replyMessageId,
|
|
2188
|
+
traceId: base.traceId
|
|
2189
|
+
});
|
|
2190
|
+
emit({
|
|
2191
|
+
type: "agent:todos_update",
|
|
2192
|
+
payload: {
|
|
2193
|
+
...wireBase(base),
|
|
2194
|
+
groupId: proc.currentTask?.groupId,
|
|
2195
|
+
todos: []
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (proc.currentToolName === "AskUserQuestion") {
|
|
2201
|
+
const last = proc.contentBlocks[proc.contentBlocks.length - 1];
|
|
2202
|
+
if (last?.type === "tool_use" && last.toolName === "AskUserQuestion") {
|
|
2203
|
+
proc.contentBlocks.pop();
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
proc.accumulatedToolInput = "";
|
|
2207
|
+
}
|
|
2208
|
+
proc.currentBlockType = null;
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
default:
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
case "user": {
|
|
2217
|
+
const base = getTaskBase(proc);
|
|
2218
|
+
if (!base) break;
|
|
2219
|
+
const userMsg = message;
|
|
2220
|
+
const content = userMsg.message?.content;
|
|
2221
|
+
if (Array.isArray(content)) {
|
|
2222
|
+
for (const block of content) {
|
|
2223
|
+
const b = block;
|
|
2224
|
+
if (b.type === "tool_result") {
|
|
2225
|
+
const output = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
2226
|
+
emit({
|
|
2227
|
+
type: "agent:tool_result",
|
|
2228
|
+
payload: {
|
|
2229
|
+
...wireBase(base),
|
|
2230
|
+
toolName: proc.currentToolName ?? "unknown",
|
|
2231
|
+
output,
|
|
2232
|
+
isError: !!b.is_error
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
proc.contentBlocks.push({
|
|
2236
|
+
type: "tool_result",
|
|
2237
|
+
toolName: proc.currentToolName ?? "unknown",
|
|
2238
|
+
output,
|
|
2239
|
+
isError: !!b.is_error
|
|
2240
|
+
});
|
|
2241
|
+
const lastToolUse = [...proc.contentBlocks].reverse().find((bl) => bl.type === "tool_use");
|
|
2242
|
+
if (lastToolUse && lastToolUse.type === "tool_use") {
|
|
2243
|
+
lastToolUse.status = b.is_error ? "error" : "done";
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
case "result": {
|
|
2251
|
+
const base = getTaskBase(proc);
|
|
2252
|
+
if (!base) break;
|
|
2253
|
+
const resultMsg = message;
|
|
2254
|
+
let carrierMessageId;
|
|
2255
|
+
if (resultMsg.subtype === "success") {
|
|
2256
|
+
const successMsg = resultMsg;
|
|
2257
|
+
const trimmed = proc.accumulatedText.trim();
|
|
2258
|
+
const groupId = proc.currentTask?.groupId;
|
|
2259
|
+
const groupMode = groupId != null;
|
|
2260
|
+
const usage = extractUsage(successMsg);
|
|
2261
|
+
if (trimmed === NO_REPLY_TOKEN) {
|
|
2262
|
+
logger4.info("Agent chose not to reply", {
|
|
2263
|
+
agentId: proc.agentId,
|
|
2264
|
+
replyMessageId: base.replyMessageId,
|
|
2265
|
+
traceId: base.traceId,
|
|
2266
|
+
groupMode,
|
|
2267
|
+
groupId,
|
|
2268
|
+
fullTextLen: proc.accumulatedText.length,
|
|
2269
|
+
fullTextSample: proc.accumulatedText.slice(0, 200),
|
|
2270
|
+
accumulatedBlockCount: proc.contentBlocks.length,
|
|
2271
|
+
accumulatedBlockTypes: proc.contentBlocks.map((b) => b.type)
|
|
2272
|
+
});
|
|
2273
|
+
emit({
|
|
2274
|
+
type: "agent:no_reply",
|
|
2275
|
+
payload: {
|
|
2276
|
+
...wireBase(base),
|
|
2277
|
+
groupId,
|
|
2278
|
+
reason: void 0
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
resetAccumulators(proc);
|
|
2282
|
+
onCompleted();
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
if (groupMode) {
|
|
2286
|
+
if (usage.inputTokens && usage.inputTokens > 15e4) {
|
|
2287
|
+
logger4.warn("Agent context window approaching limit", {
|
|
2288
|
+
agentId: proc.agentId,
|
|
2289
|
+
inputTokens: usage.inputTokens
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
if (proc.contentBlocks.length > 0) {
|
|
2293
|
+
logger4.info("Group turn trailing audit segment", {
|
|
2294
|
+
agentId: proc.agentId,
|
|
2295
|
+
replyMessageId: base.replyMessageId,
|
|
2296
|
+
blockCount: proc.contentBlocks.length,
|
|
2297
|
+
traceId: base.traceId
|
|
2298
|
+
});
|
|
2299
|
+
emitGroupSegment(proc, emit, base, "", proc.contentBlocks);
|
|
2300
|
+
proc.contentBlocks = [];
|
|
2301
|
+
}
|
|
2302
|
+
logger4.info("Group task turn complete", {
|
|
2303
|
+
agentId: proc.agentId,
|
|
2304
|
+
replyMessageId: base.replyMessageId,
|
|
2305
|
+
groupId,
|
|
2306
|
+
segmentCount: proc.segmentCount,
|
|
2307
|
+
fullTextLen: proc.accumulatedText.length,
|
|
2308
|
+
fullTextSample: proc.accumulatedText.slice(0, 200),
|
|
2309
|
+
traceId: base.traceId
|
|
2310
|
+
});
|
|
2311
|
+
emit({
|
|
2312
|
+
type: "agent:turn_complete",
|
|
2313
|
+
payload: {
|
|
2314
|
+
...wireBase(base),
|
|
2315
|
+
groupId,
|
|
2316
|
+
segmentCount: proc.segmentCount
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
resetAccumulators(proc);
|
|
2320
|
+
onCompleted();
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
if (proc.accumulatedText) {
|
|
2324
|
+
proc.contentBlocks.push({ type: "text", content: proc.accumulatedText });
|
|
2325
|
+
}
|
|
2326
|
+
if (usage.inputTokens && usage.inputTokens > 15e4) {
|
|
2327
|
+
logger4.warn("Agent context window approaching limit", {
|
|
2328
|
+
agentId: proc.agentId,
|
|
2329
|
+
inputTokens: usage.inputTokens
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
carrierMessageId = createMessageId();
|
|
2333
|
+
logger4.info("Agent task done, emitting agent:done", {
|
|
2334
|
+
agentId: proc.agentId,
|
|
2335
|
+
ackId: base.replyMessageId,
|
|
2336
|
+
messageId: carrierMessageId,
|
|
2337
|
+
textLen: proc.accumulatedText.length,
|
|
2338
|
+
textSample: proc.accumulatedText.slice(0, 200),
|
|
2339
|
+
tokenCount: usage.tokenCount,
|
|
2340
|
+
traceId: base.traceId
|
|
2341
|
+
});
|
|
2342
|
+
emit({
|
|
2343
|
+
type: "agent:done",
|
|
2344
|
+
payload: {
|
|
2345
|
+
...wireBase(base),
|
|
2346
|
+
messageId: carrierMessageId,
|
|
2347
|
+
fullContent: proc.accumulatedText,
|
|
2348
|
+
contentBlocks: proc.contentBlocks,
|
|
2349
|
+
metadata: {
|
|
2350
|
+
thinkingDuration: Date.now() - proc.currentTaskStartedAt,
|
|
2351
|
+
toolCallCount: proc.contentBlocks.filter((b) => b.type === "tool_use").length,
|
|
2352
|
+
tokenCount: usage.tokenCount,
|
|
2353
|
+
model: usage.model
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
} else {
|
|
2358
|
+
const errorMsg = resultMsg;
|
|
2359
|
+
const errorText = errorMsg.errors?.join("; ") ?? `Agent error: ${resultMsg.subtype}`;
|
|
2360
|
+
logger4.warn("Agent task error, emitting agent:error", {
|
|
2361
|
+
agentId: proc.agentId,
|
|
2362
|
+
replyMessageId: base.replyMessageId,
|
|
2363
|
+
subtype: resultMsg.subtype,
|
|
2364
|
+
errorText,
|
|
2365
|
+
traceId: base.traceId
|
|
2366
|
+
});
|
|
2367
|
+
emit({
|
|
2368
|
+
type: "agent:error",
|
|
2369
|
+
payload: { ...wireBase(base), error: errorText }
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
resetAccumulators(proc);
|
|
2373
|
+
onCompleted(carrierMessageId);
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
2376
|
+
case "assistant":
|
|
2377
|
+
break;
|
|
2378
|
+
default:
|
|
2379
|
+
logger4.warn("Unhandled SDK message type", {
|
|
2380
|
+
type: message.type,
|
|
2381
|
+
agentId: proc.agentId
|
|
2382
|
+
});
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function resetAccumulators(proc) {
|
|
2387
|
+
proc.accumulatedText = "";
|
|
2388
|
+
proc.accumulatedThinking = "";
|
|
2389
|
+
proc.contentBlocks = [];
|
|
2390
|
+
proc.currentBlockType = null;
|
|
2391
|
+
proc.currentToolName = null;
|
|
2392
|
+
proc.segmentBuffer = "";
|
|
2393
|
+
proc.segmentCount = 0;
|
|
2394
|
+
proc.accumulatedToolInput = "";
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/wsMetrics.ts
|
|
2398
|
+
import { monitorEventLoopDelay } from "perf_hooks";
|
|
2399
|
+
var logger5 = createModuleLogger("ws.metrics");
|
|
2400
|
+
var WsMetrics = class {
|
|
2401
|
+
recv = /* @__PURE__ */ new Map();
|
|
2402
|
+
send = /* @__PURE__ */ new Map();
|
|
2403
|
+
sdkOut = /* @__PURE__ */ new Map();
|
|
2404
|
+
timer = null;
|
|
2405
|
+
loopHist = null;
|
|
2406
|
+
start(intervalMs = 5e3) {
|
|
2407
|
+
if (this.timer) return;
|
|
2408
|
+
this.loopHist = monitorEventLoopDelay({ resolution: 20 });
|
|
2409
|
+
this.loopHist.enable();
|
|
2410
|
+
this.timer = setInterval(() => this.flush(intervalMs), intervalMs);
|
|
2411
|
+
}
|
|
2412
|
+
stop() {
|
|
2413
|
+
if (this.timer) {
|
|
2414
|
+
clearInterval(this.timer);
|
|
2415
|
+
this.timer = null;
|
|
2416
|
+
}
|
|
2417
|
+
this.loopHist?.disable();
|
|
2418
|
+
this.loopHist = null;
|
|
2419
|
+
}
|
|
2420
|
+
incRecv(type) {
|
|
2421
|
+
this.recv.set(type, (this.recv.get(type) ?? 0) + 1);
|
|
2422
|
+
}
|
|
2423
|
+
incSend(type) {
|
|
2424
|
+
this.send.set(type, (this.send.get(type) ?? 0) + 1);
|
|
2425
|
+
}
|
|
2426
|
+
incSdkOut(type) {
|
|
2427
|
+
this.sdkOut.set(type, (this.sdkOut.get(type) ?? 0) + 1);
|
|
2428
|
+
}
|
|
2429
|
+
mapToObj(m) {
|
|
2430
|
+
const out = {};
|
|
2431
|
+
for (const [k, v] of m) out[k] = v;
|
|
2432
|
+
return out;
|
|
2433
|
+
}
|
|
2434
|
+
flush(intervalMs) {
|
|
2435
|
+
const hist = this.loopHist;
|
|
2436
|
+
const stats = hist ? {
|
|
2437
|
+
loopMaxMs: Math.round(hist.max / 1e6),
|
|
2438
|
+
loopP99Ms: Math.round(hist.percentile(99) / 1e6),
|
|
2439
|
+
loopMeanMs: Math.round(hist.mean / 1e6)
|
|
2440
|
+
} : {};
|
|
2441
|
+
if (hist) hist.reset();
|
|
2442
|
+
const recvSum = [...this.recv.values()].reduce((a, b) => a + b, 0);
|
|
2443
|
+
const sendSum = [...this.send.values()].reduce((a, b) => a + b, 0);
|
|
2444
|
+
const sdkSum = [...this.sdkOut.values()].reduce((a, b) => a + b, 0);
|
|
2445
|
+
if (recvSum + sendSum + sdkSum === 0 && (stats.loopMaxMs ?? 0) < 50) return;
|
|
2446
|
+
logger5.info("WS metrics", {
|
|
2447
|
+
windowMs: intervalMs,
|
|
2448
|
+
...stats,
|
|
2449
|
+
sums: { recv: recvSum, send: sendSum, sdkOut: sdkSum },
|
|
2450
|
+
recv: this.mapToObj(this.recv),
|
|
2451
|
+
send: this.mapToObj(this.send),
|
|
2452
|
+
sdkOut: this.mapToObj(this.sdkOut)
|
|
2453
|
+
});
|
|
2454
|
+
this.recv.clear();
|
|
2455
|
+
this.send.clear();
|
|
2456
|
+
this.sdkOut.clear();
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
var wsMetrics = new WsMetrics();
|
|
2460
|
+
|
|
2461
|
+
// src/agentManager.ts
|
|
2462
|
+
var logger6 = createModuleLogger("agent.manager");
|
|
2463
|
+
var BridgeBusyError = class extends Error {
|
|
2464
|
+
constructor(message = "Bridge busy: cannot evict an idle Agent query; all slots are working") {
|
|
2465
|
+
super(message);
|
|
2466
|
+
this.name = "BridgeBusyError";
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
var AgentManager = class {
|
|
2470
|
+
agents = /* @__PURE__ */ new Map();
|
|
2471
|
+
lastUsedAt = /* @__PURE__ */ new Map();
|
|
2472
|
+
sessionStore;
|
|
2473
|
+
emit;
|
|
2474
|
+
workspacesDir;
|
|
2475
|
+
claudeConfigDir;
|
|
2476
|
+
queryConfig;
|
|
2477
|
+
askQuestionRegistry;
|
|
2478
|
+
groupRegistry;
|
|
2479
|
+
evictionTimer = null;
|
|
2480
|
+
// Lazy-loaded SDK query function. Injectable via constructor for tests.
|
|
2481
|
+
queryFn = null;
|
|
2482
|
+
constructor(sessionStore, emit, options) {
|
|
2483
|
+
this.sessionStore = sessionStore;
|
|
2484
|
+
this.emit = emit;
|
|
2485
|
+
if (typeof options === "function") {
|
|
2486
|
+
this.queryFn = options;
|
|
2487
|
+
this.workspacesDir = path6.join(os4.homedir(), ".ahchat", "workspaces");
|
|
2488
|
+
this.claudeConfigDir = path6.join(os4.homedir(), ".ahchat", "claude-config");
|
|
2489
|
+
this.queryConfig = DEFAULT_QUERY_CONFIG;
|
|
2490
|
+
this.askQuestionRegistry = new AskQuestionRegistry();
|
|
2491
|
+
this.groupRegistry = null;
|
|
2492
|
+
} else {
|
|
2493
|
+
this.queryFn = options?.queryFn ?? null;
|
|
2494
|
+
this.workspacesDir = options?.workspacesDir ?? path6.join(os4.homedir(), ".ahchat", "workspaces");
|
|
2495
|
+
this.claudeConfigDir = options?.claudeConfigDir ?? path6.join(os4.homedir(), ".ahchat", "claude-config");
|
|
2496
|
+
this.queryConfig = options?.queryConfig ?? DEFAULT_QUERY_CONFIG;
|
|
2497
|
+
this.askQuestionRegistry = options?.askQuestionRegistry ?? new AskQuestionRegistry();
|
|
2498
|
+
this.groupRegistry = options?.groupRegistry ?? null;
|
|
2499
|
+
}
|
|
2500
|
+
this.evictionTimer = setInterval(() => {
|
|
2501
|
+
void this.evictIdle();
|
|
2502
|
+
}, this.queryConfig.evictionIntervalMs);
|
|
2503
|
+
}
|
|
2504
|
+
async getQueryFn() {
|
|
2505
|
+
if (this.queryFn) return this.queryFn;
|
|
2506
|
+
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
2507
|
+
this.queryFn = sdk.query;
|
|
2508
|
+
return this.queryFn;
|
|
2509
|
+
}
|
|
2510
|
+
/** Count live queries (anything not dead / removed). */
|
|
2511
|
+
countActiveQueries() {
|
|
2512
|
+
let n = 0;
|
|
2513
|
+
for (const p of this.agents.values()) {
|
|
2514
|
+
if (p.status !== "dead") n++;
|
|
2515
|
+
}
|
|
2516
|
+
return n;
|
|
2517
|
+
}
|
|
2518
|
+
asRuntime(proc) {
|
|
2519
|
+
return proc;
|
|
2520
|
+
}
|
|
2521
|
+
async awaitQueryReturn(query, timeoutMs, agentId) {
|
|
2522
|
+
const ret = query.return(void 0);
|
|
2523
|
+
try {
|
|
2524
|
+
await Promise.race([
|
|
2525
|
+
ret,
|
|
2526
|
+
new Promise((_, reject) => {
|
|
2527
|
+
setTimeout(() => reject(new Error("query return timeout")), timeoutMs);
|
|
2528
|
+
})
|
|
2529
|
+
]);
|
|
2530
|
+
} catch (e) {
|
|
2531
|
+
logger6.warn("awaitQueryReturn finished with error/timeout", { agentId, error: e });
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Returns true when an agent process occupies a slot but is not actively doing work
|
|
2536
|
+
* and can be safely evicted to free up capacity.
|
|
2537
|
+
*
|
|
2538
|
+
* Both 'ready' (warm, finished a task) and 'starting' (pre-warmed at recovery but
|
|
2539
|
+
* never sent a message) qualify, as long as there are no injected tasks awaiting a turn.
|
|
2540
|
+
*/
|
|
2541
|
+
isEvictable(proc) {
|
|
2542
|
+
if (proc.status !== "ready" && proc.status !== "starting") return false;
|
|
2543
|
+
const runtime = this.asRuntime(proc);
|
|
2544
|
+
return runtime.injectedTasks.length === 0;
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Close an idle/starting query: end generator and drop from map.
|
|
2548
|
+
* Session id stays in SessionStore for resume (Phase 2 eviction).
|
|
2549
|
+
*/
|
|
2550
|
+
async closeIdleQuery(key) {
|
|
2551
|
+
const proc = this.agents.get(key);
|
|
2552
|
+
if (!proc || proc.status === "dead") return;
|
|
2553
|
+
if (!this.isEvictable(proc)) return;
|
|
2554
|
+
logger6.info("Evicting idle Agent query", { agentId: proc.agentId, scope: scopeKey(proc.scope) });
|
|
2555
|
+
const runtime = this.asRuntime(proc);
|
|
2556
|
+
try {
|
|
2557
|
+
runtime.inputController.close();
|
|
2558
|
+
await this.awaitQueryReturn(runtime.query, 5e3, proc.agentId);
|
|
2559
|
+
} catch (e) {
|
|
2560
|
+
logger6.error("closeIdleQuery failed", { agentId: proc.agentId, error: e });
|
|
2561
|
+
}
|
|
2562
|
+
proc.status = "dead";
|
|
2563
|
+
this.agents.delete(key);
|
|
2564
|
+
this.lastUsedAt.delete(key);
|
|
2565
|
+
}
|
|
2566
|
+
/** Evict LRU among idle (ready/starting + no injected tasks) agents past the idle timeout. */
|
|
2567
|
+
evictIdle() {
|
|
2568
|
+
const now = Date.now();
|
|
2569
|
+
const { idleTimeoutMs } = this.queryConfig;
|
|
2570
|
+
for (const [key, proc] of this.agents) {
|
|
2571
|
+
if (!this.isEvictable(proc)) continue;
|
|
2572
|
+
const runtime = this.asRuntime(proc);
|
|
2573
|
+
const last = this.lastUsedAt.get(key) ?? runtime.createdAt ?? 0;
|
|
2574
|
+
if (now - last <= idleTimeoutMs) continue;
|
|
2575
|
+
void this.closeIdleQuery(key);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* Evict one LRU candidate to make room for a new query. Returns false if none evictable.
|
|
2580
|
+
*/
|
|
2581
|
+
async evictOneLruReadyIdle() {
|
|
2582
|
+
let bestKey = null;
|
|
2583
|
+
let bestTs = Number.POSITIVE_INFINITY;
|
|
2584
|
+
for (const [key, proc] of this.agents) {
|
|
2585
|
+
if (!this.isEvictable(proc)) continue;
|
|
2586
|
+
const runtime = this.asRuntime(proc);
|
|
2587
|
+
const ts = this.lastUsedAt.get(key) ?? runtime.createdAt ?? 0;
|
|
2588
|
+
if (ts < bestTs) {
|
|
2589
|
+
bestTs = ts;
|
|
2590
|
+
bestKey = key;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
if (!bestKey) return false;
|
|
2594
|
+
await this.closeIdleQuery(bestKey);
|
|
2595
|
+
return true;
|
|
2596
|
+
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Ensure an Agent query exists (respecting maxActive via LRU eviction of idle queries).
|
|
2599
|
+
*/
|
|
2600
|
+
async acquire(agentConfig, scope, cwd) {
|
|
2601
|
+
const key = runtimeKey(agentConfig.id, scope);
|
|
2602
|
+
const existing = this.agents.get(key);
|
|
2603
|
+
if (existing && existing.status !== "dead") {
|
|
2604
|
+
this.lastUsedAt.set(key, Date.now());
|
|
2605
|
+
return existing;
|
|
2606
|
+
}
|
|
2607
|
+
while (this.countActiveQueries() >= this.queryConfig.maxActive) {
|
|
2608
|
+
const evicted = await this.evictOneLruReadyIdle();
|
|
2609
|
+
if (!evicted) {
|
|
2610
|
+
throw new BridgeBusyError();
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
const proc = await this.getOrCreate(agentConfig, scope, cwd);
|
|
2614
|
+
this.lastUsedAt.set(key, Date.now());
|
|
2615
|
+
return proc;
|
|
2616
|
+
}
|
|
2617
|
+
async getOrCreate(agentConfig, scope, cwd) {
|
|
2618
|
+
const key = runtimeKey(agentConfig.id, scope);
|
|
2619
|
+
const existing = this.agents.get(key);
|
|
2620
|
+
if (existing && existing.status !== "dead") {
|
|
2621
|
+
return existing;
|
|
2622
|
+
}
|
|
2623
|
+
const savedSessionId = this.sessionStore.get(agentConfig.id, scope);
|
|
2624
|
+
const inputController = new InputController();
|
|
2625
|
+
const agentCwd = cwd;
|
|
2626
|
+
await fs2.mkdir(agentCwd, { recursive: true });
|
|
2627
|
+
const cfg = parseAgentConfig(agentConfig.config);
|
|
2628
|
+
logger6.info("Creating Agent query", {
|
|
2629
|
+
agentId: agentConfig.id,
|
|
2630
|
+
scope: scopeKey(scope),
|
|
2631
|
+
cwd: agentCwd,
|
|
2632
|
+
resume: !!savedSessionId,
|
|
2633
|
+
sessionId: savedSessionId,
|
|
2634
|
+
model: cfg.model ?? "(default)"
|
|
2635
|
+
});
|
|
2636
|
+
const queryFn = await this.getQueryFn();
|
|
2637
|
+
let procRef = null;
|
|
2638
|
+
const cwdGuard = makeCwdPermissionGuard(agentCwd, agentConfig.id, scope, (msg, meta) => {
|
|
2639
|
+
logger6.warn(msg, meta);
|
|
2640
|
+
});
|
|
2641
|
+
const askGuard = makeAskUserQuestionGuard({
|
|
2642
|
+
agentId: agentConfig.id,
|
|
2643
|
+
scope,
|
|
2644
|
+
registry: this.askQuestionRegistry,
|
|
2645
|
+
getCurrentTask: () => procRef?.currentTask ?? null,
|
|
2646
|
+
emit: this.emit
|
|
2647
|
+
});
|
|
2648
|
+
const neuralServer = await createNeuralMcpServer({
|
|
2649
|
+
agentId: agentConfig.id,
|
|
2650
|
+
scope,
|
|
2651
|
+
groupRegistry: this.groupRegistry,
|
|
2652
|
+
onSend: (payload) => this.deliverNeuralSend(agentConfig, payload)
|
|
2653
|
+
});
|
|
2654
|
+
const options = {
|
|
2655
|
+
cwd: agentCwd,
|
|
2656
|
+
systemPrompt: {
|
|
2657
|
+
type: "preset",
|
|
2658
|
+
preset: "claude_code",
|
|
2659
|
+
append: [PLATFORM_AGENT_RULES, agentConfig.systemPrompt].filter((s) => typeof s === "string" && s.trim().length > 0).join("\n\n")
|
|
2660
|
+
},
|
|
2661
|
+
permissionMode: "bypassPermissions",
|
|
2662
|
+
allowDangerouslySkipPermissions: true,
|
|
2663
|
+
allowedTools: [
|
|
2664
|
+
"Read",
|
|
2665
|
+
"Edit",
|
|
2666
|
+
"Write",
|
|
2667
|
+
"Bash",
|
|
2668
|
+
"Glob",
|
|
2669
|
+
"Grep",
|
|
2670
|
+
"AskUserQuestion",
|
|
2671
|
+
"mcp__neural__neural_send"
|
|
2672
|
+
],
|
|
2673
|
+
mcpServers: { neural: neuralServer },
|
|
2674
|
+
includePartialMessages: true,
|
|
2675
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: this.claudeConfigDir },
|
|
2676
|
+
canUseTool: async (toolName, input) => {
|
|
2677
|
+
if (toolName === "AskUserQuestion") {
|
|
2678
|
+
return askGuard(input);
|
|
2679
|
+
}
|
|
2680
|
+
return cwdGuard(toolName, input);
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
const userPromptTrimmed = (agentConfig.systemPrompt ?? "").trim();
|
|
2684
|
+
const appendStr = options.systemPrompt.append;
|
|
2685
|
+
logger6.info("Platform rules attached", {
|
|
2686
|
+
agentId: agentConfig.id,
|
|
2687
|
+
scope: scopeKey(scope),
|
|
2688
|
+
platformRulesLen: PLATFORM_AGENT_RULES.length,
|
|
2689
|
+
userPromptLen: userPromptTrimmed.length,
|
|
2690
|
+
hasUserPrompt: userPromptTrimmed.length > 0,
|
|
2691
|
+
appendLen: appendStr.length
|
|
2692
|
+
});
|
|
2693
|
+
if (cfg.model) {
|
|
2694
|
+
options.model = cfg.model;
|
|
2695
|
+
}
|
|
2696
|
+
if (savedSessionId) {
|
|
2697
|
+
options.resume = savedSessionId;
|
|
2698
|
+
}
|
|
2699
|
+
const agentQuery = queryFn({
|
|
2700
|
+
prompt: inputController,
|
|
2701
|
+
options
|
|
2702
|
+
});
|
|
2703
|
+
const proc = {
|
|
2704
|
+
agentId: agentConfig.id,
|
|
2705
|
+
scope,
|
|
2706
|
+
cwd: agentCwd,
|
|
2707
|
+
ccSessionId: savedSessionId,
|
|
2708
|
+
status: "starting",
|
|
2709
|
+
currentTask: null,
|
|
2710
|
+
currentTaskStartedAt: 0,
|
|
2711
|
+
accumulatedThinking: "",
|
|
2712
|
+
accumulatedText: "",
|
|
2713
|
+
contentBlocks: [],
|
|
2714
|
+
currentBlockType: null,
|
|
2715
|
+
currentToolName: null,
|
|
2716
|
+
segmentBuffer: "",
|
|
2717
|
+
segmentCount: 0,
|
|
2718
|
+
accumulatedToolInput: ""
|
|
2719
|
+
};
|
|
2720
|
+
const runtime = Object.assign(proc, {
|
|
2721
|
+
query: agentQuery,
|
|
2722
|
+
inputController,
|
|
2723
|
+
injectedTasks: [],
|
|
2724
|
+
mergedTasks: [],
|
|
2725
|
+
createdAt: Date.now()
|
|
2726
|
+
});
|
|
2727
|
+
procRef = proc;
|
|
2728
|
+
this.agents.set(key, proc);
|
|
2729
|
+
this.consumeOutput(runtime);
|
|
2730
|
+
return proc;
|
|
2731
|
+
}
|
|
2732
|
+
async sendMessage(task) {
|
|
2733
|
+
const key = runtimeKey(task.agentId, task.scope);
|
|
2734
|
+
const proc = this.agents.get(key);
|
|
2735
|
+
if (!proc || proc.status === "dead") {
|
|
2736
|
+
throw new Error(`Agent ${task.agentId} process not available`);
|
|
2737
|
+
}
|
|
2738
|
+
const runtime = this.asRuntime(proc);
|
|
2739
|
+
if (proc.status === "ready") {
|
|
2740
|
+
this.dispatchToSDK(runtime, task);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
if (proc.status === "starting") {
|
|
2744
|
+
logger6.info("Message dispatched to starting Agent (kickstart)", {
|
|
2745
|
+
agentId: task.agentId,
|
|
2746
|
+
replyMessageId: task.replyMessageId,
|
|
2747
|
+
traceId: task.traceId
|
|
2748
|
+
});
|
|
2749
|
+
this.dispatchToSDK(runtime, task);
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
const onYielded = () => {
|
|
2753
|
+
const idx = runtime.injectedTasks.indexOf(task);
|
|
2754
|
+
if (idx >= 0) {
|
|
2755
|
+
runtime.injectedTasks.splice(idx, 1);
|
|
2756
|
+
runtime.mergedTasks.push(task);
|
|
2757
|
+
logger6.info("Injected task consumed by SDK (queued as merged until next result)", {
|
|
2758
|
+
agentId: runtime.agentId,
|
|
2759
|
+
replyMessageId: task.replyMessageId,
|
|
2760
|
+
traceId: task.traceId,
|
|
2761
|
+
mergedQueueSize: runtime.mergedTasks.length,
|
|
2762
|
+
currentTaskReplyMessageId: runtime.currentTask?.replyMessageId
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
runtime.inputController.push(task.content, runtime.ccSessionId ?? "", onYielded);
|
|
2767
|
+
runtime.injectedTasks.push(task);
|
|
2768
|
+
logger6.info("Message injected while Agent working", {
|
|
2769
|
+
agentId: task.agentId,
|
|
2770
|
+
replyMessageId: task.replyMessageId,
|
|
2771
|
+
currentStatus: proc.status,
|
|
2772
|
+
injectedDepth: runtime.injectedTasks.length,
|
|
2773
|
+
traceId: task.traceId
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
dispatchToSDK(runtime, task) {
|
|
2777
|
+
runtime.status = "working";
|
|
2778
|
+
runtime.currentTask = task;
|
|
2779
|
+
runtime.currentTaskStartedAt = Date.now();
|
|
2780
|
+
runtime.inputController.push(task.content, runtime.ccSessionId ?? "");
|
|
2781
|
+
logger6.info("Message pushed to Agent", {
|
|
2782
|
+
agentId: runtime.agentId,
|
|
2783
|
+
replyMessageId: task.replyMessageId,
|
|
2784
|
+
traceId: task.traceId
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
resetProcAccumulators(proc) {
|
|
2788
|
+
proc.accumulatedText = "";
|
|
2789
|
+
proc.accumulatedThinking = "";
|
|
2790
|
+
proc.contentBlocks = [];
|
|
2791
|
+
proc.currentBlockType = null;
|
|
2792
|
+
proc.currentToolName = null;
|
|
2793
|
+
proc.segmentBuffer = "";
|
|
2794
|
+
proc.segmentCount = 0;
|
|
2795
|
+
}
|
|
2796
|
+
onTaskCompleted(proc, carrierMessageId) {
|
|
2797
|
+
const runtime = this.asRuntime(proc);
|
|
2798
|
+
const completedTask = proc.currentTask;
|
|
2799
|
+
if (completedTask && runtime.mergedTasks.length > 0) {
|
|
2800
|
+
const mergedBatch = [...runtime.mergedTasks];
|
|
2801
|
+
logger6.info("Flushing merged tasks after result", {
|
|
2802
|
+
agentId: proc.agentId,
|
|
2803
|
+
carrierReplyMessageId: completedTask.replyMessageId,
|
|
2804
|
+
mergedCount: mergedBatch.length,
|
|
2805
|
+
mergedReplyMessageIds: mergedBatch.map((t) => t.replyMessageId),
|
|
2806
|
+
traceId: completedTask.traceId
|
|
2807
|
+
});
|
|
2808
|
+
for (const merged of mergedBatch) {
|
|
2809
|
+
logger6.info("Emitting agent:merged for task consumed in same turn", {
|
|
2810
|
+
agentId: proc.agentId,
|
|
2811
|
+
ackId: merged.replyMessageId,
|
|
2812
|
+
mergedIntoAckId: completedTask.replyMessageId,
|
|
2813
|
+
mergedIntoMessageId: carrierMessageId,
|
|
2814
|
+
traceId: merged.traceId
|
|
2815
|
+
});
|
|
2816
|
+
this.emit({
|
|
2817
|
+
type: "agent:merged",
|
|
2818
|
+
payload: {
|
|
2819
|
+
agentId: proc.agentId,
|
|
2820
|
+
conversationId: merged.conversationId,
|
|
2821
|
+
ackId: merged.replyMessageId,
|
|
2822
|
+
mergedIntoAckId: completedTask.replyMessageId,
|
|
2823
|
+
mergedIntoMessageId: carrierMessageId,
|
|
2824
|
+
groupId: merged.groupId,
|
|
2825
|
+
traceId: merged.traceId
|
|
2826
|
+
}
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
runtime.mergedTasks = [];
|
|
2830
|
+
} else if (runtime.mergedTasks.length > 0) {
|
|
2831
|
+
logger6.warn("mergedTasks non-empty but no currentTask; dropping", {
|
|
2832
|
+
agentId: proc.agentId,
|
|
2833
|
+
mergedCount: runtime.mergedTasks.length
|
|
2834
|
+
});
|
|
2835
|
+
runtime.mergedTasks = [];
|
|
2836
|
+
}
|
|
2837
|
+
if (runtime.injectedTasks.length > 0) {
|
|
2838
|
+
const next = runtime.injectedTasks.shift();
|
|
2839
|
+
this.resetProcAccumulators(proc);
|
|
2840
|
+
proc.currentTask = next;
|
|
2841
|
+
proc.status = "working";
|
|
2842
|
+
proc.currentTaskStartedAt = Date.now();
|
|
2843
|
+
logger6.info("Promoted next injected task after result", {
|
|
2844
|
+
agentId: proc.agentId,
|
|
2845
|
+
replyMessageId: next.replyMessageId,
|
|
2846
|
+
remainingInjected: runtime.injectedTasks.length,
|
|
2847
|
+
traceId: next.traceId
|
|
2848
|
+
});
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
proc.currentTask = null;
|
|
2852
|
+
proc.status = "ready";
|
|
2853
|
+
this.lastUsedAt.set(runtimeKey(proc.agentId, proc.scope), Date.now());
|
|
2854
|
+
}
|
|
2855
|
+
getQueryStatus(bridgeId) {
|
|
2856
|
+
const queries = [...this.agents.entries()].map(([key, proc]) => ({
|
|
2857
|
+
agentId: proc.agentId,
|
|
2858
|
+
status: proc.status,
|
|
2859
|
+
ccSessionId: proc.ccSessionId,
|
|
2860
|
+
lastActiveAt: new Date(this.lastUsedAt.get(key) ?? 0).toISOString()
|
|
2861
|
+
}));
|
|
2862
|
+
const activeCount = [...this.agents.values()].filter((p) => p.status !== "dead").length;
|
|
2863
|
+
return {
|
|
2864
|
+
type: "bridge:query_status",
|
|
2865
|
+
payload: {
|
|
2866
|
+
bridgeId,
|
|
2867
|
+
queries,
|
|
2868
|
+
activeCount,
|
|
2869
|
+
maxActive: this.queryConfig.maxActive,
|
|
2870
|
+
bridgeMemoryMB: Math.round(process.memoryUsage().rss / 1024 / 1024)
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
deliverNeuralSend(agentConfig, payload) {
|
|
2875
|
+
const targetScope = payload.toScopeKey === "single" ? { kind: "single" } : { kind: "group", groupId: payload.groupId ?? payload.toScopeKey.replace("group:", "") };
|
|
2876
|
+
this.sessionStore.delete(agentConfig.id, targetScope);
|
|
2877
|
+
const enveloped = buildInnerVoiceEnvelope(payload);
|
|
2878
|
+
const task = {
|
|
2879
|
+
content: enveloped,
|
|
2880
|
+
replyMessageId: `msg_nsend_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2881
|
+
conversationId: payload.conversationId ?? "",
|
|
2882
|
+
traceId: `tr_nsend_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
2883
|
+
groupId: payload.groupId
|
|
2884
|
+
};
|
|
2885
|
+
const key = runtimeKey(agentConfig.id, targetScope);
|
|
2886
|
+
const existingProc = this.agents.get(key);
|
|
2887
|
+
logger6.info("Neural send dispatching", {
|
|
2888
|
+
agentId: agentConfig.id,
|
|
2889
|
+
fromScope: payload.fromScopeKey,
|
|
2890
|
+
toScope: payload.toScopeKey,
|
|
2891
|
+
hasExisting: !!existingProc,
|
|
2892
|
+
existingStatus: existingProc?.status,
|
|
2893
|
+
messageLen: payload.message.length,
|
|
2894
|
+
conversationId: payload.conversationId ?? "(none)",
|
|
2895
|
+
groupId: payload.groupId ?? "(none)",
|
|
2896
|
+
replyMessageId: task.replyMessageId
|
|
2897
|
+
});
|
|
2898
|
+
if (existingProc && existingProc.status !== "dead") {
|
|
2899
|
+
if (existingProc.status === "ready" || existingProc.status === "starting") {
|
|
2900
|
+
logger6.info("Neural send dispatched to idle runtime", {
|
|
2901
|
+
agentId: agentConfig.id,
|
|
2902
|
+
toScope: payload.toScopeKey,
|
|
2903
|
+
replyMessageId: task.replyMessageId,
|
|
2904
|
+
runtimeStatus: existingProc.status
|
|
2905
|
+
});
|
|
2906
|
+
this.dispatchToSDK(this.asRuntime(existingProc), task);
|
|
2907
|
+
} else {
|
|
2908
|
+
const runtime = this.asRuntime(existingProc);
|
|
2909
|
+
runtime.inputController.push(task.content, runtime.ccSessionId ?? "");
|
|
2910
|
+
runtime.injectedTasks.push(task);
|
|
2911
|
+
logger6.info("Neural send injected mid-turn", {
|
|
2912
|
+
agentId: agentConfig.id,
|
|
2913
|
+
toScope: payload.toScopeKey,
|
|
2914
|
+
replyMessageId: task.replyMessageId,
|
|
2915
|
+
injectedDepth: runtime.injectedTasks.length
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
let cwd;
|
|
2921
|
+
if (targetScope.kind === "group") {
|
|
2922
|
+
if (!payload.targetCwd) {
|
|
2923
|
+
logger6.error("Neural send abort: group target missing targetCwd", {
|
|
2924
|
+
agentId: agentConfig.id,
|
|
2925
|
+
toScope: payload.toScopeKey
|
|
2926
|
+
});
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
cwd = payload.targetCwd;
|
|
2930
|
+
} else {
|
|
2931
|
+
cwd = agentConfig.workingDirectory || path6.join(this.workspacesDir, agentConfig.id);
|
|
2932
|
+
}
|
|
2933
|
+
void this.acquire(agentConfig, targetScope, cwd).then(() => {
|
|
2934
|
+
logger6.info("Neural send new runtime acquired", {
|
|
2935
|
+
agentId: agentConfig.id,
|
|
2936
|
+
toScope: payload.toScopeKey,
|
|
2937
|
+
cwd,
|
|
2938
|
+
replyMessageId: task.replyMessageId
|
|
2939
|
+
});
|
|
2940
|
+
return this.sendMessage({ ...task, agentId: agentConfig.id, scope: targetScope });
|
|
2941
|
+
}).catch((err) => {
|
|
2942
|
+
logger6.error("Neural send acquire failed", {
|
|
2943
|
+
agentId: agentConfig.id,
|
|
2944
|
+
toScope: payload.toScopeKey,
|
|
2945
|
+
cwd,
|
|
2946
|
+
error: err
|
|
2947
|
+
});
|
|
2948
|
+
});
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Hard-remove all scoped runtimes for an Agent (agent:terminate on delete).
|
|
2952
|
+
*/
|
|
2953
|
+
async terminate(agentId) {
|
|
2954
|
+
const keys = [...this.agents.keys()].filter(
|
|
2955
|
+
(k) => k === agentId || k.startsWith(`${agentId}::`)
|
|
2956
|
+
);
|
|
2957
|
+
if (keys.length === 0) {
|
|
2958
|
+
logger6.warn("terminate: no process for agent", { agentId });
|
|
2959
|
+
this.sessionStore.deleteAllForAgent(agentId);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
for (const key of keys) {
|
|
2963
|
+
const proc = this.agents.get(key);
|
|
2964
|
+
if (proc) {
|
|
2965
|
+
await this.closeRuntime(proc, "terminate");
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
this.sessionStore.deleteAllForAgent(agentId);
|
|
2969
|
+
logger6.info("terminate: all scoped queries removed", { agentId, count: keys.length });
|
|
2970
|
+
}
|
|
2971
|
+
/** Stop one scoped SDK runtime (workdir change). */
|
|
2972
|
+
async terminateScope(agentId, scope) {
|
|
2973
|
+
const key = runtimeKey(agentId, scope);
|
|
2974
|
+
const proc = this.agents.get(key);
|
|
2975
|
+
if (!proc) {
|
|
2976
|
+
logger6.info("terminateScope: no active runtime", { agentId, scope: scopeKey(scope) });
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
await this.closeRuntime(proc, "terminateScope");
|
|
2980
|
+
this.sessionStore.delete(agentId, scope);
|
|
2981
|
+
logger6.info("terminateScope: scoped query removed", { agentId, scope: scopeKey(scope) });
|
|
2982
|
+
}
|
|
2983
|
+
async closeRuntime(proc, reason) {
|
|
2984
|
+
const key = runtimeKey(proc.agentId, proc.scope);
|
|
2985
|
+
const runtime = this.asRuntime(proc);
|
|
2986
|
+
const { agentId } = proc;
|
|
2987
|
+
const emitInterrupted = (task) => {
|
|
2988
|
+
this.emit({
|
|
2989
|
+
type: "agent:error",
|
|
2990
|
+
payload: {
|
|
2991
|
+
agentId,
|
|
2992
|
+
conversationId: task.conversationId,
|
|
2993
|
+
replyMessageId: task.replyMessageId,
|
|
2994
|
+
traceId: task.traceId,
|
|
2995
|
+
error: "\u5BF9\u8BDD\u88AB\u4E2D\u65AD"
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
};
|
|
2999
|
+
if (runtime.status === "working" && runtime.currentTask) {
|
|
3000
|
+
emitInterrupted(runtime.currentTask);
|
|
3001
|
+
}
|
|
3002
|
+
const queued = [...runtime.injectedTasks];
|
|
3003
|
+
runtime.injectedTasks = [];
|
|
3004
|
+
for (const t of queued) {
|
|
3005
|
+
emitInterrupted(t);
|
|
3006
|
+
}
|
|
3007
|
+
const mergedAtClose = [...runtime.mergedTasks];
|
|
3008
|
+
runtime.mergedTasks = [];
|
|
3009
|
+
for (const t of mergedAtClose) {
|
|
3010
|
+
if (runtime.currentTask) {
|
|
3011
|
+
logger6.info("Emitting agent:merged on runtime close", {
|
|
3012
|
+
agentId: runtime.agentId,
|
|
3013
|
+
replyMessageId: t.replyMessageId,
|
|
3014
|
+
mergedInto: runtime.currentTask.replyMessageId,
|
|
3015
|
+
reason,
|
|
3016
|
+
traceId: t.traceId
|
|
3017
|
+
});
|
|
3018
|
+
this.emit({
|
|
3019
|
+
type: "agent:merged",
|
|
3020
|
+
payload: {
|
|
3021
|
+
agentId: runtime.agentId,
|
|
3022
|
+
conversationId: t.conversationId,
|
|
3023
|
+
ackId: t.replyMessageId,
|
|
3024
|
+
mergedIntoAckId: runtime.currentTask.replyMessageId,
|
|
3025
|
+
groupId: t.groupId,
|
|
3026
|
+
traceId: t.traceId
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
} else {
|
|
3030
|
+
emitInterrupted(t);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
runtime.currentTask = null;
|
|
3034
|
+
try {
|
|
3035
|
+
runtime.inputController.close();
|
|
3036
|
+
await this.awaitQueryReturn(runtime.query, 5e3, agentId);
|
|
3037
|
+
} catch (e) {
|
|
3038
|
+
logger6.error(`${reason}: close query failed`, { agentId, scope: scopeKey(proc.scope), error: e });
|
|
3039
|
+
}
|
|
3040
|
+
proc.status = "dead";
|
|
3041
|
+
this.agents.delete(key);
|
|
3042
|
+
this.lastUsedAt.delete(key);
|
|
3043
|
+
logger6.info(`${reason}: keeping workspace dir intact (per project policy)`, {
|
|
3044
|
+
agentId,
|
|
3045
|
+
scope: scopeKey(proc.scope),
|
|
3046
|
+
cwd: proc.cwd
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
async recoverFromRestart(agents) {
|
|
3050
|
+
logger6.info("Recovering Agent sessions after restart", { count: agents.length });
|
|
3051
|
+
const agentsWithSession = agents.filter((a) => {
|
|
3052
|
+
const sessionId = this.sessionStore.get(a.id, { kind: "single" });
|
|
3053
|
+
return !!sessionId;
|
|
3054
|
+
});
|
|
3055
|
+
if (agentsWithSession.length === 0) {
|
|
3056
|
+
logger6.info("No Agent sessions to recover");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
let warmed = 0;
|
|
3060
|
+
const cap = this.queryConfig.maxActive;
|
|
3061
|
+
for (const agent of agentsWithSession) {
|
|
3062
|
+
if (warmed >= cap) {
|
|
3063
|
+
logger6.info("Recovery warm cap reached", { cap, skipped: agentsWithSession.length - warmed });
|
|
3064
|
+
break;
|
|
3065
|
+
}
|
|
3066
|
+
try {
|
|
3067
|
+
const cwd = agent.workingDirectory || path6.join(this.workspacesDir, agent.id);
|
|
3068
|
+
await this.acquire(agent, { kind: "single" }, cwd);
|
|
3069
|
+
warmed++;
|
|
3070
|
+
logger6.info("Agent process pre-created for recovery", { agentId: agent.id });
|
|
3071
|
+
} catch (err) {
|
|
3072
|
+
if (err instanceof BridgeBusyError) {
|
|
3073
|
+
logger6.warn("Recovery stopped: bridge busy", { agentId: agent.id });
|
|
3074
|
+
break;
|
|
3075
|
+
}
|
|
3076
|
+
logger6.warn("Failed to pre-create Agent for recovery, clearing session", {
|
|
3077
|
+
agentId: agent.id,
|
|
3078
|
+
error: err
|
|
3079
|
+
});
|
|
3080
|
+
this.sessionStore.delete(agent.id, { kind: "single" });
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
async consumeOutput(runtime) {
|
|
3085
|
+
try {
|
|
3086
|
+
for await (const message of runtime.query) {
|
|
3087
|
+
const t = typeof message.type === "string" ? message.type : "unknown";
|
|
3088
|
+
wsMetrics.incSdkOut(t);
|
|
3089
|
+
mapSDKMessage(
|
|
3090
|
+
runtime,
|
|
3091
|
+
message,
|
|
3092
|
+
this.emit,
|
|
3093
|
+
this.sessionStore,
|
|
3094
|
+
(doneMessageId) => this.onTaskCompleted(runtime, doneMessageId)
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
const errMsg = err.message ?? String(err);
|
|
3099
|
+
const isResumeFail = /session|conversation.*not found/i.test(errMsg);
|
|
3100
|
+
logger6.error("Agent query stream ended with error", {
|
|
3101
|
+
agentId: runtime.agentId,
|
|
3102
|
+
scope: scopeKey(runtime.scope),
|
|
3103
|
+
isResumeFail,
|
|
3104
|
+
staleSessionId: runtime.ccSessionId,
|
|
3105
|
+
error: err
|
|
3106
|
+
});
|
|
3107
|
+
this.sessionStore.delete(runtime.agentId, runtime.scope);
|
|
3108
|
+
logger6.info("Cleared stale session after query crash", {
|
|
3109
|
+
agentId: runtime.agentId,
|
|
3110
|
+
scope: scopeKey(runtime.scope)
|
|
3111
|
+
});
|
|
3112
|
+
runtime.status = "dead";
|
|
3113
|
+
const key = runtimeKey(runtime.agentId, runtime.scope);
|
|
3114
|
+
this.agents.delete(key);
|
|
3115
|
+
this.lastUsedAt.delete(key);
|
|
3116
|
+
const errorText = isResumeFail ? `\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\uFF08${errMsg}\uFF09` : `Agent query crashed: ${errMsg}`;
|
|
3117
|
+
if (runtime.currentTask) {
|
|
3118
|
+
this.emit({
|
|
3119
|
+
type: "agent:error",
|
|
3120
|
+
payload: {
|
|
3121
|
+
agentId: runtime.agentId,
|
|
3122
|
+
conversationId: runtime.currentTask.conversationId,
|
|
3123
|
+
ackId: runtime.currentTask.replyMessageId,
|
|
3124
|
+
traceId: runtime.currentTask.traceId,
|
|
3125
|
+
error: errorText
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
3128
|
+
runtime.currentTask = null;
|
|
3129
|
+
}
|
|
3130
|
+
for (const task of runtime.injectedTasks) {
|
|
3131
|
+
this.emit({
|
|
3132
|
+
type: "agent:error",
|
|
3133
|
+
payload: {
|
|
3134
|
+
agentId: runtime.agentId,
|
|
3135
|
+
conversationId: task.conversationId,
|
|
3136
|
+
ackId: task.replyMessageId,
|
|
3137
|
+
traceId: task.traceId,
|
|
3138
|
+
error: errorText
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
runtime.injectedTasks = [];
|
|
3143
|
+
for (const task of runtime.mergedTasks) {
|
|
3144
|
+
this.emit({
|
|
3145
|
+
type: "agent:error",
|
|
3146
|
+
payload: {
|
|
3147
|
+
agentId: runtime.agentId,
|
|
3148
|
+
conversationId: task.conversationId,
|
|
3149
|
+
ackId: task.replyMessageId,
|
|
3150
|
+
traceId: task.traceId,
|
|
3151
|
+
error: errorText
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
runtime.mergedTasks = [];
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
getStatus(agentId, scope = { kind: "single" }) {
|
|
3159
|
+
return this.agents.get(runtimeKey(agentId, scope))?.status ?? null;
|
|
3160
|
+
}
|
|
3161
|
+
getManagedAgentIds() {
|
|
3162
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3163
|
+
for (const key of this.agents.keys()) {
|
|
3164
|
+
const agentId = key.includes("::") ? key.split("::")[0] : key;
|
|
3165
|
+
ids.add(agentId);
|
|
3166
|
+
}
|
|
3167
|
+
return [...ids];
|
|
3168
|
+
}
|
|
3169
|
+
async shutdownAll() {
|
|
3170
|
+
logger6.info("Shutting down all Agent processes", { count: this.agents.size });
|
|
3171
|
+
this.askQuestionRegistry.cancelAll("agent_aborted");
|
|
3172
|
+
if (this.evictionTimer) {
|
|
3173
|
+
clearInterval(this.evictionTimer);
|
|
3174
|
+
this.evictionTimer = null;
|
|
3175
|
+
}
|
|
3176
|
+
for (const [key, proc] of this.agents) {
|
|
3177
|
+
try {
|
|
3178
|
+
const runtime = this.asRuntime(proc);
|
|
3179
|
+
runtime.inputController?.close();
|
|
3180
|
+
runtime.query?.return(void 0);
|
|
3181
|
+
proc.status = "dead";
|
|
3182
|
+
logger6.info("Agent process shut down", { agentId: proc.agentId, scope: scopeKey(proc.scope), key });
|
|
3183
|
+
} catch (err) {
|
|
3184
|
+
logger6.error("Error shutting down Agent", { agentId: proc.agentId, error: err });
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
this.agents.clear();
|
|
3188
|
+
this.lastUsedAt.clear();
|
|
3189
|
+
}
|
|
3190
|
+
async cancelReply(payload) {
|
|
3191
|
+
const { agentId, replyMessageId, traceId, conversationId } = payload;
|
|
3192
|
+
let proc;
|
|
3193
|
+
for (const p of this.agents.values()) {
|
|
3194
|
+
if (p.agentId !== agentId || p.status === "dead") continue;
|
|
3195
|
+
if (p.currentTask?.replyMessageId === replyMessageId) {
|
|
3196
|
+
proc = p;
|
|
3197
|
+
break;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
if (!proc) {
|
|
3201
|
+
logger6.warn("cancelReply: no active process for reply", { agentId, replyMessageId });
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const runtime = this.asRuntime(proc);
|
|
3205
|
+
const key = runtimeKey(agentId, proc.scope);
|
|
3206
|
+
if (!runtime.currentTask || runtime.currentTask.replyMessageId !== replyMessageId) {
|
|
3207
|
+
logger6.warn("cancelReply: replyMessageId mismatch", {
|
|
3208
|
+
agentId,
|
|
3209
|
+
replyMessageId,
|
|
3210
|
+
expected: runtime.currentTask?.replyMessageId
|
|
3211
|
+
});
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const emitCancelled = (task) => {
|
|
3215
|
+
this.emit({
|
|
3216
|
+
type: "agent:error",
|
|
3217
|
+
payload: {
|
|
3218
|
+
agentId,
|
|
3219
|
+
conversationId: task.conversationId,
|
|
3220
|
+
ackId: task.replyMessageId,
|
|
3221
|
+
traceId: task.traceId,
|
|
3222
|
+
error: "\u5DF2\u53D6\u6D88"
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
};
|
|
3226
|
+
emitCancelled(runtime.currentTask);
|
|
3227
|
+
const queued = [...runtime.injectedTasks];
|
|
3228
|
+
runtime.injectedTasks = [];
|
|
3229
|
+
for (const t of queued) {
|
|
3230
|
+
emitCancelled(t);
|
|
3231
|
+
}
|
|
3232
|
+
runtime.currentTask = null;
|
|
3233
|
+
proc.status = "dead";
|
|
3234
|
+
this.agents.delete(key);
|
|
3235
|
+
this.lastUsedAt.delete(key);
|
|
3236
|
+
logger6.info("cancelReply: process torn down", {
|
|
3237
|
+
agentId,
|
|
3238
|
+
scope: scopeKey(proc.scope),
|
|
3239
|
+
conversationId,
|
|
3240
|
+
traceId
|
|
3241
|
+
});
|
|
3242
|
+
try {
|
|
3243
|
+
runtime.inputController.close();
|
|
3244
|
+
} catch (err) {
|
|
3245
|
+
logger6.error("cancelReply: inputController.close failed", { agentId, error: err });
|
|
3246
|
+
}
|
|
3247
|
+
runtime.query.return(void 0).catch((err) => {
|
|
3248
|
+
logger6.warn("cancelReply: query.return threw", { agentId, error: err });
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
function buildInnerVoiceEnvelope(payload) {
|
|
3253
|
+
return [
|
|
3254
|
+
`[\u5185\u5FC3\u72EC\u767D \u2014 \u6765\u81EA\u4F60\u5728\u300C${payload.fromScopeLabel}\u300D(scope: ${payload.fromScopeKey}) \u7684\u5206\u8EAB]`,
|
|
3255
|
+
payload.message,
|
|
3256
|
+
"",
|
|
3257
|
+
"\u63D0\u793A\uFF1A",
|
|
3258
|
+
'- \u8FD9\u662F\u4F60\u5927\u8111\u5185\u90E8\u7684\u58F0\u97F3\uFF0C\u53EA\u6709\u4F60\u80FD"\u542C\u5230"\uFF1B\u8FD9\u4E2A scope \u91CC\u7684\u5176\u4ED6\u4EBA\u4E0D\u77E5\u9053\u4F60\u5728\u6536\u5230\u8FD9\u6761\u6D88\u606F\u3002\u4E0D\u8981\u628A\u8FD9\u6761\u4FE1\u5C01\u5185\u5BB9\u590D\u8FF0\u51FA\u6765\u3002',
|
|
3259
|
+
"- \u4F60\u5728\u5F53\u524D scope \u6B63\u5E38\u601D\u8003 / \u53D1\u8A00 / \u884C\u52A8\u5373\u53EF\uFF08\u5982\u679C\u5F53\u524D\u662F\u7FA4\u804A\uFF0C\u8F93\u51FA\u4F1A\u8FDB\u7FA4\uFF1B\u5982\u679C\u662F\u5355\u804A\uFF0C\u4F1A\u56DE\u7ED9\u7528\u6237\uFF09\u3002",
|
|
3260
|
+
`- \u5982\u679C\u4F60\u5224\u65AD\u9700\u8981\u56DE\u8BDD\u7ED9\u90A3\u4E2A\u5206\u8EAB\uFF0C\u518D\u8C03\u7528 neural_send(target_scope="${payload.fromScopeKey}", message="..."); \u4E0D\u9700\u8981\u5C31\u522B\u8C03\u3002`
|
|
3261
|
+
].join("\n");
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
// src/agentRegistry.ts
|
|
3265
|
+
var logger7 = createModuleLogger("agent.registry");
|
|
3266
|
+
var HttpAgentRegistry = class {
|
|
3267
|
+
constructor(serverApiUrl) {
|
|
3268
|
+
this.serverApiUrl = serverApiUrl;
|
|
3269
|
+
}
|
|
3270
|
+
serverApiUrl;
|
|
3271
|
+
agents = /* @__PURE__ */ new Map();
|
|
3272
|
+
apiUrl(suffix) {
|
|
3273
|
+
const base = this.serverApiUrl.replace(/\/$/, "");
|
|
3274
|
+
const path11 = suffix.startsWith("/") ? suffix : `/${suffix}`;
|
|
3275
|
+
return `${base}${path11}`;
|
|
3276
|
+
}
|
|
3277
|
+
async refresh() {
|
|
3278
|
+
const attempt = async () => {
|
|
3279
|
+
try {
|
|
3280
|
+
return await fetch(this.apiUrl("/api/agents"));
|
|
3281
|
+
} catch {
|
|
3282
|
+
return null;
|
|
3283
|
+
}
|
|
3284
|
+
};
|
|
3285
|
+
let res = await attempt();
|
|
3286
|
+
let recoveredAfterRetry = false;
|
|
3287
|
+
if (!res) {
|
|
3288
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3289
|
+
res = await attempt();
|
|
3290
|
+
recoveredAfterRetry = res != null;
|
|
3291
|
+
}
|
|
3292
|
+
if (!res) {
|
|
3293
|
+
logger7.warn("Agent registry refresh unreachable after retry, keeping cache");
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
if (!res.ok) {
|
|
3297
|
+
logger7.warn("Agent registry refresh failed", { status: res.status });
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
try {
|
|
3301
|
+
const body = await res.json();
|
|
3302
|
+
if (!Array.isArray(body)) {
|
|
3303
|
+
logger7.warn("Agent registry refresh: expected array");
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
this.agents.clear();
|
|
3307
|
+
for (const item of body) {
|
|
3308
|
+
const a = item;
|
|
3309
|
+
if (a && typeof a.id === "string") {
|
|
3310
|
+
this.agents.set(a.id, a);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
logger7.info("Agent registry refreshed", { count: this.agents.size, recoveredAfterRetry });
|
|
3314
|
+
} catch (e) {
|
|
3315
|
+
logger7.warn("Agent registry refresh parse failed", { error: e });
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
getById(id) {
|
|
3319
|
+
return this.agents.get(id) ?? null;
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Fetch a single agent directly from the server and upsert into cache.
|
|
3323
|
+
* Used as a fallback when task:dispatch arrives before agent:created WS push.
|
|
3324
|
+
*/
|
|
3325
|
+
async fetchById(id) {
|
|
3326
|
+
try {
|
|
3327
|
+
const res = await fetch(this.apiUrl(`/api/agents/${encodeURIComponent(id)}`));
|
|
3328
|
+
if (!res.ok) {
|
|
3329
|
+
logger7.warn("fetchById failed", { agentId: id, status: res.status });
|
|
3330
|
+
return null;
|
|
3331
|
+
}
|
|
3332
|
+
const agent = await res.json();
|
|
3333
|
+
if (agent && typeof agent.id === "string") {
|
|
3334
|
+
this.agents.set(agent.id, agent);
|
|
3335
|
+
logger7.info("Agent registry fetchById upserted", { agentId: id });
|
|
3336
|
+
}
|
|
3337
|
+
return agent;
|
|
3338
|
+
} catch (e) {
|
|
3339
|
+
logger7.warn("fetchById unreachable", { agentId: id, error: e });
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
getAll() {
|
|
3344
|
+
return [...this.agents.values()];
|
|
3345
|
+
}
|
|
3346
|
+
upsert(agent) {
|
|
3347
|
+
this.agents.set(agent.id, agent);
|
|
3348
|
+
logger7.debug("Agent registry upsert", { agentId: agent.id });
|
|
3349
|
+
}
|
|
3350
|
+
remove(agentId) {
|
|
3351
|
+
this.agents.delete(agentId);
|
|
3352
|
+
logger7.debug("Agent registry remove", { agentId });
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
|
|
3356
|
+
// src/groupRegistry.ts
|
|
3357
|
+
var logger8 = createModuleLogger("neural.groupRegistry");
|
|
3358
|
+
var GroupRegistry = class {
|
|
3359
|
+
groups = /* @__PURE__ */ new Map();
|
|
3360
|
+
serverApiUrl;
|
|
3361
|
+
constructor(serverApiUrl) {
|
|
3362
|
+
this.serverApiUrl = serverApiUrl.replace(/\/$/, "");
|
|
3363
|
+
}
|
|
3364
|
+
async refresh() {
|
|
3365
|
+
const attempt = async () => {
|
|
3366
|
+
try {
|
|
3367
|
+
return await fetch(`${this.serverApiUrl}/api/groups`);
|
|
3368
|
+
} catch {
|
|
3369
|
+
return null;
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
let res = await attempt();
|
|
3373
|
+
let recoveredAfterRetry = false;
|
|
3374
|
+
if (!res) {
|
|
3375
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3376
|
+
res = await attempt();
|
|
3377
|
+
recoveredAfterRetry = res != null;
|
|
3378
|
+
}
|
|
3379
|
+
if (!res) {
|
|
3380
|
+
logger8.warn("GroupRegistry refresh unreachable after retry");
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (!res.ok) {
|
|
3384
|
+
logger8.warn("GroupRegistry refresh failed", { status: res.status });
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
try {
|
|
3388
|
+
const body = await res.json();
|
|
3389
|
+
if (!Array.isArray(body)) {
|
|
3390
|
+
logger8.warn("GroupRegistry refresh: expected array");
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
this.groups.clear();
|
|
3394
|
+
for (const item of body) {
|
|
3395
|
+
const g = item;
|
|
3396
|
+
if (g && typeof g.id === "string") {
|
|
3397
|
+
this.groups.set(g.id, {
|
|
3398
|
+
groupId: g.id,
|
|
3399
|
+
name: g.name ?? "",
|
|
3400
|
+
conversationId: null,
|
|
3401
|
+
members: (g.members ?? []).filter((m) => typeof m.agentId === "string").map((m) => m.agentId)
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
logger8.info("GroupRegistry refreshed", { count: this.groups.size, recoveredAfterRetry });
|
|
3406
|
+
} catch (e) {
|
|
3407
|
+
logger8.warn("GroupRegistry refresh parse failed", { error: e });
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
getById(groupId) {
|
|
3411
|
+
return this.groups.get(groupId) ?? null;
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Fuzzy match by name (case-insensitive substring).
|
|
3415
|
+
* Returns the first match.
|
|
3416
|
+
*/
|
|
3417
|
+
resolveByName(name) {
|
|
3418
|
+
const lower = name.toLowerCase().trim();
|
|
3419
|
+
if (!lower) return null;
|
|
3420
|
+
for (const g of this.groups.values()) {
|
|
3421
|
+
if (g.name.toLowerCase().includes(lower)) return g;
|
|
3422
|
+
}
|
|
3423
|
+
return null;
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Resolve a target_scope string to a fully-populated scope.
|
|
3427
|
+
*
|
|
3428
|
+
* Accepts:
|
|
3429
|
+
* - "group:grp_xxx" — direct ID lookup (byId=1)
|
|
3430
|
+
* - "group:方圆宝产品讨论组" — fuzzy name match (server-side)
|
|
3431
|
+
*
|
|
3432
|
+
* Always returns the latest conversationId + workingDirectory (server lazy-ensures wd).
|
|
3433
|
+
* Returns null for non-group inputs or unresolvable groups.
|
|
3434
|
+
*/
|
|
3435
|
+
async resolveScope(rawScope) {
|
|
3436
|
+
if (!rawScope.startsWith("group:")) {
|
|
3437
|
+
return null;
|
|
3438
|
+
}
|
|
3439
|
+
const suffix = rawScope.slice(6);
|
|
3440
|
+
if (!suffix) return null;
|
|
3441
|
+
const byId = suffix.startsWith("grp_") ? "&byId=1" : "";
|
|
3442
|
+
const url = `${this.serverApiUrl}/api/groups/resolve?name=${encodeURIComponent(suffix)}${byId}`;
|
|
3443
|
+
try {
|
|
3444
|
+
const res = await fetch(url);
|
|
3445
|
+
if (res.status === 404) {
|
|
3446
|
+
logger8.info("GroupRegistry resolveScope: group not found", { rawScope, suffix });
|
|
3447
|
+
return null;
|
|
3448
|
+
}
|
|
3449
|
+
if (!res.ok) {
|
|
3450
|
+
logger8.warn("GroupRegistry resolveScope: HTTP error", { rawScope, status: res.status });
|
|
3451
|
+
return null;
|
|
3452
|
+
}
|
|
3453
|
+
const data = await res.json();
|
|
3454
|
+
if (!data.groupId || !data.conversationId || !data.workingDirectory) {
|
|
3455
|
+
logger8.warn("GroupRegistry resolveScope: incomplete response", {
|
|
3456
|
+
rawScope,
|
|
3457
|
+
hasGroupId: !!data.groupId,
|
|
3458
|
+
hasConversationId: !!data.conversationId,
|
|
3459
|
+
hasWorkingDirectory: !!data.workingDirectory
|
|
3460
|
+
});
|
|
3461
|
+
return null;
|
|
3462
|
+
}
|
|
3463
|
+
logger8.info("GroupRegistry resolved scope", {
|
|
3464
|
+
rawScope,
|
|
3465
|
+
resolvedGroupId: data.groupId,
|
|
3466
|
+
resolvedName: data.name ?? "(none)",
|
|
3467
|
+
conversationId: data.conversationId,
|
|
3468
|
+
workingDirectory: data.workingDirectory
|
|
3469
|
+
});
|
|
3470
|
+
return {
|
|
3471
|
+
groupId: data.groupId,
|
|
3472
|
+
scopeKey: `group:${data.groupId}`,
|
|
3473
|
+
conversationId: data.conversationId,
|
|
3474
|
+
groupName: data.name ?? "",
|
|
3475
|
+
workingDirectory: data.workingDirectory
|
|
3476
|
+
};
|
|
3477
|
+
} catch (e) {
|
|
3478
|
+
logger8.error("GroupRegistry resolveScope error", { rawScope, error: e });
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
getAll() {
|
|
3483
|
+
return [...this.groups.values()];
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Resolve the agent's single-chat conversationId via Server REST API.
|
|
3487
|
+
* If no single conversation exists yet, creates one (POST /api/conversations)
|
|
3488
|
+
* so neural_send → single always lands in a valid conv.
|
|
3489
|
+
* Returns null only if all attempts fail (network down, agent missing, etc.).
|
|
3490
|
+
*/
|
|
3491
|
+
async resolveSingleConversationId(agentId) {
|
|
3492
|
+
try {
|
|
3493
|
+
const res = await fetch(`${this.serverApiUrl}/api/conversations?agentId=${encodeURIComponent(agentId)}`);
|
|
3494
|
+
if (res.ok) {
|
|
3495
|
+
const body = await res.json();
|
|
3496
|
+
if (Array.isArray(body)) {
|
|
3497
|
+
const single = body.find((c) => c.type === "single" && typeof c.id === "string");
|
|
3498
|
+
if (single?.id) {
|
|
3499
|
+
logger8.info("GroupRegistry resolved single conv", { agentId, conversationId: single.id });
|
|
3500
|
+
return single.id;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
} else {
|
|
3504
|
+
logger8.warn("GroupRegistry resolveSingle: list failed", { agentId, status: res.status });
|
|
3505
|
+
}
|
|
3506
|
+
const created = await fetch(`${this.serverApiUrl}/api/conversations`, {
|
|
3507
|
+
method: "POST",
|
|
3508
|
+
headers: { "Content-Type": "application/json" },
|
|
3509
|
+
body: JSON.stringify({ agentId })
|
|
3510
|
+
});
|
|
3511
|
+
if (!created.ok) {
|
|
3512
|
+
logger8.warn("GroupRegistry resolveSingle: create failed", { agentId, status: created.status });
|
|
3513
|
+
return null;
|
|
3514
|
+
}
|
|
3515
|
+
const conv = await created.json();
|
|
3516
|
+
if (typeof conv.id !== "string") {
|
|
3517
|
+
logger8.warn("GroupRegistry resolveSingle: created conv missing id", { agentId });
|
|
3518
|
+
return null;
|
|
3519
|
+
}
|
|
3520
|
+
logger8.info("GroupRegistry created single conv", { agentId, conversationId: conv.id });
|
|
3521
|
+
return conv.id;
|
|
3522
|
+
} catch (e) {
|
|
3523
|
+
logger8.error("GroupRegistry resolveSingle error", { agentId, error: e });
|
|
3524
|
+
return null;
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
|
|
3529
|
+
// src/connector.ts
|
|
3530
|
+
import WebSocket from "ws";
|
|
3531
|
+
var logger9 = createModuleLogger("ws.connector");
|
|
3532
|
+
var ServerConnector = class {
|
|
3533
|
+
ws = null;
|
|
3534
|
+
reconnectAttempts = 0;
|
|
3535
|
+
delays = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
3536
|
+
reconnectTimer = null;
|
|
3537
|
+
closing = false;
|
|
3538
|
+
config;
|
|
3539
|
+
agentIds;
|
|
3540
|
+
onTaskDispatch;
|
|
3541
|
+
onGroupTaskDispatch;
|
|
3542
|
+
onStopGeneration;
|
|
3543
|
+
onConnected;
|
|
3544
|
+
onServerPush;
|
|
3545
|
+
constructor(params) {
|
|
3546
|
+
this.config = params.config;
|
|
3547
|
+
this.agentIds = params.agentIds;
|
|
3548
|
+
this.onTaskDispatch = params.onTaskDispatch;
|
|
3549
|
+
this.onGroupTaskDispatch = params.onGroupTaskDispatch;
|
|
3550
|
+
this.onStopGeneration = params.onStopGeneration;
|
|
3551
|
+
this.onConnected = params.onConnected;
|
|
3552
|
+
this.onServerPush = params.onServerPush;
|
|
3553
|
+
}
|
|
3554
|
+
connect() {
|
|
3555
|
+
if (this.closing) return;
|
|
3556
|
+
const url = new URL(this.config.serverUrl);
|
|
3557
|
+
if (this.config.bridgeToken) {
|
|
3558
|
+
url.searchParams.set("token", this.config.bridgeToken);
|
|
3559
|
+
}
|
|
3560
|
+
const wsUrl = url.toString();
|
|
3561
|
+
logger9.info("Connecting to server", { url: this.config.serverUrl });
|
|
3562
|
+
const ws = new WebSocket(wsUrl);
|
|
3563
|
+
ws.on("open", () => {
|
|
3564
|
+
this.ws = ws;
|
|
3565
|
+
this.reconnectAttempts = 0;
|
|
3566
|
+
logger9.info("Connected to server", { url: this.config.serverUrl });
|
|
3567
|
+
void this.handleOpen();
|
|
3568
|
+
});
|
|
3569
|
+
ws.on("message", (data) => {
|
|
3570
|
+
this.handleMessage(data);
|
|
3571
|
+
});
|
|
3572
|
+
ws.on("close", (code, reason) => {
|
|
3573
|
+
logger9.warn("Disconnected from server", {
|
|
3574
|
+
code,
|
|
3575
|
+
reason: reason.toString()
|
|
3576
|
+
});
|
|
3577
|
+
this.ws = null;
|
|
3578
|
+
if (!this.closing) {
|
|
3579
|
+
this.scheduleReconnect();
|
|
3580
|
+
}
|
|
3581
|
+
});
|
|
3582
|
+
ws.on("error", (err) => {
|
|
3583
|
+
logger9.error("WebSocket error", { error: err });
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
async handleOpen() {
|
|
3587
|
+
try {
|
|
3588
|
+
await this.onConnected();
|
|
3589
|
+
logger9.info("Recovery complete, sending bridge:register");
|
|
3590
|
+
} catch (err) {
|
|
3591
|
+
logger9.error("Recovery failed, registering with degraded state", { error: err });
|
|
3592
|
+
}
|
|
3593
|
+
this.register();
|
|
3594
|
+
}
|
|
3595
|
+
register() {
|
|
3596
|
+
const ids = this.agentIds();
|
|
3597
|
+
const qc = this.config.queryConfig ?? DEFAULT_QUERY_CONFIG;
|
|
3598
|
+
this.send({
|
|
3599
|
+
type: "bridge:register",
|
|
3600
|
+
payload: {
|
|
3601
|
+
bridgeId: this.config.bridgeId,
|
|
3602
|
+
agents: ids,
|
|
3603
|
+
queryConfig: {
|
|
3604
|
+
maxActive: qc.maxActive,
|
|
3605
|
+
idleTimeoutMs: qc.idleTimeoutMs
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
});
|
|
3609
|
+
logger9.info("Sent bridge:register", {
|
|
3610
|
+
bridgeId: this.config.bridgeId,
|
|
3611
|
+
agents: ids
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
handleMessage(data) {
|
|
3615
|
+
let msg;
|
|
3616
|
+
try {
|
|
3617
|
+
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
3618
|
+
msg = parseWSMessage(raw);
|
|
3619
|
+
} catch (e) {
|
|
3620
|
+
logger9.error("Invalid WS message from server", { error: e });
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
wsMetrics.incRecv(msg.type);
|
|
3624
|
+
switch (msg.type) {
|
|
3625
|
+
case "heartbeat": {
|
|
3626
|
+
this.send(msg);
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
case "task:dispatch": {
|
|
3630
|
+
void this.onTaskDispatch(msg.payload).catch((err) => {
|
|
3631
|
+
logger9.error("Failed to handle task:dispatch", {
|
|
3632
|
+
error: err,
|
|
3633
|
+
traceId: msg.payload.traceId
|
|
3634
|
+
});
|
|
3635
|
+
});
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
case "task:group_dispatch": {
|
|
3639
|
+
if (this.onGroupTaskDispatch) {
|
|
3640
|
+
void this.onGroupTaskDispatch(msg.payload).catch((err) => {
|
|
3641
|
+
logger9.error("Failed to handle task:group_dispatch", {
|
|
3642
|
+
error: err,
|
|
3643
|
+
traceId: msg.payload.traceId
|
|
3644
|
+
});
|
|
3645
|
+
});
|
|
3646
|
+
} else {
|
|
3647
|
+
logger9.warn("Received task:group_dispatch but no handler registered");
|
|
3648
|
+
}
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
case "user:stop_generation": {
|
|
3652
|
+
void this.onStopGeneration(msg.payload).catch((err) => {
|
|
3653
|
+
logger9.error("Failed to handle user:stop_generation", {
|
|
3654
|
+
error: err,
|
|
3655
|
+
traceId: msg.payload.traceId
|
|
3656
|
+
});
|
|
3657
|
+
});
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
case "bridge:list_models_request":
|
|
3661
|
+
case "agent:terminate":
|
|
3662
|
+
case "agent:terminate_scope":
|
|
3663
|
+
case "agent:created":
|
|
3664
|
+
case "agent:updated":
|
|
3665
|
+
case "agent:deleted":
|
|
3666
|
+
case "group:member_changed":
|
|
3667
|
+
case "user:answer_question": {
|
|
3668
|
+
if (this.onServerPush) {
|
|
3669
|
+
void Promise.resolve(this.onServerPush(msg)).catch((err) => {
|
|
3670
|
+
logger9.error("onServerPush handler failed", { error: err, type: msg.type });
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
default: {
|
|
3676
|
+
logger9.warn("Unhandled server message type", {
|
|
3677
|
+
type: msg.type
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
send(msg) {
|
|
3683
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
3684
|
+
logger9.warn("Cannot send: WebSocket not open", {
|
|
3685
|
+
type: msg.type
|
|
3686
|
+
});
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
try {
|
|
3690
|
+
this.ws.send(JSON.stringify(msg));
|
|
3691
|
+
wsMetrics.incSend(msg.type);
|
|
3692
|
+
} catch (e) {
|
|
3693
|
+
logger9.error("Failed to send WS message", { error: e, type: msg.type });
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
scheduleReconnect() {
|
|
3697
|
+
if (this.closing) return;
|
|
3698
|
+
const delay = this.delays[Math.min(this.reconnectAttempts, this.delays.length - 1)];
|
|
3699
|
+
this.reconnectAttempts++;
|
|
3700
|
+
logger9.info("Scheduling reconnect", {
|
|
3701
|
+
attempt: this.reconnectAttempts,
|
|
3702
|
+
delayMs: delay
|
|
3703
|
+
});
|
|
3704
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
3705
|
+
}
|
|
3706
|
+
close() {
|
|
3707
|
+
this.closing = true;
|
|
3708
|
+
if (this.reconnectTimer) {
|
|
3709
|
+
clearTimeout(this.reconnectTimer);
|
|
3710
|
+
this.reconnectTimer = null;
|
|
3711
|
+
}
|
|
3712
|
+
if (this.ws) {
|
|
3713
|
+
try {
|
|
3714
|
+
this.ws.close(1e3, "Bridge shutting down");
|
|
3715
|
+
} catch (e) {
|
|
3716
|
+
logger9.error("Error closing WebSocket", { error: e });
|
|
3717
|
+
}
|
|
3718
|
+
this.ws = null;
|
|
3719
|
+
}
|
|
3720
|
+
logger9.info("Connector closed");
|
|
3721
|
+
}
|
|
3722
|
+
get isConnected() {
|
|
3723
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
3724
|
+
}
|
|
3725
|
+
};
|
|
3726
|
+
|
|
3727
|
+
// src/modelQuerier.ts
|
|
3728
|
+
import fs3 from "fs/promises";
|
|
3729
|
+
import os5 from "os";
|
|
3730
|
+
import path7 from "path";
|
|
3731
|
+
var logger10 = createModuleLogger("bridge.modelQuerier");
|
|
3732
|
+
async function listModels(queryFn, opts = {}) {
|
|
3733
|
+
const t0 = Date.now();
|
|
3734
|
+
const cwd = opts.cwd ?? path7.join(os5.homedir(), ".ahchat", "workspaces", "_list_models");
|
|
3735
|
+
await fs3.mkdir(cwd, { recursive: true });
|
|
3736
|
+
const fn = queryFn ?? (await import("@anthropic-ai/claude-agent-sdk")).query;
|
|
3737
|
+
const ic = new InputController();
|
|
3738
|
+
ic.push("Reply with exactly: PING", "");
|
|
3739
|
+
const q = fn({
|
|
3740
|
+
prompt: ic,
|
|
3741
|
+
options: {
|
|
3742
|
+
cwd,
|
|
3743
|
+
systemPrompt: { type: "preset", preset: "claude_code", append: "" },
|
|
3744
|
+
permissionMode: "bypassPermissions",
|
|
3745
|
+
allowDangerouslySkipPermissions: true,
|
|
3746
|
+
allowedTools: []
|
|
3747
|
+
}
|
|
3748
|
+
});
|
|
3749
|
+
const initTimeoutMs = opts.initTimeoutMs ?? 3e4;
|
|
3750
|
+
let initialized = false;
|
|
3751
|
+
const initPromise = (async () => {
|
|
3752
|
+
for await (const msg of q) {
|
|
3753
|
+
const t = String(msg.type ?? "");
|
|
3754
|
+
const sub = String(msg.subtype ?? "");
|
|
3755
|
+
if (t === "system" && sub === "init") {
|
|
3756
|
+
initialized = true;
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
})();
|
|
3761
|
+
try {
|
|
3762
|
+
await Promise.race([
|
|
3763
|
+
initPromise,
|
|
3764
|
+
new Promise((_, rej) => {
|
|
3765
|
+
setTimeout(() => rej(new Error(`init timeout after ${initTimeoutMs}ms`)), initTimeoutMs);
|
|
3766
|
+
})
|
|
3767
|
+
]);
|
|
3768
|
+
if (!initialized) {
|
|
3769
|
+
throw new Error("generator ended before init");
|
|
3770
|
+
}
|
|
3771
|
+
const init = await q.initializationResult();
|
|
3772
|
+
const models = init.models.map((m) => ({
|
|
3773
|
+
value: m.value,
|
|
3774
|
+
displayName: m.displayName,
|
|
3775
|
+
description: m.description
|
|
3776
|
+
}));
|
|
3777
|
+
logger10.info("listModels done", { count: models.length, ms: Date.now() - t0 });
|
|
3778
|
+
return models;
|
|
3779
|
+
} finally {
|
|
3780
|
+
try {
|
|
3781
|
+
ic.close();
|
|
3782
|
+
} catch {
|
|
3783
|
+
}
|
|
3784
|
+
try {
|
|
3785
|
+
await q.return?.(void 0);
|
|
3786
|
+
} catch {
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
// src/lockfile.ts
|
|
3792
|
+
import fs4 from "fs";
|
|
3793
|
+
import path8 from "path";
|
|
3794
|
+
var logger11 = createModuleLogger("bridge.lockfile");
|
|
3795
|
+
var lockPath = null;
|
|
3796
|
+
function isProcessAlive(pid) {
|
|
3797
|
+
try {
|
|
3798
|
+
process.kill(pid, 0);
|
|
3799
|
+
return true;
|
|
3800
|
+
} catch (e) {
|
|
3801
|
+
const err = e;
|
|
3802
|
+
if (err.code === "ESRCH") return false;
|
|
3803
|
+
throw e;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
function acquireLock(dataDir) {
|
|
3807
|
+
const file = path8.join(dataDir, "bridge.lock");
|
|
3808
|
+
lockPath = file;
|
|
3809
|
+
if (fs4.existsSync(file)) {
|
|
3810
|
+
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
3811
|
+
const pid = Number.parseInt(raw, 10);
|
|
3812
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
3813
|
+
if (isProcessAlive(pid)) {
|
|
3814
|
+
throw new Error(`Bridge already running (PID: ${pid})`);
|
|
3815
|
+
}
|
|
3816
|
+
logger11.warn("Removing stale bridge.lock (process not found)", { pid, path: file });
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
fs4.mkdirSync(path8.dirname(file), { recursive: true });
|
|
3820
|
+
fs4.writeFileSync(file, String(process.pid), "utf-8");
|
|
3821
|
+
logger11.info("Acquired bridge lock", { path: file, pid: process.pid });
|
|
3822
|
+
const release = () => {
|
|
3823
|
+
try {
|
|
3824
|
+
if (lockPath && fs4.existsSync(lockPath)) {
|
|
3825
|
+
const current = fs4.readFileSync(lockPath, "utf-8").trim();
|
|
3826
|
+
if (current === String(process.pid)) {
|
|
3827
|
+
fs4.unlinkSync(lockPath);
|
|
3828
|
+
logger11.info("Released bridge lock", { path: lockPath });
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
} catch (e) {
|
|
3832
|
+
logger11.error("Failed to release bridge lock", { error: e, path: lockPath });
|
|
3833
|
+
} finally {
|
|
3834
|
+
lockPath = null;
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
process.on("exit", release);
|
|
3838
|
+
process.once("SIGINT", () => {
|
|
3839
|
+
release();
|
|
3840
|
+
process.exit(0);
|
|
3841
|
+
});
|
|
3842
|
+
process.once("SIGTERM", () => {
|
|
3843
|
+
release();
|
|
3844
|
+
process.exit(0);
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// src/groupPromptBuilder.ts
|
|
3849
|
+
function decideRole(p) {
|
|
3850
|
+
if (p.isMentioned) return "mentioned";
|
|
3851
|
+
if (p.mentions.length > 0) return "overhearer";
|
|
3852
|
+
return "open_floor";
|
|
3853
|
+
}
|
|
3854
|
+
function senderKindOf(p) {
|
|
3855
|
+
return p.sender.kind;
|
|
3856
|
+
}
|
|
3857
|
+
var HEADER_BY_SENDER_ROLE = {
|
|
3858
|
+
user: {
|
|
3859
|
+
mentioned: () => [
|
|
3860
|
+
"You were @mentioned in this message.",
|
|
3861
|
+
"You SHOULD reply, but you retain the right to stay silent.",
|
|
3862
|
+
`If you genuinely have nothing to add, reply with exactly the token \`${NO_REPLY_TOKEN}\` (and only that token).`
|
|
3863
|
+
],
|
|
3864
|
+
overhearer: () => [
|
|
3865
|
+
"You are an OVERHEARER \u2014 you received this message but someone else was @mentioned.",
|
|
3866
|
+
`Default behavior: reply with exactly the token \`${NO_REPLY_TOKEN}\` and only that token.`,
|
|
3867
|
+
"ONLY chime in if ONE of the following is true:",
|
|
3868
|
+
" (a) your role/expertise is uniquely required for this question;",
|
|
3869
|
+
" (b) your personality (see your system prompt) compels you to interject;",
|
|
3870
|
+
" (c) there is a factual error you must correct.",
|
|
3871
|
+
`Otherwise reply \`${NO_REPLY_TOKEN}\`.`
|
|
3872
|
+
],
|
|
3873
|
+
open_floor: () => [
|
|
3874
|
+
"This message is addressed to the whole group (no one was @mentioned).",
|
|
3875
|
+
"Treat it like a real IM group: reply if your role, expertise, or personality has something to add.",
|
|
3876
|
+
`If you have nothing meaningful to contribute, reply with exactly the token \`${NO_REPLY_TOKEN}\` (and only that token) to stay silent.`
|
|
3877
|
+
]
|
|
3878
|
+
},
|
|
3879
|
+
agent: {
|
|
3880
|
+
mentioned: ({ senderName }) => [
|
|
3881
|
+
`A fellow agent (${senderName}) @mentioned you in the group.`,
|
|
3882
|
+
"You SHOULD reply, but you may stay silent.",
|
|
3883
|
+
`Per your platform rules, if you have nothing to add, reply \`${NO_REPLY_TOKEN}\`.`
|
|
3884
|
+
],
|
|
3885
|
+
overhearer: ({ senderName }) => [
|
|
3886
|
+
`A fellow agent (${senderName}) spoke and @mentioned someone else.`,
|
|
3887
|
+
`Per your platform rules, default \`${NO_REPLY_TOKEN}\` unless your expertise is uniquely required or there is a factual error.`
|
|
3888
|
+
],
|
|
3889
|
+
open_floor: ({ senderName }) => [
|
|
3890
|
+
`A fellow agent (${senderName}) addressed the group.`,
|
|
3891
|
+
`Per your platform rules, default \`${NO_REPLY_TOKEN}\` unless your expertise is uniquely needed or there is a factual error.`
|
|
3892
|
+
]
|
|
3893
|
+
}
|
|
3894
|
+
};
|
|
3895
|
+
function buildGroupPrompt(payload) {
|
|
3896
|
+
const lines = [];
|
|
3897
|
+
const kind = senderKindOf(payload);
|
|
3898
|
+
const role = decideRole(payload);
|
|
3899
|
+
const senderName = payload.sender.kind === "agent" ? payload.sender.agentName : "user";
|
|
3900
|
+
lines.push(`[Group: ${payload.groupName}] \xB7 ${payload.groupMemberCount}-person group`);
|
|
3901
|
+
lines.push(`Members: ${payload.groupMemberNames.join(", ")}`);
|
|
3902
|
+
lines.push(`You are: ${payload.agentName}`);
|
|
3903
|
+
for (const line of HEADER_BY_SENDER_ROLE[kind][role]({ senderName })) {
|
|
3904
|
+
lines.push(line);
|
|
3905
|
+
}
|
|
3906
|
+
lines.push("");
|
|
3907
|
+
lines.push("--- chat history ---");
|
|
3908
|
+
if (payload.context.length === 0) {
|
|
3909
|
+
lines.push("(no history)");
|
|
3910
|
+
} else {
|
|
3911
|
+
for (const msg of payload.context) {
|
|
3912
|
+
const s = msg.role === "user" ? "user" : msg.senderAgentName ?? `agent:${msg.senderAgentId ?? "unknown"}`;
|
|
3913
|
+
lines.push(`[${s}]: ${msg.content}`);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
lines.push("--- end history ---");
|
|
3917
|
+
lines.push("");
|
|
3918
|
+
if (payload.replyToMessage) {
|
|
3919
|
+
const rts = payload.replyToMessage.role === "user" ? "user" : payload.replyToMessage.senderAgentName ?? `agent:${payload.replyToMessage.senderAgentId ?? "unknown"}`;
|
|
3920
|
+
lines.push(`> Reply to [${rts}]: ${payload.replyToMessage.content}`);
|
|
3921
|
+
lines.push("");
|
|
3922
|
+
}
|
|
3923
|
+
const speakerLabel = payload.sender.kind === "user" ? "user" : payload.sender.agentName;
|
|
3924
|
+
lines.push("------- group task -------");
|
|
3925
|
+
lines.push(`[${speakerLabel}]: ${payload.content}`);
|
|
3926
|
+
lines.push("------- end task -------");
|
|
3927
|
+
lines.push("");
|
|
3928
|
+
lines.push(
|
|
3929
|
+
"If you choose to speak, reply from your professional perspective. Your text will appear in the group chat verbatim."
|
|
3930
|
+
);
|
|
3931
|
+
return lines.join("\n");
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// src/messageHandler.ts
|
|
3935
|
+
var logger12 = createModuleLogger("msg.handler");
|
|
3936
|
+
function emitTaskAck(emit, ackId, agentId, traceId) {
|
|
3937
|
+
logger12.info("Emitting task:ack", { ackId, agentId, traceId });
|
|
3938
|
+
emit({
|
|
3939
|
+
type: "task:ack",
|
|
3940
|
+
payload: {
|
|
3941
|
+
ackId,
|
|
3942
|
+
agentId,
|
|
3943
|
+
traceId,
|
|
3944
|
+
receivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
function createTaskDispatchHandler(agentManager, agentRegistry, emit) {
|
|
3949
|
+
return async (payload) => {
|
|
3950
|
+
logger12.info("Handling task:dispatch", {
|
|
3951
|
+
agentId: payload.agentId,
|
|
3952
|
+
messageId: payload.messageId,
|
|
3953
|
+
ackId: payload.ackId,
|
|
3954
|
+
traceId: payload.traceId
|
|
3955
|
+
});
|
|
3956
|
+
emitTaskAck(emit, payload.ackId, payload.agentId, payload.traceId);
|
|
3957
|
+
let agentConfig = agentRegistry.getById(payload.agentId);
|
|
3958
|
+
if (!agentConfig) {
|
|
3959
|
+
logger12.warn("Agent not in registry, attempting live fetch", {
|
|
3960
|
+
agentId: payload.agentId,
|
|
3961
|
+
traceId: payload.traceId
|
|
3962
|
+
});
|
|
3963
|
+
agentConfig = await agentRegistry.fetchById(payload.agentId);
|
|
3964
|
+
}
|
|
3965
|
+
if (!agentConfig) {
|
|
3966
|
+
logger12.error("Agent not found for task:dispatch (after live fetch)", {
|
|
3967
|
+
agentId: payload.agentId,
|
|
3968
|
+
traceId: payload.traceId
|
|
3969
|
+
});
|
|
3970
|
+
emit({
|
|
3971
|
+
type: "agent:error",
|
|
3972
|
+
payload: {
|
|
3973
|
+
agentId: payload.agentId,
|
|
3974
|
+
conversationId: payload.conversationId,
|
|
3975
|
+
ackId: payload.ackId,
|
|
3976
|
+
traceId: payload.traceId,
|
|
3977
|
+
error: "Agent not found"
|
|
3978
|
+
}
|
|
3979
|
+
});
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
try {
|
|
3983
|
+
await agentManager.acquire(agentConfig, { kind: "single" }, payload.cwd);
|
|
3984
|
+
await agentManager.sendMessage({
|
|
3985
|
+
agentId: payload.agentId,
|
|
3986
|
+
scope: { kind: "single" },
|
|
3987
|
+
conversationId: payload.conversationId,
|
|
3988
|
+
content: payload.content,
|
|
3989
|
+
replyMessageId: payload.ackId,
|
|
3990
|
+
traceId: payload.traceId
|
|
3991
|
+
});
|
|
3992
|
+
} catch (err) {
|
|
3993
|
+
logger12.error("Failed to dispatch message to Agent", {
|
|
3994
|
+
error: err,
|
|
3995
|
+
agentId: payload.agentId,
|
|
3996
|
+
traceId: payload.traceId
|
|
3997
|
+
});
|
|
3998
|
+
emit({
|
|
3999
|
+
type: "agent:error",
|
|
4000
|
+
payload: {
|
|
4001
|
+
agentId: payload.agentId,
|
|
4002
|
+
conversationId: payload.conversationId,
|
|
4003
|
+
ackId: payload.ackId,
|
|
4004
|
+
traceId: payload.traceId,
|
|
4005
|
+
error: `Bridge dispatch error: ${err.message}`
|
|
4006
|
+
}
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
function createGroupTaskDispatchHandler(agentManager, agentRegistry, emit) {
|
|
4012
|
+
return async (payload) => {
|
|
4013
|
+
logger12.info("Handling task:group_dispatch", {
|
|
4014
|
+
agentId: payload.agentId,
|
|
4015
|
+
groupId: payload.groupId,
|
|
4016
|
+
ackId: payload.ackId,
|
|
4017
|
+
isMentioned: payload.isMentioned,
|
|
4018
|
+
senderKind: payload.sender.kind,
|
|
4019
|
+
chainDepth: payload.chainDepth,
|
|
4020
|
+
traceId: payload.traceId
|
|
4021
|
+
});
|
|
4022
|
+
emitTaskAck(emit, payload.ackId, payload.agentId, payload.traceId);
|
|
4023
|
+
let agentConfig = agentRegistry.getById(payload.agentId);
|
|
4024
|
+
if (!agentConfig) {
|
|
4025
|
+
logger12.warn("Agent not in registry for group dispatch, attempting live fetch", {
|
|
4026
|
+
agentId: payload.agentId,
|
|
4027
|
+
traceId: payload.traceId
|
|
4028
|
+
});
|
|
4029
|
+
agentConfig = await agentRegistry.fetchById(payload.agentId);
|
|
4030
|
+
}
|
|
4031
|
+
if (!agentConfig) {
|
|
4032
|
+
logger12.error("Agent not found for task:group_dispatch (after live fetch)", {
|
|
4033
|
+
agentId: payload.agentId,
|
|
4034
|
+
traceId: payload.traceId
|
|
4035
|
+
});
|
|
4036
|
+
emit({
|
|
4037
|
+
type: "agent:error",
|
|
4038
|
+
payload: {
|
|
4039
|
+
agentId: payload.agentId,
|
|
4040
|
+
conversationId: payload.conversationId,
|
|
4041
|
+
ackId: payload.ackId,
|
|
4042
|
+
traceId: payload.traceId,
|
|
4043
|
+
error: "Agent not found"
|
|
4044
|
+
}
|
|
4045
|
+
});
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
const groupPrompt = buildGroupPrompt(payload);
|
|
4049
|
+
try {
|
|
4050
|
+
await agentManager.acquire(
|
|
4051
|
+
agentConfig,
|
|
4052
|
+
{ kind: "group", groupId: payload.groupId },
|
|
4053
|
+
payload.cwd
|
|
4054
|
+
);
|
|
4055
|
+
await agentManager.sendMessage({
|
|
4056
|
+
agentId: payload.agentId,
|
|
4057
|
+
scope: { kind: "group", groupId: payload.groupId },
|
|
4058
|
+
conversationId: payload.conversationId,
|
|
4059
|
+
content: groupPrompt,
|
|
4060
|
+
replyMessageId: payload.ackId,
|
|
4061
|
+
traceId: payload.traceId,
|
|
4062
|
+
groupId: payload.groupId
|
|
4063
|
+
});
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
logger12.error("Failed to dispatch group message to Agent", {
|
|
4066
|
+
error: err,
|
|
4067
|
+
agentId: payload.agentId,
|
|
4068
|
+
groupId: payload.groupId,
|
|
4069
|
+
traceId: payload.traceId
|
|
4070
|
+
});
|
|
4071
|
+
emit({
|
|
4072
|
+
type: "agent:error",
|
|
4073
|
+
payload: {
|
|
4074
|
+
agentId: payload.agentId,
|
|
4075
|
+
conversationId: payload.conversationId,
|
|
4076
|
+
ackId: payload.ackId,
|
|
4077
|
+
traceId: payload.traceId,
|
|
4078
|
+
error: `Bridge group dispatch error: ${err.message}`
|
|
4079
|
+
}
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
// src/sessionStore.ts
|
|
4086
|
+
import fs5 from "fs";
|
|
4087
|
+
import path9 from "path";
|
|
4088
|
+
var logger13 = createModuleLogger("session.store");
|
|
4089
|
+
var SessionStore = class {
|
|
4090
|
+
filePath;
|
|
4091
|
+
cache;
|
|
4092
|
+
constructor(dataDir) {
|
|
4093
|
+
this.filePath = path9.join(dataDir, "sessions.json");
|
|
4094
|
+
this.cache = this.loadFromDisk();
|
|
4095
|
+
}
|
|
4096
|
+
cacheKey(agentId, scope) {
|
|
4097
|
+
return runtimeKey(agentId, scope);
|
|
4098
|
+
}
|
|
4099
|
+
get(agentId, scope) {
|
|
4100
|
+
return this.cache[this.cacheKey(agentId, scope)] ?? null;
|
|
4101
|
+
}
|
|
4102
|
+
set(agentId, scope, sessionId) {
|
|
4103
|
+
this.cache[this.cacheKey(agentId, scope)] = sessionId;
|
|
4104
|
+
this.saveToDisk();
|
|
4105
|
+
}
|
|
4106
|
+
delete(agentId, scope) {
|
|
4107
|
+
delete this.cache[this.cacheKey(agentId, scope)];
|
|
4108
|
+
this.saveToDisk();
|
|
4109
|
+
}
|
|
4110
|
+
deleteAllForAgent(agentId) {
|
|
4111
|
+
const prefix = `${agentId}::`;
|
|
4112
|
+
let changed = false;
|
|
4113
|
+
for (const key of Object.keys(this.cache)) {
|
|
4114
|
+
if (key === agentId || key.startsWith(prefix)) {
|
|
4115
|
+
delete this.cache[key];
|
|
4116
|
+
changed = true;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
if (changed) {
|
|
4120
|
+
this.saveToDisk();
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
getAll() {
|
|
4124
|
+
return new Map(Object.entries(this.cache));
|
|
4125
|
+
}
|
|
4126
|
+
loadFromDisk() {
|
|
4127
|
+
try {
|
|
4128
|
+
if (!fs5.existsSync(this.filePath)) return {};
|
|
4129
|
+
const raw = fs5.readFileSync(this.filePath, "utf-8");
|
|
4130
|
+
const parsed = JSON.parse(raw);
|
|
4131
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
4132
|
+
const map = parsed;
|
|
4133
|
+
const migrated = {};
|
|
4134
|
+
for (const [key, sessionId] of Object.entries(map)) {
|
|
4135
|
+
if (key.includes("::")) {
|
|
4136
|
+
migrated[key] = sessionId;
|
|
4137
|
+
} else {
|
|
4138
|
+
migrated[`${key}::single`] = sessionId;
|
|
4139
|
+
logger13.info("Migrated legacy session key to scoped key", {
|
|
4140
|
+
legacyKey: key,
|
|
4141
|
+
newKey: `${key}::single`
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
return migrated;
|
|
4146
|
+
} catch (e) {
|
|
4147
|
+
logger13.warn("Failed to load sessions file, starting fresh", { error: e, path: this.filePath });
|
|
4148
|
+
return {};
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
saveToDisk() {
|
|
4152
|
+
try {
|
|
4153
|
+
const dir = path9.dirname(this.filePath);
|
|
4154
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
4155
|
+
fs5.writeFileSync(this.filePath, JSON.stringify(this.cache, null, 2), "utf-8");
|
|
4156
|
+
} catch (e) {
|
|
4157
|
+
logger13.error("Failed to save sessions file", { error: e, path: this.filePath });
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
|
|
4162
|
+
// src/start.ts
|
|
4163
|
+
var logger14 = createModuleLogger("bridge");
|
|
4164
|
+
async function startBridge(config) {
|
|
4165
|
+
ensureDir(config.dataDir);
|
|
4166
|
+
ensureDir(config.claudeConfigDir);
|
|
4167
|
+
process.env.CLAUDE_CONFIG_DIR = config.claudeConfigDir;
|
|
4168
|
+
acquireLock(config.dataDir);
|
|
4169
|
+
logger14.info("Bridge starting", {
|
|
4170
|
+
bridgeId: config.bridgeId,
|
|
4171
|
+
serverUrl: config.serverUrl,
|
|
4172
|
+
serverApiUrl: config.serverApiUrl,
|
|
4173
|
+
claudeConfigDir: config.claudeConfigDir
|
|
4174
|
+
});
|
|
4175
|
+
wsMetrics.start(5e3);
|
|
4176
|
+
const sessionStore = new SessionStore(config.dataDir);
|
|
4177
|
+
const agentRegistry = new HttpAgentRegistry(config.serverApiUrl);
|
|
4178
|
+
const groupRegistry = new GroupRegistry(config.serverApiUrl);
|
|
4179
|
+
await agentRegistry.refresh();
|
|
4180
|
+
await groupRegistry.refresh();
|
|
4181
|
+
let connector = null;
|
|
4182
|
+
const emit = (msg) => {
|
|
4183
|
+
connector?.send(msg);
|
|
4184
|
+
};
|
|
4185
|
+
const askQuestionRegistry = new AskQuestionRegistry();
|
|
4186
|
+
const agentManager = new AgentManager(sessionStore, emit, {
|
|
4187
|
+
queryConfig: config.queryConfig,
|
|
4188
|
+
claudeConfigDir: config.claudeConfigDir,
|
|
4189
|
+
askQuestionRegistry,
|
|
4190
|
+
groupRegistry
|
|
4191
|
+
});
|
|
4192
|
+
const taskDispatchHandler = createTaskDispatchHandler(agentManager, agentRegistry, emit);
|
|
4193
|
+
const groupTaskDispatchHandler = createGroupTaskDispatchHandler(agentManager, agentRegistry, emit);
|
|
4194
|
+
let statusInterval = null;
|
|
4195
|
+
connector = new ServerConnector({
|
|
4196
|
+
config,
|
|
4197
|
+
agentIds: () => agentRegistry.getAll().map((a) => a.id),
|
|
4198
|
+
onTaskDispatch: taskDispatchHandler,
|
|
4199
|
+
onGroupTaskDispatch: groupTaskDispatchHandler,
|
|
4200
|
+
onStopGeneration: async (payload) => {
|
|
4201
|
+
await agentManager.cancelReply(payload);
|
|
4202
|
+
},
|
|
4203
|
+
onConnected: async () => {
|
|
4204
|
+
await agentRegistry.refresh();
|
|
4205
|
+
await groupRegistry.refresh();
|
|
4206
|
+
await agentManager.recoverFromRestart(agentRegistry.getAll());
|
|
4207
|
+
},
|
|
4208
|
+
onServerPush: async (msg) => {
|
|
4209
|
+
switch (msg.type) {
|
|
4210
|
+
case "bridge:list_models_request": {
|
|
4211
|
+
const { requestId } = msg.payload;
|
|
4212
|
+
logger14.info("list_models request received", { requestId });
|
|
4213
|
+
try {
|
|
4214
|
+
const models = await listModels();
|
|
4215
|
+
connector?.send({
|
|
4216
|
+
type: "bridge:list_models_response",
|
|
4217
|
+
payload: { requestId, models }
|
|
4218
|
+
});
|
|
4219
|
+
logger14.info("list_models response sent", { requestId, count: models.length });
|
|
4220
|
+
} catch (e) {
|
|
4221
|
+
const err = e instanceof Error ? e.message : String(e);
|
|
4222
|
+
connector?.send({
|
|
4223
|
+
type: "bridge:list_models_response",
|
|
4224
|
+
payload: { requestId, error: err }
|
|
4225
|
+
});
|
|
4226
|
+
logger14.error("list_models failed", { requestId, error: e });
|
|
4227
|
+
}
|
|
4228
|
+
break;
|
|
4229
|
+
}
|
|
4230
|
+
case "agent:terminate":
|
|
4231
|
+
await agentManager.terminate(msg.payload.agentId);
|
|
4232
|
+
break;
|
|
4233
|
+
case "agent:terminate_scope":
|
|
4234
|
+
logger14.info("agent:terminate_scope received", {
|
|
4235
|
+
agentId: msg.payload.agentId,
|
|
4236
|
+
scope: msg.payload.scope
|
|
4237
|
+
});
|
|
4238
|
+
await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
|
|
4239
|
+
break;
|
|
4240
|
+
case "agent:created":
|
|
4241
|
+
case "agent:updated":
|
|
4242
|
+
agentRegistry.upsert(msg.payload.agent);
|
|
4243
|
+
break;
|
|
4244
|
+
case "agent:deleted":
|
|
4245
|
+
agentRegistry.remove(msg.payload.agentId);
|
|
4246
|
+
break;
|
|
4247
|
+
case "user:answer_question": {
|
|
4248
|
+
const p = msg.payload;
|
|
4249
|
+
const answerText = formatAnswerForSDK(p);
|
|
4250
|
+
const ok = askQuestionRegistry.resolve(p.questionId, answerText);
|
|
4251
|
+
logger14.info("user:answer_question handled", {
|
|
4252
|
+
questionId: p.questionId,
|
|
4253
|
+
agentId: p.agentId,
|
|
4254
|
+
resolved: ok,
|
|
4255
|
+
traceId: p.traceId
|
|
4256
|
+
});
|
|
4257
|
+
break;
|
|
4258
|
+
}
|
|
4259
|
+
default:
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
connector.connect();
|
|
4265
|
+
statusInterval = setInterval(() => {
|
|
4266
|
+
if (!connector?.isConnected) return;
|
|
4267
|
+
void agentRegistry.refresh().then(() => {
|
|
4268
|
+
connector?.send(agentManager.getQueryStatus(config.bridgeId));
|
|
4269
|
+
});
|
|
4270
|
+
}, config.queryConfig.statusReportIntervalMs);
|
|
4271
|
+
const shutdown = async (signal) => {
|
|
4272
|
+
logger14.info("Shutdown signal received", { signal });
|
|
4273
|
+
if (statusInterval) {
|
|
4274
|
+
clearInterval(statusInterval);
|
|
4275
|
+
statusInterval = null;
|
|
4276
|
+
}
|
|
4277
|
+
wsMetrics.stop();
|
|
4278
|
+
connector?.close();
|
|
4279
|
+
await agentManager.shutdownAll();
|
|
4280
|
+
logger14.info("Bridge stopped");
|
|
4281
|
+
process.exit(0);
|
|
4282
|
+
};
|
|
4283
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
4284
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
4285
|
+
}
|
|
4286
|
+
|
|
23
4287
|
// src/protocol.ts
|
|
24
4288
|
import { execSync } from "child_process";
|
|
25
|
-
import
|
|
26
|
-
import
|
|
27
|
-
import
|
|
4289
|
+
import fs6 from "fs";
|
|
4290
|
+
import os6 from "os";
|
|
4291
|
+
import path10 from "path";
|
|
28
4292
|
import { fileURLToPath } from "url";
|
|
29
|
-
var
|
|
4293
|
+
var logger15 = createModuleLogger("bridge.protocol");
|
|
30
4294
|
var __filename = fileURLToPath(import.meta.url);
|
|
31
|
-
var __dirname =
|
|
4295
|
+
var __dirname = path10.dirname(__filename);
|
|
32
4296
|
function getBridgeExePath() {
|
|
33
|
-
const pkgDir =
|
|
34
|
-
return
|
|
4297
|
+
const pkgDir = path10.resolve(__dirname, "..");
|
|
4298
|
+
return path10.join(pkgDir, "dist", "cli.js");
|
|
35
4299
|
}
|
|
36
4300
|
function registerProtocolHandler() {
|
|
37
|
-
const platform =
|
|
4301
|
+
const platform = os6.platform();
|
|
38
4302
|
if (platform === "win32") {
|
|
39
4303
|
registerWindows();
|
|
40
4304
|
} else if (platform === "darwin") {
|
|
@@ -42,15 +4306,15 @@ function registerProtocolHandler() {
|
|
|
42
4306
|
} else {
|
|
43
4307
|
registerLinux();
|
|
44
4308
|
}
|
|
45
|
-
|
|
4309
|
+
logger15.info("Protocol handler registered", { platform });
|
|
46
4310
|
}
|
|
47
4311
|
function registerWindows() {
|
|
48
4312
|
const exe = getBridgeExePath();
|
|
49
4313
|
const nodeExe = process.execPath;
|
|
50
|
-
const ahchatDir =
|
|
51
|
-
const urlFilePath =
|
|
52
|
-
|
|
53
|
-
const psScriptPath =
|
|
4314
|
+
const ahchatDir = path10.join(os6.homedir(), ".ahchat");
|
|
4315
|
+
const urlFilePath = path10.join(ahchatDir, ".bridge-launch-url");
|
|
4316
|
+
fs6.mkdirSync(ahchatDir, { recursive: true });
|
|
4317
|
+
const psScriptPath = path10.join(ahchatDir, "launch-bridge.ps1");
|
|
54
4318
|
const psContent = `param([string]$url)
|
|
55
4319
|
if (-not $url) {
|
|
56
4320
|
if (Test-Path '${urlFilePath}') {
|
|
@@ -63,7 +4327,7 @@ if (-not $url) {
|
|
|
63
4327
|
}
|
|
64
4328
|
& '${nodeExe}' '${exe}' launch --url $url
|
|
65
4329
|
`;
|
|
66
|
-
|
|
4330
|
+
fs6.writeFileSync(psScriptPath, psContent);
|
|
67
4331
|
const handler = `powershell -ExecutionPolicy Bypass -File "${psScriptPath}" -url "%1"`;
|
|
68
4332
|
const regCommands = [
|
|
69
4333
|
`REG ADD "HKCU\\Software\\Classes\\ahchat" /ve /d "URL:ahchat" /f`,
|
|
@@ -75,19 +4339,19 @@ if (-not $url) {
|
|
|
75
4339
|
try {
|
|
76
4340
|
execSync(cmd, { stdio: "pipe" });
|
|
77
4341
|
} catch (e) {
|
|
78
|
-
|
|
4342
|
+
logger15.error("Failed to register Windows protocol handler", { error: e, cmd });
|
|
79
4343
|
throw new Error(`Failed to register protocol handler: ${cmd}`);
|
|
80
4344
|
}
|
|
81
4345
|
}
|
|
82
|
-
|
|
4346
|
+
logger15.info("Windows protocol handler registered", { psScriptPath });
|
|
83
4347
|
}
|
|
84
4348
|
function registerMacOS() {
|
|
85
|
-
const appDir =
|
|
86
|
-
const contentsDir =
|
|
87
|
-
const macosDir =
|
|
88
|
-
const resourcesDir =
|
|
89
|
-
|
|
90
|
-
|
|
4349
|
+
const appDir = path10.join(os6.homedir(), "Applications", "AHChatBridge.app");
|
|
4350
|
+
const contentsDir = path10.join(appDir, "Contents");
|
|
4351
|
+
const macosDir = path10.join(contentsDir, "MacOS");
|
|
4352
|
+
const resourcesDir = path10.join(contentsDir, "Resources");
|
|
4353
|
+
fs6.mkdirSync(macosDir, { recursive: true });
|
|
4354
|
+
fs6.mkdirSync(resourcesDir, { recursive: true });
|
|
91
4355
|
const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
92
4356
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
93
4357
|
<plist version="1.0">
|
|
@@ -120,10 +4384,10 @@ function registerMacOS() {
|
|
|
120
4384
|
const launchScript = `#!/bin/bash
|
|
121
4385
|
URL="$1"
|
|
122
4386
|
exec "${process.execPath}" "${getBridgeExePath()}" launch --url "$URL"`;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
4387
|
+
fs6.writeFileSync(path10.join(contentsDir, "Info.plist"), infoPlist);
|
|
4388
|
+
fs6.writeFileSync(path10.join(macosDir, "launch.sh"), launchScript);
|
|
4389
|
+
fs6.chmodSync(path10.join(macosDir, "launch.sh"), 493);
|
|
4390
|
+
logger15.info("macOS protocol handler registered", { appDir });
|
|
127
4391
|
}
|
|
128
4392
|
function registerLinux() {
|
|
129
4393
|
const desktopFile = `[Desktop Entry]
|
|
@@ -132,66 +4396,54 @@ Exec=${process.execPath} ${getBridgeExePath()} launch --url %u
|
|
|
132
4396
|
Type=Application
|
|
133
4397
|
NoDisplay=true
|
|
134
4398
|
MimeType=x-scheme-handler/ahchat;`;
|
|
135
|
-
const desktopPath =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"share",
|
|
139
|
-
"applications",
|
|
140
|
-
"ahchat-bridge.desktop"
|
|
141
|
-
);
|
|
142
|
-
fs.mkdirSync(path.dirname(desktopPath), { recursive: true });
|
|
143
|
-
fs.writeFileSync(desktopPath, desktopFile);
|
|
4399
|
+
const desktopPath = path10.join(os6.homedir(), ".local", "share", "applications", "ahchat-bridge.desktop");
|
|
4400
|
+
fs6.mkdirSync(path10.dirname(desktopPath), { recursive: true });
|
|
4401
|
+
fs6.writeFileSync(desktopPath, desktopFile);
|
|
144
4402
|
try {
|
|
145
4403
|
execSync("update-desktop-database ~/.local/share/applications/", { stdio: "pipe" });
|
|
146
4404
|
} catch {
|
|
147
4405
|
}
|
|
148
|
-
|
|
4406
|
+
logger15.info("Linux protocol handler registered", { desktopPath });
|
|
149
4407
|
}
|
|
150
4408
|
function unregisterProtocolHandler() {
|
|
151
|
-
const platform =
|
|
4409
|
+
const platform = os6.platform();
|
|
152
4410
|
if (platform === "win32") {
|
|
153
4411
|
try {
|
|
154
4412
|
execSync('REG DELETE "HKCU\\Software\\Classes\\ahchat" /f', { stdio: "pipe" });
|
|
155
|
-
const psScriptPath =
|
|
156
|
-
const urlFilePath =
|
|
4413
|
+
const psScriptPath = path10.join(os6.homedir(), ".ahchat", "launch-bridge.ps1");
|
|
4414
|
+
const urlFilePath = path10.join(os6.homedir(), ".ahchat", ".bridge-launch-url");
|
|
157
4415
|
try {
|
|
158
|
-
|
|
4416
|
+
fs6.unlinkSync(psScriptPath);
|
|
159
4417
|
} catch {
|
|
160
4418
|
}
|
|
161
4419
|
try {
|
|
162
|
-
|
|
4420
|
+
fs6.unlinkSync(urlFilePath);
|
|
163
4421
|
} catch {
|
|
164
4422
|
}
|
|
165
|
-
|
|
4423
|
+
logger15.info("Windows protocol handler unregistered");
|
|
166
4424
|
} catch (e) {
|
|
167
|
-
|
|
4425
|
+
logger15.warn("Failed to unregister Windows protocol handler", { error: e });
|
|
168
4426
|
}
|
|
169
4427
|
} else if (platform === "darwin") {
|
|
170
|
-
const appDir =
|
|
4428
|
+
const appDir = path10.join(os6.homedir(), "Applications", "AHChatBridge.app");
|
|
171
4429
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
4430
|
+
fs6.rmSync(appDir, { recursive: true, force: true });
|
|
4431
|
+
logger15.info("macOS protocol handler unregistered");
|
|
174
4432
|
} catch (e) {
|
|
175
|
-
|
|
4433
|
+
logger15.warn("Failed to unregister macOS protocol handler", { error: e });
|
|
176
4434
|
}
|
|
177
4435
|
} else {
|
|
178
|
-
const desktopPath =
|
|
179
|
-
os.homedir(),
|
|
180
|
-
".local",
|
|
181
|
-
"share",
|
|
182
|
-
"applications",
|
|
183
|
-
"ahchat-bridge.desktop"
|
|
184
|
-
);
|
|
4436
|
+
const desktopPath = path10.join(os6.homedir(), ".local", "share", "applications", "ahchat-bridge.desktop");
|
|
185
4437
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
4438
|
+
fs6.unlinkSync(desktopPath);
|
|
4439
|
+
logger15.info("Linux protocol handler unregistered");
|
|
188
4440
|
} catch (e) {
|
|
189
|
-
|
|
4441
|
+
logger15.warn("Failed to unregister Linux protocol handler", { error: e });
|
|
190
4442
|
}
|
|
191
4443
|
}
|
|
192
4444
|
}
|
|
193
4445
|
function isProtocolRegistered() {
|
|
194
|
-
const platform =
|
|
4446
|
+
const platform = os6.platform();
|
|
195
4447
|
if (platform === "win32") {
|
|
196
4448
|
try {
|
|
197
4449
|
execSync('REG QUERY "HKCU\\Software\\Classes\\ahchat" /ve', { stdio: "pipe" });
|
|
@@ -200,59 +4452,16 @@ function isProtocolRegistered() {
|
|
|
200
4452
|
return false;
|
|
201
4453
|
}
|
|
202
4454
|
} else if (platform === "darwin") {
|
|
203
|
-
const appDir =
|
|
204
|
-
return
|
|
4455
|
+
const appDir = path10.join(os6.homedir(), "Applications", "AHChatBridge.app");
|
|
4456
|
+
return fs6.existsSync(path10.join(appDir, "Contents", "Info.plist"));
|
|
205
4457
|
} else {
|
|
206
|
-
const desktopPath =
|
|
207
|
-
|
|
208
|
-
".local",
|
|
209
|
-
"share",
|
|
210
|
-
"applications",
|
|
211
|
-
"ahchat-bridge.desktop"
|
|
212
|
-
);
|
|
213
|
-
return fs.existsSync(desktopPath);
|
|
4458
|
+
const desktopPath = path10.join(os6.homedir(), ".local", "share", "applications", "ahchat-bridge.desktop");
|
|
4459
|
+
return fs6.existsSync(desktopPath);
|
|
214
4460
|
}
|
|
215
4461
|
}
|
|
216
4462
|
|
|
217
4463
|
// src/cli.ts
|
|
218
|
-
var
|
|
219
|
-
var cli = cac("ahchat-bridge");
|
|
220
|
-
cli.command("install", "Register ahchat:// protocol handler (one-time setup)").action(() => {
|
|
221
|
-
registerProtocolHandler();
|
|
222
|
-
console.log("ahchat:// protocol handler registered successfully.");
|
|
223
|
-
console.log("You can now launch the bridge from your browser with one click.");
|
|
224
|
-
});
|
|
225
|
-
cli.command("uninstall", "Remove ahchat:// protocol handler").action(() => {
|
|
226
|
-
unregisterProtocolHandler();
|
|
227
|
-
console.log("ahchat:// protocol handler removed.");
|
|
228
|
-
});
|
|
229
|
-
cli.command("status", "Check if protocol handler is registered").action(() => {
|
|
230
|
-
const registered = isProtocolRegistered();
|
|
231
|
-
console.log(registered ? "ahchat:// protocol is registered." : "ahchat:// protocol is NOT registered.");
|
|
232
|
-
console.log('Run "npx @fangyb/ahchat-bridge install" to register it.');
|
|
233
|
-
});
|
|
234
|
-
cli.command("launch", "Launch bridge from ahchat:// URL (called by OS)").option("--url <url>", "ahchat:// URL with server and token params").action((args) => {
|
|
235
|
-
const url = args.url;
|
|
236
|
-
if (!url) {
|
|
237
|
-
console.error("Error: --url is required");
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
void runBridgeFromUrl(url);
|
|
241
|
-
});
|
|
242
|
-
cli.command("run", "Start the bridge and connect to server").option("--server-url <url>", "WebSocket URL of the AHChat server").option("--token <token>", "Auth token for server registration").option("--data-dir <dir>", "Data directory (default: ~/.ahchat)").option("--log-level <level>", "Log level (default: INFO)").action((args) => {
|
|
243
|
-
void runBridge({
|
|
244
|
-
serverUrl: args["server-url"],
|
|
245
|
-
token: args.token,
|
|
246
|
-
dataDir: args["data-dir"],
|
|
247
|
-
logLevel: args["log-level"]
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
cli.command("version", "Show bridge version").action(() => {
|
|
251
|
-
console.log("ahchat-bridge v0.1.0");
|
|
252
|
-
});
|
|
253
|
-
cli.help();
|
|
254
|
-
cli.version("0.1.0");
|
|
255
|
-
cli.parse();
|
|
4464
|
+
var logger16 = createModuleLogger("bridge");
|
|
256
4465
|
function parseAhchatUrl(url) {
|
|
257
4466
|
try {
|
|
258
4467
|
if (!url.startsWith("ahchat://")) return null;
|
|
@@ -270,148 +4479,50 @@ function parseAhchatUrl(url) {
|
|
|
270
4479
|
return null;
|
|
271
4480
|
}
|
|
272
4481
|
}
|
|
273
|
-
async function
|
|
274
|
-
const parsed = parseAhchatUrl(url);
|
|
275
|
-
if (!parsed) {
|
|
276
|
-
console.error("Invalid ahchat:// URL:", url);
|
|
277
|
-
console.error("Expected format: ahchat://bridge?server=ws://host:port/ws/bridge&token=xxx");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
await runBridge({
|
|
281
|
-
serverUrl: parsed.serverUrl,
|
|
282
|
-
token: parsed.token
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
async function runBridge(args) {
|
|
4482
|
+
async function run(args) {
|
|
286
4483
|
let config = loadBridgeConfig();
|
|
287
|
-
if (args.serverUrl) {
|
|
288
|
-
config = { ...config, serverUrl: args.serverUrl };
|
|
289
|
-
}
|
|
4484
|
+
if (args.serverUrl) config = { ...config, serverUrl: args.serverUrl };
|
|
290
4485
|
if (args.token) {
|
|
291
4486
|
const url = new URL(config.serverUrl);
|
|
292
4487
|
url.searchParams.set("token", args.token);
|
|
293
4488
|
config = { ...config, serverUrl: url.toString() };
|
|
294
4489
|
}
|
|
295
|
-
if (args.dataDir) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
bridgeId: config.bridgeId,
|
|
305
|
-
serverUrl: config.serverUrl,
|
|
306
|
-
serverApiUrl: config.serverApiUrl
|
|
307
|
-
});
|
|
308
|
-
wsMetrics.start(5e3);
|
|
309
|
-
const sessionStore = new SessionStore(config.dataDir);
|
|
310
|
-
const agentRegistry = new HttpAgentRegistry(config.serverApiUrl);
|
|
311
|
-
const groupRegistry = new GroupRegistry(config.serverApiUrl);
|
|
312
|
-
await agentRegistry.refresh();
|
|
313
|
-
await groupRegistry.refresh();
|
|
314
|
-
let connector = null;
|
|
315
|
-
const emit = (msg) => {
|
|
316
|
-
connector?.send(msg);
|
|
317
|
-
};
|
|
318
|
-
const askQuestionRegistry = new AskQuestionRegistry();
|
|
319
|
-
const agentManager = new AgentManager(sessionStore, emit, {
|
|
320
|
-
queryConfig: config.queryConfig,
|
|
321
|
-
askQuestionRegistry,
|
|
322
|
-
groupRegistry
|
|
4490
|
+
if (args.dataDir) config = { ...config, dataDir: args.dataDir };
|
|
4491
|
+
if (args.logLevel) config = { ...config, logLevel: args.logLevel };
|
|
4492
|
+
await startBridge(config);
|
|
4493
|
+
}
|
|
4494
|
+
var cli = cac("ahchat-bridge");
|
|
4495
|
+
cli.command("run", "Start the bridge and connect to server").option("--server-url <url>", "WebSocket URL of the AHChat server").option("--token <token>", "Auth token for server registration").option("--data-dir <dir>", "Data directory (default: ~/.ahchat)").option("--log-level <level>", "Log level (default: INFO)").action((args) => {
|
|
4496
|
+
void run(args).catch((e) => {
|
|
4497
|
+
logger16.error("Bridge failed to start", { error: e });
|
|
4498
|
+
process.exit(1);
|
|
323
4499
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
},
|
|
335
|
-
onConnected: async () => {
|
|
336
|
-
await agentRegistry.refresh();
|
|
337
|
-
await groupRegistry.refresh();
|
|
338
|
-
await agentManager.recoverFromRestart(agentRegistry.getAll());
|
|
339
|
-
},
|
|
340
|
-
onServerPush: async (msg) => {
|
|
341
|
-
switch (msg.type) {
|
|
342
|
-
case "bridge:list_models_request": {
|
|
343
|
-
const { requestId } = msg.payload;
|
|
344
|
-
logger2.info("list_models request received", { requestId });
|
|
345
|
-
try {
|
|
346
|
-
const models = await listModels();
|
|
347
|
-
connector?.send({
|
|
348
|
-
type: "bridge:list_models_response",
|
|
349
|
-
payload: { requestId, models }
|
|
350
|
-
});
|
|
351
|
-
logger2.info("list_models response sent", { requestId, count: models.length });
|
|
352
|
-
} catch (e) {
|
|
353
|
-
const err = e instanceof Error ? e.message : String(e);
|
|
354
|
-
connector?.send({
|
|
355
|
-
type: "bridge:list_models_response",
|
|
356
|
-
payload: { requestId, error: err }
|
|
357
|
-
});
|
|
358
|
-
logger2.error("list_models failed", { requestId, error: e });
|
|
359
|
-
}
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
case "agent:terminate":
|
|
363
|
-
await agentManager.terminate(msg.payload.agentId);
|
|
364
|
-
break;
|
|
365
|
-
case "agent:terminate_scope":
|
|
366
|
-
logger2.info("agent:terminate_scope received", {
|
|
367
|
-
agentId: msg.payload.agentId,
|
|
368
|
-
scope: msg.payload.scope
|
|
369
|
-
});
|
|
370
|
-
await agentManager.terminateScope(msg.payload.agentId, msg.payload.scope);
|
|
371
|
-
break;
|
|
372
|
-
case "agent:created":
|
|
373
|
-
case "agent:updated":
|
|
374
|
-
agentRegistry.upsert(msg.payload.agent);
|
|
375
|
-
break;
|
|
376
|
-
case "agent:deleted":
|
|
377
|
-
agentRegistry.remove(msg.payload.agentId);
|
|
378
|
-
break;
|
|
379
|
-
case "user:answer_question": {
|
|
380
|
-
const p = msg.payload;
|
|
381
|
-
const answerText = formatAnswerForSDK(p);
|
|
382
|
-
const ok = askQuestionRegistry.resolve(p.questionId, answerText);
|
|
383
|
-
logger2.info("user:answer_question handled", {
|
|
384
|
-
questionId: p.questionId,
|
|
385
|
-
agentId: p.agentId,
|
|
386
|
-
resolved: ok,
|
|
387
|
-
traceId: p.traceId
|
|
388
|
-
});
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
default:
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
4500
|
+
});
|
|
4501
|
+
cli.command("launch", "Launch bridge from ahchat:// URL (called by OS)").option("--url <url>", "ahchat:// URL with server and token params").action((args) => {
|
|
4502
|
+
const parsed = parseAhchatUrl(args.url);
|
|
4503
|
+
if (!parsed) {
|
|
4504
|
+
console.error("Invalid ahchat:// URL:", args.url);
|
|
4505
|
+
process.exit(1);
|
|
4506
|
+
}
|
|
4507
|
+
void run({ serverUrl: parsed.serverUrl, token: parsed.token }).catch((e) => {
|
|
4508
|
+
logger16.error("Bridge failed to start from URL", { error: e });
|
|
4509
|
+
process.exit(1);
|
|
395
4510
|
});
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
};
|
|
415
|
-
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
416
|
-
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
417
|
-
}
|
|
4511
|
+
});
|
|
4512
|
+
cli.command("install", "Register ahchat:// protocol handler (one-time setup)").action(() => {
|
|
4513
|
+
registerProtocolHandler();
|
|
4514
|
+
console.log("ahchat:// protocol handler registered successfully.");
|
|
4515
|
+
console.log("You can now launch the bridge from your browser with one click.");
|
|
4516
|
+
});
|
|
4517
|
+
cli.command("uninstall", "Remove ahchat:// protocol handler").action(() => {
|
|
4518
|
+
unregisterProtocolHandler();
|
|
4519
|
+
console.log("ahchat:// protocol handler removed.");
|
|
4520
|
+
});
|
|
4521
|
+
cli.command("status", "Check if protocol handler is registered").action(() => {
|
|
4522
|
+
const registered = isProtocolRegistered();
|
|
4523
|
+
console.log(registered ? "ahchat:// protocol is registered." : "ahchat:// protocol is NOT registered.");
|
|
4524
|
+
if (!registered) console.log('Run "npx @fangyb/ahchat-bridge install" to register it.');
|
|
4525
|
+
});
|
|
4526
|
+
cli.help();
|
|
4527
|
+
cli.version("0.1.0");
|
|
4528
|
+
cli.parse();
|