@fangyb/ahchat-bridge 0.1.7 → 0.1.8

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