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