dsh-interconnect 0.11.7 → 0.11.9

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.
@@ -1,5 +1,1737 @@
1
1
  // src/tool-interconnect/index.ts
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
+
4
+ // src/interconnect/index.ts
5
+ import { Service } from "@deepseek-ai/cordis";
6
+ import { createHash, randomUUID, timingSafeEqual as constantTimeEqual } from "node:crypto";
7
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
8
+ import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
9
+
10
+ // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/misc.ts
11
+ function isNullable(value) {
12
+ return value === null || value === void 0;
13
+ }
14
+ function isPlainObject(data) {
15
+ return data && typeof data === "object" && !Array.isArray(data);
16
+ }
17
+ function filterKeys(object, filter) {
18
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
19
+ }
20
+ function mapValues(object, transform) {
21
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
22
+ }
23
+ function pick(source, keys, forced) {
24
+ if (!keys) return { ...source };
25
+ const result = {};
26
+ for (const key of keys) {
27
+ if (forced || source[key] !== void 0) result[key] = source[key];
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/types.ts
33
+ function is(type, value) {
34
+ if (arguments.length === 1) return (value2) => is(type, value2);
35
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
36
+ }
37
+ function isArrayBufferLike(value) {
38
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
39
+ }
40
+ function isArrayBufferSource(value) {
41
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
42
+ }
43
+ var Binary;
44
+ ((Binary2) => {
45
+ Binary2.is = isArrayBufferLike;
46
+ Binary2.isSource = isArrayBufferSource;
47
+ function fromSource(source) {
48
+ if (ArrayBuffer.isView(source)) {
49
+ return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
50
+ } else {
51
+ return source;
52
+ }
53
+ }
54
+ Binary2.fromSource = fromSource;
55
+ function toBase64(source) {
56
+ source = fromSource(source);
57
+ if (typeof Buffer !== "undefined") {
58
+ return Buffer.from(source).toString("base64");
59
+ }
60
+ let binary = "";
61
+ const bytes = new Uint8Array(source);
62
+ for (let i = 0; i < bytes.byteLength; i++) {
63
+ binary += String.fromCharCode(bytes[i]);
64
+ }
65
+ return btoa(binary);
66
+ }
67
+ Binary2.toBase64 = toBase64;
68
+ function fromBase64(source) {
69
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
70
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
71
+ }
72
+ Binary2.fromBase64 = fromBase64;
73
+ function toHex(source) {
74
+ source = fromSource(source);
75
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
76
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
77
+ }
78
+ Binary2.toHex = toHex;
79
+ function fromHex(source) {
80
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
81
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
82
+ const buffer = [];
83
+ for (let i = 0; i < hex.length; i += 2) {
84
+ buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
85
+ }
86
+ return Uint8Array.from(buffer).buffer;
87
+ }
88
+ Binary2.fromHex = fromHex;
89
+ })(Binary || (Binary = {}));
90
+ var base64ToArrayBuffer = Binary.fromBase64;
91
+ var arrayBufferToBase64 = Binary.toBase64;
92
+ var hexToArrayBuffer = Binary.fromHex;
93
+ var arrayBufferToHex = Binary.toHex;
94
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
95
+ if (!source || typeof source !== "object") return source;
96
+ if (is("Date", source)) return new Date(source.valueOf());
97
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
98
+ if (isArrayBufferLike(source)) return source.slice(0);
99
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
100
+ const cached = refs.get(source);
101
+ if (cached) return cached;
102
+ if (Array.isArray(source)) {
103
+ const result2 = [];
104
+ refs.set(source, result2);
105
+ source.forEach((value, index) => {
106
+ result2[index] = Reflect.apply(clone, null, [value, refs]);
107
+ });
108
+ return result2;
109
+ }
110
+ const result = Object.create(Object.getPrototypeOf(source));
111
+ refs.set(source, result);
112
+ for (const key of Reflect.ownKeys(source)) {
113
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
114
+ if ("value" in descriptor) {
115
+ descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
116
+ }
117
+ Reflect.defineProperty(result, key, descriptor);
118
+ }
119
+ return result;
120
+ }
121
+ function deepEqual(a, b, strict) {
122
+ if (a === b) return true;
123
+ if (!strict && isNullable(a) && isNullable(b)) return true;
124
+ if (typeof a !== typeof b) return false;
125
+ if (typeof a !== "object") return false;
126
+ if (!a || !b) return false;
127
+ function check(test, then) {
128
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
129
+ }
130
+ return check(Array.isArray, (a2, b2) => a2.length === b2.length && a2.every((item, index) => deepEqual(item, b2[index]))) ?? check(is("Date"), (a2, b2) => a2.valueOf() === b2.valueOf()) ?? check(is("RegExp"), (a2, b2) => a2.source === b2.source && a2.flags === b2.flags) ?? check(isArrayBufferLike, (a2, b2) => {
131
+ if (a2.byteLength !== b2.byteLength) return false;
132
+ const viewA = new Uint8Array(a2);
133
+ const viewB = new Uint8Array(b2);
134
+ for (let i = 0; i < viewA.length; i++) {
135
+ if (viewA[i] !== viewB[i]) return false;
136
+ }
137
+ return true;
138
+ }) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
139
+ }
140
+
141
+ // ../.dsh/source/staging-20260807T143236Z/vendor/cosmokit/src/time.ts
142
+ var Time;
143
+ ((Time2) => {
144
+ Time2.millisecond = 1;
145
+ Time2.second = 1e3;
146
+ Time2.minute = Time2.second * 60;
147
+ Time2.hour = Time2.minute * 60;
148
+ Time2.day = Time2.hour * 24;
149
+ Time2.week = Time2.day * 7;
150
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
151
+ function setTimezoneOffset(offset) {
152
+ timezoneOffset = offset;
153
+ }
154
+ Time2.setTimezoneOffset = setTimezoneOffset;
155
+ function getTimezoneOffset() {
156
+ return timezoneOffset;
157
+ }
158
+ Time2.getTimezoneOffset = getTimezoneOffset;
159
+ function getDateNumber(date2 = /* @__PURE__ */ new Date(), offset) {
160
+ if (typeof date2 === "number") date2 = new Date(date2);
161
+ if (offset === void 0) offset = timezoneOffset;
162
+ return Math.floor((date2.valueOf() / Time2.minute - offset) / 1440);
163
+ }
164
+ Time2.getDateNumber = getDateNumber;
165
+ function fromDateNumber(value, offset) {
166
+ const date2 = new Date(value * Time2.day);
167
+ if (offset === void 0) offset = timezoneOffset;
168
+ return new Date(+date2 + offset * Time2.minute);
169
+ }
170
+ Time2.fromDateNumber = fromDateNumber;
171
+ const numeric = /\d+(?:\.\d+)?/.source;
172
+ const timeRegExp = new RegExp(`^${[
173
+ "w(?:eek(?:s)?)?",
174
+ "d(?:ay(?:s)?)?",
175
+ "h(?:our(?:s)?)?",
176
+ "m(?:in(?:ute)?(?:s)?)?",
177
+ "s(?:ec(?:ond)?(?:s)?)?"
178
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
179
+ function parseTime(source) {
180
+ const capture = timeRegExp.exec(source);
181
+ if (!capture) return 0;
182
+ return (parseFloat(capture[1]) * Time2.week || 0) + (parseFloat(capture[2]) * Time2.day || 0) + (parseFloat(capture[3]) * Time2.hour || 0) + (parseFloat(capture[4]) * Time2.minute || 0) + (parseFloat(capture[5]) * Time2.second || 0);
183
+ }
184
+ Time2.parseTime = parseTime;
185
+ function parseDate(date2) {
186
+ const parsed = parseTime(date2);
187
+ if (parsed) {
188
+ date2 = Date.now() + parsed;
189
+ } else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date2)) {
190
+ date2 = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date2}`;
191
+ } else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date2)) {
192
+ date2 = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date2}`;
193
+ }
194
+ return date2 ? new Date(date2) : /* @__PURE__ */ new Date();
195
+ }
196
+ Time2.parseDate = parseDate;
197
+ function format(ms) {
198
+ const abs = Math.abs(ms);
199
+ if (abs >= Time2.day - Time2.hour / 2) {
200
+ return Math.round(ms / Time2.day) + "d";
201
+ } else if (abs >= Time2.hour - Time2.minute / 2) {
202
+ return Math.round(ms / Time2.hour) + "h";
203
+ } else if (abs >= Time2.minute - Time2.second / 2) {
204
+ return Math.round(ms / Time2.minute) + "m";
205
+ } else if (abs >= Time2.second) {
206
+ return Math.round(ms / Time2.second) + "s";
207
+ }
208
+ return ms + "ms";
209
+ }
210
+ Time2.format = format;
211
+ function toDigits(source, length = 2) {
212
+ return source.toString().padStart(length, "0");
213
+ }
214
+ Time2.toDigits = toDigits;
215
+ function template(template2, time = /* @__PURE__ */ new Date()) {
216
+ return template2.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
217
+ }
218
+ Time2.template = template;
219
+ })(Time || (Time = {}));
220
+
221
+ // ../.dsh/source/staging-20260807T143236Z/vendor/schemastery/lib/index.mjs
222
+ var kSchema = Symbol.for("schemastery");
223
+ var kValidationError = Symbol.for("ValidationError");
224
+ globalThis.__schemastery_index__ ??= 0;
225
+ globalThis.__schemastery_refs__ = void 0;
226
+ var ValidationError = class extends TypeError {
227
+ options;
228
+ name = "ValidationError";
229
+ constructor(message, options) {
230
+ let prefix = "$";
231
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
232
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
233
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
234
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
235
+ super((prefix === "$" ? "" : `${prefix} `) + message);
236
+ this.options = options;
237
+ }
238
+ static is(error) {
239
+ return !!error?.[kValidationError];
240
+ }
241
+ };
242
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
243
+ var Schema = function(options) {
244
+ const schema = function(data, options2 = {}) {
245
+ return Schema.resolve(data, schema, options2)[0];
246
+ };
247
+ if (options.refs) {
248
+ const refs = mapValues(options.refs, (options2) => new Schema(options2));
249
+ const getRef = (uid) => refs[uid];
250
+ for (const key in refs) {
251
+ const options2 = refs[key];
252
+ options2.sKey = getRef(options2.sKey);
253
+ options2.inner = getRef(options2.inner);
254
+ options2.list = options2.list && options2.list.map(getRef);
255
+ options2.dict = options2.dict && mapValues(options2.dict, getRef);
256
+ }
257
+ return refs[options.uid];
258
+ }
259
+ Object.assign(schema, options);
260
+ if (typeof schema.callback === "string") try {
261
+ schema.callback = new Function("return " + schema.callback)();
262
+ } catch {
263
+ }
264
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
265
+ Object.setPrototypeOf(schema, Schema.prototype);
266
+ schema.meta ||= {};
267
+ schema.toString = schema.toString.bind(schema);
268
+ return schema;
269
+ };
270
+ Schema.prototype = Object.create(Function.prototype);
271
+ Schema.prototype[kSchema] = true;
272
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
273
+ return {
274
+ version: 1,
275
+ vendor: "schemastery",
276
+ validate: (value) => {
277
+ try {
278
+ return { value: Schema.resolve(value, this, {})[0] };
279
+ } catch (error) {
280
+ if (ValidationError.is(error)) return { issues: [{
281
+ message: error.message,
282
+ path: error.options.path
283
+ }] };
284
+ throw error;
285
+ }
286
+ }
287
+ };
288
+ } });
289
+ Schema.ValidationError = ValidationError;
290
+ Schema.prototype.toJSON = function toJSON() {
291
+ if (globalThis.__schemastery_refs__) {
292
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
293
+ return this.uid;
294
+ }
295
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
296
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
297
+ const result = {
298
+ uid: this.uid,
299
+ refs: globalThis.__schemastery_refs__
300
+ };
301
+ globalThis.__schemastery_refs__ = void 0;
302
+ return result;
303
+ };
304
+ Schema.prototype.set = function set(key, value) {
305
+ this.dict[key] = value;
306
+ return this;
307
+ };
308
+ Schema.prototype.push = function push(value) {
309
+ this.list.push(value);
310
+ return this;
311
+ };
312
+ function mergeDesc(original, messages) {
313
+ const result = typeof original === "string" ? { "": original } : { ...original };
314
+ for (const locale in messages) {
315
+ const value = messages[locale];
316
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
317
+ else if (typeof value === "string") result[locale] = value;
318
+ }
319
+ return result;
320
+ }
321
+ function getInner(value) {
322
+ return value?.$value ?? value?.$inner;
323
+ }
324
+ function extractKeys(data) {
325
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
326
+ }
327
+ Schema.prototype.i18n = function i18n(messages) {
328
+ const schema = Schema(this);
329
+ const desc = mergeDesc(schema.meta.description, messages);
330
+ if (Object.keys(desc).length) schema.meta.description = desc;
331
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
332
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
333
+ });
334
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
335
+ return inner.i18n(mapValues(messages, (data = {}) => {
336
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
337
+ if (Array.isArray(data)) return data[index];
338
+ return extractKeys(data);
339
+ }));
340
+ });
341
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
342
+ if (getInner(data)) return getInner(data);
343
+ return extractKeys(data);
344
+ }));
345
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
346
+ return schema;
347
+ };
348
+ Schema.prototype.extra = function extra(key, value) {
349
+ const schema = Schema(this);
350
+ schema.meta = {
351
+ ...schema.meta,
352
+ [key]: value
353
+ };
354
+ return schema;
355
+ };
356
+ for (const key of [
357
+ "required",
358
+ "disabled",
359
+ "collapse",
360
+ "hidden",
361
+ "loose"
362
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
363
+ const schema = Schema(this);
364
+ schema.meta = {
365
+ ...schema.meta,
366
+ [key]: value
367
+ };
368
+ return schema;
369
+ } });
370
+ Schema.prototype.deprecated = function deprecated() {
371
+ const schema = Schema(this);
372
+ schema.meta.badges ||= [];
373
+ schema.meta.badges.push({
374
+ text: "deprecated",
375
+ type: "danger"
376
+ });
377
+ return schema;
378
+ };
379
+ Schema.prototype.experimental = function experimental() {
380
+ const schema = Schema(this);
381
+ schema.meta.badges ||= [];
382
+ schema.meta.badges.push({
383
+ text: "experimental",
384
+ type: "warning"
385
+ });
386
+ return schema;
387
+ };
388
+ Schema.prototype.pattern = function pattern(regexp) {
389
+ const schema = Schema(this);
390
+ const pattern2 = pick(regexp, ["source", "flags"]);
391
+ schema.meta = {
392
+ ...schema.meta,
393
+ pattern: pattern2
394
+ };
395
+ return schema;
396
+ };
397
+ Schema.prototype.simplify = function simplify(value) {
398
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
399
+ if (isNullable(value)) return value;
400
+ if (this.type === "object" || this.type === "dict") {
401
+ const result = {};
402
+ for (const key in value) {
403
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
404
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
405
+ }
406
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
407
+ return result;
408
+ } else if (this.type === "array" || this.type === "tuple") {
409
+ const result = [];
410
+ value.forEach((value2, index) => {
411
+ const schema = this.type === "array" ? this.inner : this.list[index];
412
+ const item = schema ? schema.simplify(value2) : value2;
413
+ result.push(item);
414
+ });
415
+ return result;
416
+ } else if (this.type === "intersect") {
417
+ const result = {};
418
+ for (const item of this.list) Object.assign(result, item.simplify(value));
419
+ return result;
420
+ } else if (this.type === "union") for (const schema of this.list) try {
421
+ Schema.resolve(value, schema, {});
422
+ return schema.simplify(value);
423
+ } catch {
424
+ }
425
+ return value;
426
+ };
427
+ Schema.prototype.toString = function toString(inline) {
428
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
429
+ };
430
+ Schema.prototype.role = function role(role, extra2) {
431
+ const schema = Schema(this);
432
+ schema.meta = {
433
+ ...schema.meta,
434
+ role,
435
+ extra: extra2
436
+ };
437
+ return schema;
438
+ };
439
+ for (const key of [
440
+ "default",
441
+ "link",
442
+ "comment",
443
+ "description",
444
+ "max",
445
+ "min",
446
+ "step"
447
+ ]) Object.assign(Schema.prototype, { [key](value) {
448
+ const schema = Schema(this);
449
+ schema.meta = {
450
+ ...schema.meta,
451
+ [key]: value
452
+ };
453
+ return schema;
454
+ } });
455
+ var resolvers = {};
456
+ Schema.extend = function extend(type, resolve2) {
457
+ resolvers[type] = resolve2;
458
+ };
459
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
460
+ if (!schema) return [data];
461
+ if (options.ignore?.(data, schema)) return [data];
462
+ if (isNullable(data) && schema.type !== "lazy") {
463
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
464
+ let current = schema;
465
+ let fallback = schema.meta.default;
466
+ while (current?.type === "intersect" && isNullable(fallback)) {
467
+ current = current.list[0];
468
+ fallback = current?.meta.default;
469
+ }
470
+ if (isNullable(fallback)) return [data];
471
+ data = clone(fallback);
472
+ }
473
+ const callback = resolvers[schema.type];
474
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
475
+ try {
476
+ return callback(data, schema, options, strict);
477
+ } catch (error) {
478
+ if (!schema.meta.loose) throw error;
479
+ return [schema.meta.default];
480
+ }
481
+ };
482
+ Schema.from = function from(source) {
483
+ if (isNullable(source)) return Schema.any();
484
+ else if ([
485
+ "string",
486
+ "number",
487
+ "boolean"
488
+ ].includes(typeof source)) return Schema.const(source).required();
489
+ else if (source[kSchema]) return source;
490
+ else if (typeof source === "function") switch (source) {
491
+ case String:
492
+ return Schema.string().required();
493
+ case Number:
494
+ return Schema.number().required();
495
+ case Boolean:
496
+ return Schema.boolean().required();
497
+ case Function:
498
+ return Schema.function().required();
499
+ default:
500
+ return Schema.is(source).required();
501
+ }
502
+ else throw new TypeError(`cannot infer schema from ${source}`);
503
+ };
504
+ Schema.lazy = function lazy(builder) {
505
+ const toJSON2 = () => {
506
+ if (!schema.inner[kSchema]) {
507
+ schema.inner = schema.builder();
508
+ schema.inner.meta = {
509
+ ...schema.meta,
510
+ ...schema.inner.meta
511
+ };
512
+ }
513
+ return schema.inner.toJSON();
514
+ };
515
+ const schema = new Schema({
516
+ type: "lazy",
517
+ builder,
518
+ inner: { toJSON: toJSON2 }
519
+ });
520
+ return schema;
521
+ };
522
+ Schema.natural = function natural() {
523
+ return Schema.number().step(1).min(0);
524
+ };
525
+ Schema.percent = function percent() {
526
+ return Schema.number().step(0.01).min(0).max(1).role("slider");
527
+ };
528
+ Schema.date = function date() {
529
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
530
+ const date2 = new Date(value);
531
+ if (isNaN(+date2)) throw new ValidationError(`invalid date "${value}"`, options);
532
+ return date2;
533
+ }, true)]);
534
+ };
535
+ Schema.regExp = function regExp(flag = "") {
536
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
537
+ try {
538
+ return new RegExp(value, flag);
539
+ } catch (e) {
540
+ throw new ValidationError(e.message, options);
541
+ }
542
+ }, true)]);
543
+ };
544
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
545
+ return Schema.union([
546
+ Schema.is(ArrayBuffer),
547
+ Schema.is(SharedArrayBuffer),
548
+ Schema.transform(Schema.any(), (value, options) => {
549
+ if (Binary.isSource(value)) return Binary.fromSource(value);
550
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
551
+ }, true),
552
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
553
+ try {
554
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
555
+ } catch (e) {
556
+ throw new ValidationError(e.message, options);
557
+ }
558
+ }, true)] : []
559
+ ]);
560
+ };
561
+ Schema.extend("lazy", (data, schema, options, strict) => {
562
+ if (!schema.inner[kSchema]) {
563
+ schema.inner = schema.builder();
564
+ schema.inner.meta = {
565
+ ...schema.meta,
566
+ ...schema.inner.meta
567
+ };
568
+ }
569
+ return Schema.resolve(data, schema.inner, options, strict);
570
+ });
571
+ Schema.extend("any", (data) => {
572
+ return [data];
573
+ });
574
+ Schema.extend("never", (data, _, options) => {
575
+ throw new ValidationError(`expected nullable but got ${data}`, options);
576
+ });
577
+ Schema.extend("const", (data, { value }, options) => {
578
+ if (deepEqual(data, value)) return [value];
579
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
580
+ });
581
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
582
+ const { max = Infinity, min = -Infinity } = meta;
583
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
584
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
585
+ }
586
+ Schema.extend("string", (data, { meta }, options) => {
587
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
588
+ if (meta.pattern) {
589
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
590
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
591
+ }
592
+ checkWithinRange(data.length, meta, "string length", options);
593
+ return [data];
594
+ });
595
+ function decimalShift(data, digits) {
596
+ const str = data.toString();
597
+ if (str.includes("e")) return data * Math.pow(10, digits);
598
+ const index = str.indexOf(".");
599
+ if (index === -1) return data * Math.pow(10, digits);
600
+ const frac = str.slice(index + 1);
601
+ const integer = str.slice(0, index);
602
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
603
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
604
+ }
605
+ function isMultipleOf(data, min, step) {
606
+ step = Math.abs(step);
607
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
608
+ const index = step.toString().indexOf(".");
609
+ const digits = step.toString().slice(index + 1).length;
610
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
611
+ }
612
+ Schema.extend("number", (data, { meta }, options) => {
613
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
614
+ checkWithinRange(data, meta, "number", options);
615
+ const { step } = meta;
616
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
617
+ return [data];
618
+ });
619
+ Schema.extend("boolean", (data, _, options) => {
620
+ if (typeof data === "boolean") return [data];
621
+ throw new ValidationError(`expected boolean but got ${data}`, options);
622
+ });
623
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
624
+ let value = 0, keys = [];
625
+ if (typeof data === "number") {
626
+ value = data;
627
+ for (const key in bits) if (data & bits[key]) keys.push(key);
628
+ } else if (Array.isArray(data)) {
629
+ keys = data;
630
+ for (const key of keys) {
631
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
632
+ if (key in bits) value |= bits[key];
633
+ }
634
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
635
+ if (value === meta.default) return [value];
636
+ return [value, keys];
637
+ });
638
+ Schema.extend("function", (data, _, options) => {
639
+ if (typeof data === "function") return [data];
640
+ throw new ValidationError(`expected function but got ${data}`, options);
641
+ });
642
+ Schema.extend("is", (data, { constructor }, options) => {
643
+ if (typeof constructor === "function") {
644
+ if (data instanceof constructor) return [data];
645
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
646
+ } else {
647
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
648
+ let prototype = Object.getPrototypeOf(data);
649
+ while (prototype) {
650
+ if (prototype.constructor?.name === constructor) return [data];
651
+ prototype = Object.getPrototypeOf(prototype);
652
+ }
653
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
654
+ }
655
+ });
656
+ function property(data, key, schema, options) {
657
+ try {
658
+ const [value, adapted] = Schema.resolve(data[key], schema, {
659
+ ...options,
660
+ path: [...options.path || [], key]
661
+ });
662
+ if (adapted !== void 0) data[key] = adapted;
663
+ return value;
664
+ } catch (e) {
665
+ if (!options?.autofix) throw e;
666
+ delete data[key];
667
+ return schema.meta.default;
668
+ }
669
+ }
670
+ Schema.extend("array", (data, { inner, meta }, options) => {
671
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
672
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
673
+ return [data.map((_, index) => property(data, index, inner, options))];
674
+ });
675
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
676
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
677
+ const result = {};
678
+ for (const key in data) {
679
+ let rKey;
680
+ try {
681
+ rKey = Schema.resolve(key, sKey, options)[0];
682
+ } catch (error) {
683
+ if (strict) continue;
684
+ throw error;
685
+ }
686
+ result[rKey] = property(data, key, inner, options);
687
+ data[rKey] = data[key];
688
+ if (key !== rKey) delete data[key];
689
+ }
690
+ return [result];
691
+ });
692
+ Schema.extend("tuple", (data, { list }, options, strict) => {
693
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
694
+ const result = list.map((inner, index) => property(data, index, inner, options));
695
+ if (strict) return [result];
696
+ result.push(...data.slice(list.length));
697
+ return [result];
698
+ });
699
+ function merge(result, data) {
700
+ for (const key in data) {
701
+ if (key in result) continue;
702
+ result[key] = data[key];
703
+ }
704
+ }
705
+ Schema.extend("object", (data, { dict }, options, strict) => {
706
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
707
+ const result = {};
708
+ for (const key in dict) {
709
+ const value = property(data, key, dict[key], options);
710
+ if (!isNullable(value) || key in data) result[key] = value;
711
+ }
712
+ if (!strict) merge(result, data);
713
+ return [result];
714
+ });
715
+ Schema.extend("union", (data, { list, toString: toString2 }, options, strict) => {
716
+ const messages = [];
717
+ for (const inner of list) try {
718
+ return Schema.resolve(data, inner, options, strict);
719
+ } catch (error) {
720
+ messages.push(error);
721
+ }
722
+ throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
723
+ });
724
+ Schema.extend("intersect", (data, { list, toString: toString2 }, options, strict) => {
725
+ if (!list.length) return [data];
726
+ let result;
727
+ for (const inner of list) {
728
+ const value = Schema.resolve(data, inner, options, true)[0];
729
+ if (isNullable(value)) continue;
730
+ if (isNullable(result)) result = value;
731
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
732
+ else if (typeof value === "object") merge(result ??= {}, value);
733
+ else if (result !== value) throw new ValidationError(`expected ${toString2()} but got ${JSON.stringify(data)}`, options);
734
+ }
735
+ if (!strict && isPlainObject(data)) merge(result, data);
736
+ return [result];
737
+ });
738
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
739
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
740
+ if (preserve) return [callback(result)];
741
+ else return [callback(result), callback(adapted)];
742
+ });
743
+ var formatters = {};
744
+ function defineMethod(name2, keys, format) {
745
+ formatters[name2] = format;
746
+ Object.assign(Schema, { [name2](...args) {
747
+ const schema = new Schema({ type: name2 });
748
+ keys.forEach((key, index) => {
749
+ switch (key) {
750
+ case "sKey":
751
+ schema.sKey = args[index] ?? Schema.string();
752
+ break;
753
+ case "inner":
754
+ schema.inner = Schema.from(args[index]);
755
+ break;
756
+ case "list":
757
+ schema.list = args[index].map(Schema.from);
758
+ break;
759
+ case "dict":
760
+ schema.dict = mapValues(args[index], Schema.from);
761
+ break;
762
+ case "bits":
763
+ schema.bits = {};
764
+ for (const key2 in args[index]) {
765
+ if (typeof args[index][key2] !== "number") continue;
766
+ schema.bits[key2] = args[index][key2];
767
+ }
768
+ break;
769
+ case "callback": {
770
+ const callback = schema.callback = args[index];
771
+ callback["toJSON"] ||= () => callback.toString();
772
+ break;
773
+ }
774
+ case "constructor": {
775
+ const constructor = schema.constructor = args[index];
776
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
777
+ break;
778
+ }
779
+ default:
780
+ schema[key] = args[index];
781
+ }
782
+ });
783
+ if (name2 === "object" || name2 === "dict") schema.meta.default = {};
784
+ else if (name2 === "array" || name2 === "tuple") schema.meta.default = [];
785
+ else if (name2 === "bitset") schema.meta.default = 0;
786
+ return schema;
787
+ } });
788
+ }
789
+ defineMethod("is", ["constructor"], ({ constructor }) => {
790
+ if (typeof constructor === "function") return constructor.name;
791
+ else return constructor;
792
+ });
793
+ defineMethod("any", [], () => "any");
794
+ defineMethod("never", [], () => "never");
795
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
796
+ defineMethod("string", [], () => "string");
797
+ defineMethod("number", [], () => "number");
798
+ defineMethod("boolean", [], () => "boolean");
799
+ defineMethod("bitset", ["bits"], () => "bitset");
800
+ defineMethod("function", [], () => "function");
801
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
802
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
803
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
804
+ defineMethod("object", ["dict"], ({ dict }) => {
805
+ if (Object.keys(dict).length === 0) return "{}";
806
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
807
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
808
+ }).join(", ")} }`;
809
+ });
810
+ defineMethod("union", ["list"], ({ list }, inline) => {
811
+ const result = list.map(({ toString: format }) => format()).join(" | ");
812
+ return inline ? `(${result})` : result;
813
+ });
814
+ defineMethod("intersect", ["list"], ({ list }) => {
815
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
816
+ });
817
+ defineMethod("transform", [
818
+ "inner",
819
+ "callback",
820
+ "preserve"
821
+ ], ({ inner }, isInner) => inner.toString(isInner));
822
+
823
+ // src/interconnect/index.ts
824
+ import WebSocket, { WebSocketServer } from "ws";
825
+ function isSessionOwnedBySubagent(ctx, session, agent) {
826
+ if (session.header.origin === "subagent") return true;
827
+ const parentId = session.header.parentSession;
828
+ if (parentId === void 0 || agent === void 0) return false;
829
+ const parent = ctx.agents.get(parentId);
830
+ return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
831
+ }
832
+ function assertNever(value) {
833
+ throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
834
+ }
835
+ var INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
836
+ var PLUGIN_SOURCE = "dsh-interconnect";
837
+ var MAX_LINK_FRAME_BYTES = 1024 * 1024;
838
+ var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
839
+ var LINK_CHANNEL = "/interconnect/link";
840
+ var LIST_FRAME_ENVELOPE_BYTES = 4096;
841
+ var MAX_LIST_ROWS_BYTES = MAX_LINK_FRAME_BYTES - LIST_FRAME_ENVELOPE_BYTES;
842
+ var MAX_LISTED_SESSIONS = 100;
843
+ var rowBytes = (row) => Buffer.byteLength(JSON.stringify(row), "utf8") + 1;
844
+ var notificationSchema = Schema.union([
845
+ Schema.object({ kind: Schema.const("agent/created").required(), sessionId: Schema.string().required() }),
846
+ Schema.object({ kind: Schema.const("agent/disposed").required(), sessionId: Schema.string().required() }),
847
+ Schema.object({ kind: Schema.const("agent/status").required(), sessionId: Schema.string().required(), status: Schema.union([Schema.const("idle"), Schema.const("running")]).required() }),
848
+ Schema.object({ kind: Schema.const("session/created").required(), sessionId: Schema.string().required(), parentSessionId: Schema.string() }),
849
+ Schema.object({ kind: Schema.const("session/disposed").required(), sessionId: Schema.string().required() }),
850
+ Schema.object({ kind: Schema.const("subagent/end").required(), provider: Schema.string().required(), childSessionId: Schema.string().required(), stopReason: Schema.string().required() })
851
+ ]);
852
+ var senderSchema = Schema.object({ instanceId: Schema.string(), sessionId: Schema.string() });
853
+ var pingResultSchema = Schema.object({ pong: Schema.const(true).required(), instance: Schema.string().required() });
854
+ var listResultSchema = Schema.object({
855
+ instance: Schema.string().required(),
856
+ sessions: Schema.array(Schema.object({
857
+ sessionId: Schema.string().required(),
858
+ title: Schema.string().required(false),
859
+ status: Schema.string().required(false)
860
+ })).required()
861
+ });
862
+ var withoutNullFields = (value) => {
863
+ if (Array.isArray(value)) return value.map((item) => withoutNullFields(item));
864
+ if (value === null || typeof value !== "object") return value;
865
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== null).map(([key, field]) => [key, withoutNullFields(field)]));
866
+ };
867
+ var projectMsgResult = (result) => result.delivered ? { delivered: true, instance: result.instance, ...result.delivery === void 0 ? {} : { delivery: result.delivery } } : { delivered: false, instance: result.instance, reason: result.reason };
868
+ var parseQueryResult = (schema, result) => {
869
+ try {
870
+ const [parsed] = Schema.resolve(result, schema, {});
871
+ return parsed;
872
+ } catch {
873
+ return void 0;
874
+ }
875
+ };
876
+ var projectPingResult = (result) => {
877
+ const parsed = parseQueryResult(pingResultSchema, result);
878
+ return parsed === void 0 ? void 0 : { pong: true, instance: parsed.instance };
879
+ };
880
+ var projectListResult = (result) => {
881
+ const parsed = parseQueryResult(listResultSchema, result);
882
+ if (parsed === void 0) return void 0;
883
+ const rows = parsed.sessions;
884
+ return {
885
+ instance: parsed.instance,
886
+ sessions: rows.map((row) => ({
887
+ sessionId: row.sessionId,
888
+ ...row.title === void 0 || row.title === null ? {} : { title: row.title },
889
+ ...row.status === void 0 || row.status === null ? {} : { status: row.status }
890
+ }))
891
+ };
892
+ };
893
+ var queryResultSchema = Schema.union([
894
+ pingResultSchema,
895
+ listResultSchema,
896
+ Schema.object({ accepted: Schema.const(true).required() })
897
+ ]);
898
+ var messageSchema = Schema.object({
899
+ kind: Schema.const("send").required(),
900
+ sessionId: Schema.string().required(),
901
+ text: Schema.string().required(),
902
+ sender: senderSchema,
903
+ delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]),
904
+ resume: Schema.boolean()
905
+ });
906
+ var querySchema = Schema.union([
907
+ Schema.object({ kind: Schema.const("ping").required() }),
908
+ Schema.object({ kind: Schema.const("list").required() }),
909
+ Schema.object({ kind: Schema.const("event").required(), notification: notificationSchema.required() })
910
+ ]);
911
+ var msgResultSchema = Schema.union([
912
+ Schema.object({ delivered: Schema.const(true).required(), instance: Schema.string().required(), delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]) }),
913
+ Schema.object({ delivered: Schema.const(false).required(), instance: Schema.string().required(), reason: Schema.union([
914
+ Schema.const("session-not-live"),
915
+ Schema.const("unreachable"),
916
+ Schema.const("resume-refused"),
917
+ Schema.const("resume-failed"),
918
+ Schema.const("session-owned-by-subagent"),
919
+ Schema.const("no-sender-known")
920
+ ]).required() })
921
+ ]);
922
+ var linkFrameSchema = Schema.union([
923
+ Schema.object({ type: Schema.const("hello").required(), sender: Schema.string().required() }),
924
+ Schema.object({ type: Schema.const("event").required(), notification: notificationSchema.required() }),
925
+ Schema.object({ type: Schema.const("msg").required(), reqId: Schema.string().required(), message: messageSchema.required() }),
926
+ Schema.object({ type: Schema.const("msg-result").required(), reqId: Schema.string().required(), result: msgResultSchema.required() }),
927
+ Schema.object({ type: Schema.const("query").required(), reqId: Schema.string().required(), query: querySchema.required() }),
928
+ Schema.object({ type: Schema.const("query-result").required(), reqId: Schema.string().required(), result: queryResultSchema.required() })
929
+ ]);
930
+ var InterconnectService = class extends Service {
931
+ static inject = ["agents", "credentials"];
932
+ static Config = Schema.object({
933
+ instanceId: Schema.string().default("dsh"),
934
+ requestTimeoutMs: Schema.natural().max(6e4).default(1e4),
935
+ peers: Schema.dict(Schema.string()).default({}),
936
+ delivery: Schema.union([Schema.const("followup"), Schema.const("steer"), Schema.const("inject")]).default("followup"),
937
+ allowResume: Schema.boolean().default(true)
938
+ });
939
+ instanceId;
940
+ requestTimeoutMs;
941
+ delivery;
942
+ allowResume;
943
+ subscriptions = [];
944
+ server = new WebSocketServer({ noServer: true, maxPayload: MAX_LINK_FRAME_BYTES });
945
+ sockets = /* @__PURE__ */ new Set();
946
+ /** Outbound peer links keyed by the peer's `instanceId`. */
947
+ linkStates = /* @__PURE__ */ new Map();
948
+ heartbeatTimer;
949
+ /** Peer identity each live socket announced via its `hello` frame, if any. */
950
+ peerOf = /* @__PURE__ */ new WeakMap();
951
+ /** Sender each local session last received a send from, keyed by local session id. */
952
+ senders = /* @__PURE__ */ new Map();
953
+ /** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
954
+ pendingMessages = /* @__PURE__ */ new Map();
955
+ reqIdCounter = 0;
956
+ /** Set when the service fiber unwinds; guards late inbound deliveries against driving disposed agents. */
957
+ disposed = false;
958
+ constructor(ctx, config) {
959
+ super(ctx, "interconnect");
960
+ this.instanceId = config.instanceId;
961
+ this.requestTimeoutMs = config.requestTimeoutMs;
962
+ this.delivery = config.delivery ?? "followup";
963
+ this.allowResume = config.allowResume ?? true;
964
+ for (const origin of Object.values(config.peers ?? {})) {
965
+ linkUrl(trimBase(origin));
966
+ }
967
+ for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
968
+ this.link(peerInstanceId, origin);
969
+ }
970
+ const upgrade = {
971
+ path: LINK_CHANNEL,
972
+ handler: (req, socket, head) => {
973
+ void this.handleUpgrade(req, socket, head).catch((error) => {
974
+ this.ctx.logger.warn(`interconnect: upgrade failed: ${error instanceof Error ? error.message : String(error)}`);
975
+ socket.destroy();
976
+ });
977
+ }
978
+ };
979
+ ctx.inject(["webServer"], (webCtx) => {
980
+ webCtx.effect(() => webCtx.webServer.registerUpgrade(upgrade), "interconnect: /interconnect/link websocket");
981
+ });
982
+ this.heartbeatTimer = setInterval(() => {
983
+ for (const socket of this.sockets) {
984
+ if (socket.readyState !== WebSocket.OPEN) {
985
+ this.sockets.delete(socket);
986
+ continue;
987
+ }
988
+ if (socket.isAlive === false) {
989
+ socket.terminate();
990
+ this.sockets.delete(socket);
991
+ continue;
992
+ }
993
+ ;
994
+ socket.isAlive = false;
995
+ socket.ping();
996
+ }
997
+ }, 3e4);
998
+ this.subscriptions.push(ctx.on("agent/status", ({ agent, status }) => {
999
+ this.fanout({ kind: "agent/status", sessionId: String(agent.session.id), status });
1000
+ }));
1001
+ this.subscriptions.push(ctx.on("agent/created", ({ agent }) => {
1002
+ this.fanout({ kind: "agent/created", sessionId: String(agent.session.id) });
1003
+ }));
1004
+ this.subscriptions.push(ctx.on("agent/disposed", ({ agent }) => {
1005
+ this.fanout({ kind: "agent/disposed", sessionId: String(agent.session.id) });
1006
+ }));
1007
+ this.subscriptions.push(ctx.on("session/created", (session) => {
1008
+ const parentSessionId = session.header.parentSession === void 0 ? void 0 : String(session.header.parentSession);
1009
+ this.fanout({
1010
+ kind: "session/created",
1011
+ sessionId: String(session.id),
1012
+ ...parentSessionId === void 0 ? {} : { parentSessionId }
1013
+ });
1014
+ }));
1015
+ this.subscriptions.push(ctx.on("session/disposed", (session) => {
1016
+ this.senders.delete(String(session.id));
1017
+ this.fanout({ kind: "session/disposed", sessionId: String(session.id) });
1018
+ }));
1019
+ this.subscriptions.push(ctx.on("subagent/end", (info) => {
1020
+ if (!info.local) return;
1021
+ this.fanout({
1022
+ kind: "subagent/end",
1023
+ provider: info.provider,
1024
+ childSessionId: String(info.id),
1025
+ stopReason: info.stopReason
1026
+ });
1027
+ }));
1028
+ ctx.effect(() => () => {
1029
+ for (const dispose of this.subscriptions.splice(0)) dispose();
1030
+ }, "interconnect: event subscriptions");
1031
+ ctx.effect(() => () => {
1032
+ if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
1033
+ for (const state of this.linkStates.values()) state.close();
1034
+ this.linkStates.clear();
1035
+ for (const socket of this.sockets) socket.terminate();
1036
+ this.sockets.clear();
1037
+ this.server.close();
1038
+ for (const pending of this.pendingMessages.values()) {
1039
+ clearTimeout(pending.timer);
1040
+ pending.resolve?.(void 0);
1041
+ }
1042
+ this.pendingMessages.clear();
1043
+ this.disposed = true;
1044
+ }, "interconnect: websocket teardown");
1045
+ }
1046
+ /**
1047
+ * This instance's identity to attach to outbound messages. Always present:
1048
+ * it needs no address, only this instance's id and the calling session.
1049
+ * @param sessionId - the local session that is sending, used as the reply target.
1050
+ * @returns this instance's address-free identity for the calling session.
1051
+ */
1052
+ selfSender(sessionId) {
1053
+ return { instanceId: this.instanceId, sessionId };
1054
+ }
1055
+ /**
1056
+ * Deliver one text message to a live session on a peer instance.
1057
+ * @param request - the peer instance id, target session, text, and optional delivery/resume overrides.
1058
+ * @returns the peer's answer, or an unreachable result when no link answers.
1059
+ */
1060
+ async send(request) {
1061
+ const payload = {
1062
+ sessionId: request.sessionId,
1063
+ text: request.text,
1064
+ ...request.sender === void 0 ? {} : { sender: request.sender },
1065
+ ...request.delivery === void 0 ? {} : { delivery: request.delivery },
1066
+ ...request.resume === void 0 ? {} : { resume: request.resume }
1067
+ };
1068
+ try {
1069
+ const result = await this.msgRequest(request.instanceId, "send", payload);
1070
+ return result ?? { delivered: false, instance: request.instanceId, reason: "unreachable" };
1071
+ } catch {
1072
+ return { delivered: false, instance: request.instanceId, reason: "unreachable" };
1073
+ }
1074
+ }
1075
+ /**
1076
+ * Deliver one text message back to the peer that a local session last
1077
+ * received a send from. `sessionId` names the LOCAL replying session; the
1078
+ * outbound target is the `sender` that session recorded, addressed through
1079
+ * this instance's own link to the sender's instance.
1080
+ * @param request - the LOCAL replying session id, the reply text, and optional delivery/resume overrides.
1081
+ * @returns the recalled sender's answer, or an unreachable result when no link answers.
1082
+ */
1083
+ async reply(request) {
1084
+ const sender = this.senders.get(request.sessionId);
1085
+ if (sender === void 0) {
1086
+ return { delivered: false, instance: this.instanceId, reason: "no-sender-known" };
1087
+ }
1088
+ const payload = {
1089
+ sessionId: sender.sessionId,
1090
+ text: request.text,
1091
+ ...request.delivery === void 0 ? {} : { delivery: request.delivery },
1092
+ ...request.resume === void 0 ? {} : { resume: request.resume },
1093
+ // Attribute the reply to this instance so the peer, in turn, can reply
1094
+ // back — chaining the conversation.
1095
+ sender: this.selfSender(request.sessionId)
1096
+ };
1097
+ try {
1098
+ const result = await this.msgRequest(sender.instanceId, "send", payload);
1099
+ return result ?? { delivered: false, instance: sender.instanceId, reason: "unreachable" };
1100
+ } catch {
1101
+ return { delivered: false, instance: sender.instanceId, reason: "unreachable" };
1102
+ }
1103
+ }
1104
+ /**
1105
+ * Probe a peer instance for liveness and identity over its persistent link.
1106
+ * Returns the peer identity when reachable, or undefined when the link is
1107
+ * not up or no answer arrives.
1108
+ * @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
1109
+ * @returns the peer's identity, or undefined when the link is not up or no answer arrives.
1110
+ */
1111
+ async ping(instanceId) {
1112
+ return this.queryRequest(instanceId, { kind: "ping" });
1113
+ }
1114
+ /**
1115
+ * List a peer instance's live sessions over its persistent link. Undefined on
1116
+ * transport failure, matching `ping`.
1117
+ * @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
1118
+ * @returns the peer's live session rows, or undefined on transport failure.
1119
+ */
1120
+ async list(instanceId) {
1121
+ return this.queryRequest(instanceId, { kind: "list" });
1122
+ }
1123
+ /**
1124
+ * Send a `msg` frame over the live link to a peer and resolve with its
1125
+ * correlated `msg-result`. Undefined when the peer has no live link here
1126
+ * (not configured, or the link is down) — sends can only address configured+
1127
+ * connected peers by design.
1128
+ */
1129
+ async msgRequest(instanceId, kind, payload) {
1130
+ const state = this.linkStates.get(instanceId);
1131
+ if (state === void 0 || !state.writable()) return void 0;
1132
+ const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
1133
+ const message = {
1134
+ kind,
1135
+ sessionId: payload.sessionId,
1136
+ text: payload.text,
1137
+ ...payload.sender === void 0 ? {} : { sender: payload.sender },
1138
+ ...payload.delivery === void 0 ? {} : { delivery: payload.delivery },
1139
+ ...payload.resume === void 0 ? {} : { resume: payload.resume }
1140
+ };
1141
+ return this.waitForResult(reqId, "msg", (failure) => {
1142
+ const wrote = state.sendFrame({ type: "msg", reqId, message });
1143
+ if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while sending`));
1144
+ });
1145
+ }
1146
+ /** Send a `query` frame over the live link to a peer and resolve its result. */
1147
+ async queryRequest(instanceId, query) {
1148
+ const state = this.linkStates.get(instanceId);
1149
+ if (state === void 0 || !state.writable()) return void 0;
1150
+ const reqId = `q${++this.reqIdCounter}-${randomUUID()}`;
1151
+ const accept = query.kind === "ping" ? projectPingResult : projectListResult;
1152
+ try {
1153
+ return await this.waitForResult(reqId, "query", (failure) => {
1154
+ const wrote = state.sendFrame({ type: "query", reqId, query });
1155
+ if (!wrote) failure(new Error(`interconnect: peer link to ${instanceId} closed while querying`));
1156
+ }, accept);
1157
+ } catch {
1158
+ return void 0;
1159
+ }
1160
+ }
1161
+ /** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
1162
+ waitForResult(reqId, kind, send, accept) {
1163
+ return new Promise((resolve2, reject) => {
1164
+ const timer = setTimeout(() => {
1165
+ this.pendingMessages.delete(reqId);
1166
+ reject(new Error(`interconnect: no result for ${reqId} within ${this.requestTimeoutMs}ms`));
1167
+ }, this.requestTimeoutMs);
1168
+ const settle = (result) => {
1169
+ clearTimeout(timer);
1170
+ this.pendingMessages.delete(reqId);
1171
+ resolve2(result);
1172
+ };
1173
+ this.pendingMessages.set(reqId, kind === "query" ? { timer, kind, accept, resolve: settle } : { timer, kind, resolve: settle });
1174
+ send((error) => {
1175
+ clearTimeout(timer);
1176
+ this.pendingMessages.delete(reqId);
1177
+ reject(error instanceof Error ? error : new Error(String(error)));
1178
+ });
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Add a peer route at runtime. Returns a disposer that removes it. Re-adding
1183
+ * an existing instanceId re-routes it to the new origin.
1184
+ * @param instanceId - the peer's `instanceId`.
1185
+ * @param origin - origin this instance dials to reach that peer.
1186
+ * @returns disposer removing the peer route.
1187
+ */
1188
+ subscribe(instanceId, origin) {
1189
+ const linked = this.link(instanceId, origin);
1190
+ const dialedOrigin = trimBase(origin);
1191
+ return () => {
1192
+ const state = this.linkStates.get(instanceId);
1193
+ if (state === linked && state.peer === dialedOrigin) {
1194
+ state.close();
1195
+ this.linkStates.delete(instanceId);
1196
+ }
1197
+ };
1198
+ }
1199
+ /**
1200
+ * Remove a peer route, closing its outbound link.
1201
+ * @param instanceId - the peer route to remove.
1202
+ */
1203
+ unsubscribe(instanceId) {
1204
+ const state = this.linkStates.get(instanceId);
1205
+ if (state !== void 0) state.close();
1206
+ this.linkStates.delete(instanceId);
1207
+ }
1208
+ /**
1209
+ * Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
1210
+ * events stream over the link in real time, and events the peer pushes are
1211
+ * surfaced as `interconnect/event`. Repeating for the same instanceId
1212
+ * re-routes the link to the new origin.
1213
+ * @param instanceId - the peer's `instanceId`.
1214
+ * @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
1215
+ * @returns a handle closing the link and cancelling reconnection.
1216
+ */
1217
+ link(instanceId, origin) {
1218
+ const existing = this.linkStates.get(instanceId);
1219
+ if (existing !== void 0 && !existing.isClosed()) {
1220
+ existing.reroute(trimBase(origin));
1221
+ return existing;
1222
+ }
1223
+ const state = new LinkState(
1224
+ (socket) => {
1225
+ this.attachSocket(socket);
1226
+ },
1227
+ this.ctx.logger,
1228
+ () => this.resolveToken(),
1229
+ instanceId,
1230
+ trimBase(origin)
1231
+ );
1232
+ this.linkStates.set(instanceId, state);
1233
+ state.dial();
1234
+ return state;
1235
+ }
1236
+ /** Push one serialized lifecycle fact out to every linked peer over WS. */
1237
+ fanout(notification) {
1238
+ this.broadcast(notification);
1239
+ }
1240
+ /** Push one fact over every live WebSocket link, dropping closed sockets. */
1241
+ broadcast(notification) {
1242
+ if (this.sockets.size === 0) return;
1243
+ const frame = { type: "event", notification };
1244
+ const encoded = JSON.stringify(frame);
1245
+ const sent = /* @__PURE__ */ new Set();
1246
+ for (const socket of this.sockets) {
1247
+ if (socket.readyState !== WebSocket.OPEN) {
1248
+ this.sockets.delete(socket);
1249
+ continue;
1250
+ }
1251
+ const peer = this.peerOf.get(socket);
1252
+ if (peer !== void 0 && sent.has(peer)) continue;
1253
+ if (peer !== void 0) sent.add(peer);
1254
+ socket.send(encoded);
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Dispatch one inbound `msg` frame: deliver a `send`/`reply` and answer the
1259
+ * correlated `msg-result` on the same socket. Errors are surfaced as the
1260
+ * result so a throwing handler cannot escape the socket's message callback.
1261
+ */
1262
+ async handleMsgFrame(socket, reqId, message) {
1263
+ const result = await this.deliver({
1264
+ sessionId: message.sessionId,
1265
+ text: message.text,
1266
+ /* v8 ignore next 1 -- schemastery resolves an absent optional `sender` to an empty object, never undefined. */
1267
+ ...message.sender === void 0 ? {} : { sender: message.sender },
1268
+ ...message.delivery === void 0 ? {} : { delivery: message.delivery },
1269
+ ...message.resume === void 0 ? {} : { resume: message.resume }
1270
+ });
1271
+ this.sendFrame(socket, { type: "msg-result", reqId, result });
1272
+ }
1273
+ /** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
1274
+ handleQueryFrame(socket, reqId, query) {
1275
+ let result;
1276
+ if (query.kind === "ping") {
1277
+ result = { pong: true, instance: this.instanceId };
1278
+ } else if (query.kind === "list") {
1279
+ result = this.listSessions();
1280
+ } else {
1281
+ const payload = {
1282
+ sender: this.peerOf.get(socket) ?? "unknown-peer",
1283
+ notification: query.notification
1284
+ };
1285
+ this.receiveEvent(payload);
1286
+ result = { accepted: true };
1287
+ }
1288
+ this.sendFrame(socket, { type: "query-result", reqId, result });
1289
+ }
1290
+ /** Surface one remote notification to local listeners and the log. */
1291
+ receiveEvent(eventPayload) {
1292
+ this.ctx.logger.info(`interconnect: remote event ${eventPayload.notification.kind} from ${eventPayload.sender}`);
1293
+ this.ctx.emit("interconnect/event", eventPayload.notification, eventPayload.sender);
1294
+ }
1295
+ /**
1296
+ * Summarize the live local sessions so a sender can discover valid targets
1297
+ * instead of having to know a session id already. Only live agents are listed
1298
+ * because `send` can reach exactly those, and the answer carries at most
1299
+ * {@link MAX_LISTED_SESSIONS} rows so a large live set cannot push the
1300
+ * `query-result` frame past the link's frame cap.
1301
+ *
1302
+ * Title and status are best-effort: the title projection is an optional
1303
+ * service, and a receiver without it still returns the ids. A projection that
1304
+ * throws degrades that one row rather than failing the whole listing, which
1305
+ * matches how the Host's own session listing treats its projection column.
1306
+ */
1307
+ listSessions() {
1308
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1309
+ const sessions = [];
1310
+ let used = 0;
1311
+ for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
1312
+ const row = this.sessionRow(agent);
1313
+ const sized = used + rowBytes(row);
1314
+ if (sized > MAX_LIST_ROWS_BYTES) {
1315
+ const untitled = {
1316
+ sessionId: row.sessionId,
1317
+ ...row.status === void 0 ? {} : { status: row.status }
1318
+ };
1319
+ const untitledSized = used + rowBytes(untitled);
1320
+ if (untitledSized > MAX_LIST_ROWS_BYTES) break;
1321
+ sessions.push(untitled);
1322
+ used = untitledSized;
1323
+ continue;
1324
+ }
1325
+ sessions.push(row);
1326
+ used = sized;
1327
+ }
1328
+ return { sessions, instance: this.instanceId };
1329
+ }
1330
+ /** One listing row for a live agent; the title projection is best-effort. */
1331
+ sessionRow(agent) {
1332
+ let title;
1333
+ try {
1334
+ const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
1335
+ const value = snapshot?.values.title;
1336
+ if (typeof value === "string" && value !== "") title = value;
1337
+ } catch {
1338
+ title = void 0;
1339
+ }
1340
+ return {
1341
+ sessionId: agent.id,
1342
+ ...title === void 0 ? {} : { title },
1343
+ ...typeof agent.status === "string" ? { status: agent.status } : {}
1344
+ };
1345
+ }
1346
+ /**
1347
+ * Resolve a session that is not currently live, when the sender asked to wake
1348
+ * it. Returns the agent, or the reason it stays undelivered.
1349
+ *
1350
+ * The resume is delegated to the Host's configured `agent` lookup rather than
1351
+ * calling `ctx.agents.resume()` here, and that is the whole point: a handle
1352
+ * from `resume()` is owned by the CALLING context, so resuming on this
1353
+ * plugin's fiber would tear the session down again the moment the plugin
1354
+ * unloads (measured: the same call through the root context leaves it alive).
1355
+ * The Host's resolver owns it instead, and it also composes the preset the
1356
+ * session recorded — so a woken agent comes back with the toolset its history
1357
+ * was produced under, not an empty one.
1358
+ */
1359
+ async wake(payload) {
1360
+ if (payload.resume !== true) return { reason: "session-not-live" };
1361
+ if (!this.allowResume) return { reason: "resume-refused" };
1362
+ const lookup = this.ctx.get("typert")?.lookups.get("agent");
1363
+ if (lookup === void 0) return { reason: "session-not-live" };
1364
+ try {
1365
+ const resolved = await lookup.resolve(payload.sessionId);
1366
+ if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
1367
+ return { agent: resolved };
1368
+ } catch (error) {
1369
+ this.ctx.logger.info(
1370
+ `interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
1371
+ );
1372
+ return { reason: "resume-failed" };
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Deliver one message to a live local session, waking a persisted one only
1377
+ * when the sender asked and this receiver allows it.
1378
+ */
1379
+ async deliver(payload) {
1380
+ if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
1381
+ let agent = this.ctx.agents.get(payload.sessionId);
1382
+ if (agent === void 0) {
1383
+ const woken = await this.wake(payload);
1384
+ if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
1385
+ if ("reason" in woken) {
1386
+ return { delivered: false, instance: this.instanceId, reason: woken.reason };
1387
+ }
1388
+ agent = woken.agent;
1389
+ }
1390
+ if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1391
+ return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1392
+ }
1393
+ const sender = (() => {
1394
+ const candidate = payload.sender;
1395
+ if (typeof candidate !== "object" || candidate === null) return void 0;
1396
+ const record = candidate;
1397
+ if (typeof record.instanceId !== "string" || typeof record.sessionId !== "string") return void 0;
1398
+ return { instanceId: record.instanceId, sessionId: record.sessionId };
1399
+ })();
1400
+ const message = createUserMessage({
1401
+ source: {
1402
+ kind: "plugin",
1403
+ plugin: PLUGIN_SOURCE,
1404
+ form: "notice",
1405
+ summary: boundContextSummary(
1406
+ sender === void 0 ? `interconnect handoff delivered on instance ${this.instanceId}` : `interconnect handoff from ${sender.instanceId} (session ${sender.sessionId}) delivered on instance ${this.instanceId}`
1407
+ )
1408
+ },
1409
+ content: [{ type: "text", text: payload.text }]
1410
+ });
1411
+ const mode = payload.delivery ?? this.delivery;
1412
+ switch (mode) {
1413
+ case "steer":
1414
+ agent.steer(message);
1415
+ break;
1416
+ case "inject":
1417
+ agent.inject(message);
1418
+ break;
1419
+ case "followup":
1420
+ agent.followup(message);
1421
+ break;
1422
+ /* v8 ignore next -- closed-union exhaustiveness guard */
1423
+ default:
1424
+ assertNever(mode);
1425
+ }
1426
+ if (sender !== void 0) {
1427
+ this.senders.set(payload.sessionId, sender);
1428
+ } else {
1429
+ this.senders.delete(payload.sessionId);
1430
+ }
1431
+ return { delivered: true, instance: this.instanceId, delivery: mode };
1432
+ }
1433
+ async resolveToken() {
1434
+ const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
1435
+ return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
1436
+ }
1437
+ /**
1438
+ * Answer one refused upgrade and close its socket. `socket.end()` alone only
1439
+ * half-closes the connection: a client that keeps its own writing side open
1440
+ * (a socket created with `allowHalfOpen`) leaves the connection live, and a
1441
+ * refused socket never enters the link pool, so nothing else would reap it.
1442
+ * @param socket - the refused upgrade socket.
1443
+ * @param response - the complete HTTP response sent to the client.
1444
+ */
1445
+ refuseUpgrade(socket, response) {
1446
+ socket.end(response, () => {
1447
+ socket.destroy();
1448
+ });
1449
+ }
1450
+ /** Inbound WebSocket upgrade: authenticate the bearer header, then accept. */
1451
+ async handleUpgrade(req, socket, head) {
1452
+ let token;
1453
+ try {
1454
+ token = await this.resolveToken();
1455
+ } catch (error) {
1456
+ this.ctx.logger.warn(`interconnect: upgrade token read failed: ${error instanceof Error ? error.message : String(error)}`);
1457
+ this.refuseUpgrade(socket, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 9\r\n\r\nforbidden");
1458
+ return;
1459
+ }
1460
+ if (token === void 0) {
1461
+ this.refuseUpgrade(socket, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 9\r\n\r\nforbidden");
1462
+ return;
1463
+ }
1464
+ if (this.disposed) {
1465
+ this.refuseUpgrade(socket, "HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 11\r\n\r\nunavailable");
1466
+ return;
1467
+ }
1468
+ const expected = `Bearer ${token}`;
1469
+ const header = req.headers.authorization;
1470
+ if (header === void 0 || !timingSafeEqual(header, expected)) {
1471
+ this.refuseUpgrade(socket, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 12\r\n\r\nunauthorized");
1472
+ return;
1473
+ }
1474
+ this.server.handleUpgrade(req, socket, head, (websocket) => {
1475
+ this.attachSocket(websocket);
1476
+ });
1477
+ }
1478
+ /**
1479
+ * Install frame + liveness handling on one socket and add it to the live
1480
+ * pool. Used by both the server half (accepted upgrade) and the client half
1481
+ * (outbound dial), so a single socket carries events both directions. Not a
1482
+ * model-facing entry point; the outbound dial reaches it through a private
1483
+ * closure.
1484
+ * @param websocket - the accepted or dialed socket to install handlers on.
1485
+ */
1486
+ attachSocket(websocket) {
1487
+ this.sockets.add(websocket);
1488
+ websocket.isAlive = true;
1489
+ websocket.on("pong", () => {
1490
+ ;
1491
+ websocket.isAlive = true;
1492
+ });
1493
+ websocket.on("message", (data, isBinary) => {
1494
+ if (isBinary) return;
1495
+ this.handleFrame(websocket, data);
1496
+ });
1497
+ websocket.on("close", () => {
1498
+ this.sockets.delete(websocket);
1499
+ this.peerOf.delete(websocket);
1500
+ });
1501
+ websocket.on("error", () => {
1502
+ this.sockets.delete(websocket);
1503
+ });
1504
+ websocket.send(JSON.stringify({ type: "hello", sender: this.instanceId }));
1505
+ }
1506
+ /** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
1507
+ handleFrame(socket, data) {
1508
+ const byteLength = typeof data === "string" ? Buffer.byteLength(data) : Array.isArray(data) ? data.reduce((total, part) => total + part.byteLength, 0) : data.byteLength;
1509
+ if (byteLength > MAX_LINK_FRAME_BYTES) {
1510
+ this.ctx.logger.warn("interconnect: dropping oversized link frame");
1511
+ return;
1512
+ }
1513
+ const text = typeof data === "string" ? data : Array.isArray(data) ? Buffer.concat(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8");
1514
+ let parsed;
1515
+ try {
1516
+ parsed = withoutNullFields(Schema.resolve(JSON.parse(text), linkFrameSchema, {})[0]);
1517
+ } catch {
1518
+ this.ctx.logger.warn("interconnect: dropping malformed link frame");
1519
+ return;
1520
+ }
1521
+ if (parsed === null || typeof parsed !== "object") {
1522
+ this.ctx.logger.warn("interconnect: dropping malformed link frame");
1523
+ return;
1524
+ }
1525
+ const frame = parsed;
1526
+ if (frame.type === "hello") {
1527
+ this.peerOf.set(socket, frame.sender);
1528
+ return;
1529
+ }
1530
+ if (frame.type === "event") {
1531
+ const sender = this.peerOf.get(socket) ?? "unknown-peer";
1532
+ const kind = frame.notification.kind;
1533
+ try {
1534
+ this.receiveEvent({ sender, notification: frame.notification });
1535
+ } catch (error) {
1536
+ this.ctx.logger.warn(
1537
+ `interconnect: listener for a ${kind} event from ${sender} threw: ` + (error instanceof Error ? error.message : String(error))
1538
+ );
1539
+ }
1540
+ return;
1541
+ }
1542
+ if (frame.type === "msg-result") {
1543
+ const pending = this.pendingMessages.get(frame.reqId);
1544
+ if (pending !== void 0 && pending.kind === "msg") {
1545
+ ;
1546
+ pending.resolve?.(projectMsgResult(frame.result));
1547
+ }
1548
+ return;
1549
+ }
1550
+ if (frame.type === "query-result") {
1551
+ const pending = this.pendingMessages.get(frame.reqId);
1552
+ if (pending !== void 0 && pending.kind === "query") {
1553
+ const answer = pending.accept(frame.result);
1554
+ if (answer !== void 0) pending.resolve?.(answer);
1555
+ }
1556
+ return;
1557
+ }
1558
+ if (frame.type === "msg") {
1559
+ void Promise.resolve().then(async () => {
1560
+ await this.handleMsgFrame(socket, frame.reqId, frame.message);
1561
+ }).catch((error) => {
1562
+ this.ctx.logger.warn(`interconnect: msg ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
1563
+ void this.sendFrame(socket, {
1564
+ type: "msg-result",
1565
+ reqId: frame.reqId,
1566
+ result: { delivered: false, instance: this.instanceId, reason: "unreachable" }
1567
+ });
1568
+ });
1569
+ return;
1570
+ }
1571
+ void Promise.resolve().then(() => {
1572
+ this.handleQueryFrame(socket, frame.reqId, frame.query);
1573
+ }).catch((error) => {
1574
+ this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
1575
+ });
1576
+ }
1577
+ /** Write one frame to a socket, returning false when the socket cannot take it. */
1578
+ sendFrame(socket, frame) {
1579
+ if (socket.readyState !== WebSocket.OPEN) return false;
1580
+ socket.send(JSON.stringify(frame));
1581
+ return true;
1582
+ }
1583
+ };
1584
+ var LinkState = class {
1585
+ constructor(attachSocket, logger, resolveToken, instanceId, origin) {
1586
+ this.attachSocket = attachSocket;
1587
+ this.logger = logger;
1588
+ this.resolveToken = resolveToken;
1589
+ this.instanceId = instanceId;
1590
+ this.peer = origin;
1591
+ }
1592
+ socket;
1593
+ dialEpoch = 0;
1594
+ closed = false;
1595
+ retry = 0;
1596
+ reconnectTimer;
1597
+ warnedNoToken = false;
1598
+ /** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
1599
+ dialListeners;
1600
+ // `peer` is mutable so `reroute` can point the link at a new origin.
1601
+ peer;
1602
+ instanceId;
1603
+ /** Point this link at a different origin; re-dials immediately. */
1604
+ reroute(origin) {
1605
+ if (origin === this.peer) return;
1606
+ this.peer = origin;
1607
+ this.dialEpoch += 1;
1608
+ if (this.reconnectTimer !== void 0) {
1609
+ clearTimeout(this.reconnectTimer);
1610
+ this.reconnectTimer = void 0;
1611
+ }
1612
+ const socket = this.socket;
1613
+ if (socket !== void 0) {
1614
+ this.removeDialListeners(socket);
1615
+ socket.on("error", () => {
1616
+ });
1617
+ socket.terminate();
1618
+ this.socket = void 0;
1619
+ }
1620
+ this.dial();
1621
+ }
1622
+ /** Open the socket; reconnect is scheduled by the close handler. */
1623
+ dial() {
1624
+ if (this.closed) return;
1625
+ const epoch = this.dialEpoch;
1626
+ let url;
1627
+ try {
1628
+ url = linkUrl(this.peer);
1629
+ } catch {
1630
+ this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
1631
+ return;
1632
+ }
1633
+ void this.resolveToken().then((token) => {
1634
+ if (token === void 0) {
1635
+ if (!this.warnedNoToken) {
1636
+ this.warnedNoToken = true;
1637
+ this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down, retrying until one is set`);
1638
+ }
1639
+ if (this.closed || epoch !== this.dialEpoch) return;
1640
+ this.scheduleReconnect();
1641
+ return;
1642
+ }
1643
+ if (this.closed || epoch !== this.dialEpoch) return;
1644
+ const socket = new WebSocket(url, {
1645
+ headers: { authorization: `Bearer ${token}` },
1646
+ maxPayload: MAX_LINK_FRAME_BYTES,
1647
+ // A peer that accepts TCP but never completes the upgrade would leave
1648
+ // the socket in CONNECTING forever: no open (so no heartbeat) and no
1649
+ // close (so no reconnect). ws aborts the connection past the handshake
1650
+ // window, so its own error+close fire and the close handler schedules
1651
+ // a reconnect — and ws owns the timer, so nothing leaks on teardown.
1652
+ handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
1653
+ });
1654
+ this.socket = socket;
1655
+ const onOpen = () => {
1656
+ this.retry = 0;
1657
+ this.attachSocket(socket);
1658
+ };
1659
+ const onClose = () => {
1660
+ if (this.closed) return;
1661
+ this.scheduleReconnect();
1662
+ };
1663
+ const onError = () => {
1664
+ };
1665
+ this.dialListeners = { open: onOpen, close: onClose, error: onError };
1666
+ socket.once("open", onOpen);
1667
+ socket.once("close", onClose);
1668
+ socket.on("error", onError);
1669
+ }).catch(() => {
1670
+ if (this.closed || epoch !== this.dialEpoch) return;
1671
+ this.scheduleReconnect();
1672
+ });
1673
+ }
1674
+ /** Whether this link has been closed; a closed state is never reused by `link()`. */
1675
+ isClosed() {
1676
+ return this.closed;
1677
+ }
1678
+ /** Whether this link currently holds an open socket that can carry frames. */
1679
+ writable() {
1680
+ return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
1681
+ }
1682
+ /** Write one frame over this link's outbound socket; false when not open. */
1683
+ sendFrame(frame) {
1684
+ const socket = this.socket;
1685
+ if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
1686
+ socket.send(JSON.stringify(frame));
1687
+ return true;
1688
+ }
1689
+ close() {
1690
+ this.closed = true;
1691
+ if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
1692
+ const socket = this.socket;
1693
+ if (socket !== void 0) {
1694
+ this.removeDialListeners(socket);
1695
+ socket.on("error", () => {
1696
+ });
1697
+ socket.terminate();
1698
+ }
1699
+ }
1700
+ /** Detach the dial-registered handlers from a socket before terminating it. */
1701
+ removeDialListeners(socket) {
1702
+ const listeners = this.dialListeners;
1703
+ if (listeners === void 0) return;
1704
+ socket.removeListener("open", listeners.open);
1705
+ socket.removeListener("close", listeners.close);
1706
+ socket.removeListener("error", listeners.error);
1707
+ this.dialListeners = void 0;
1708
+ }
1709
+ scheduleReconnect() {
1710
+ const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
1711
+ this.retry += 1;
1712
+ this.reconnectTimer = setTimeout(() => {
1713
+ this.reconnectTimer = void 0;
1714
+ this.dial();
1715
+ }, delay);
1716
+ }
1717
+ };
1718
+ function timingSafeEqual(a, b) {
1719
+ return constantTimeEqual(createHash("sha256").update(a, "utf8").digest(), createHash("sha256").update(b, "utf8").digest());
1720
+ }
1721
+ function linkUrl(origin) {
1722
+ const url = new URL("/interconnect/link", origin);
1723
+ if (url.protocol === "http:") url.protocol = "ws:";
1724
+ else if (url.protocol === "https:") url.protocol = "wss:";
1725
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1726
+ throw new TypeError(`interconnect: unsupported peer origin protocol ${url.protocol}`);
1727
+ }
1728
+ return url;
1729
+ }
1730
+ function trimBase(baseUrl) {
1731
+ return baseUrl.replace(/\/+$/, "");
1732
+ }
1733
+
1734
+ // src/tool-interconnect/index.ts
3
1735
  var name = "tool-interconnect";
4
1736
  var inject = ["interconnect", "tools"];
5
1737
  var deliveryOutcomeSchema = {
@@ -12,7 +1744,7 @@ var deliveryOutcomeSchema = {
12
1744
  reason: { type: "string" }
13
1745
  }
14
1746
  };
15
- var raceSignal = (signal, promise) => new Promise((resolve, reject) => {
1747
+ var raceSignal = (signal, promise) => new Promise((resolve2, reject) => {
16
1748
  const abort = () => {
17
1749
  const reason = signal.reason;
18
1750
  reject(reason instanceof Error ? reason : new Error(typeof reason === "string" ? reason : "aborted"));
@@ -25,7 +1757,7 @@ var raceSignal = (signal, promise) => new Promise((resolve, reject) => {
25
1757
  promise.then(
26
1758
  (value) => {
27
1759
  signal.removeEventListener("abort", abort);
28
- resolve(value);
1760
+ resolve2(value);
29
1761
  },
30
1762
  (error) => {
31
1763
  signal.removeEventListener("abort", abort);
@@ -43,12 +1775,12 @@ function apply(ctx) {
43
1775
  const interconnect = ctx.interconnect;
44
1776
  ctx.tools.register(defineTool({
45
1777
  name: "interconnect_send",
46
- description: 'Deliver one text message to a live session on another DSH instance (same machine, another machine, or another session), over a shared-secret-authenticated channel. Returns whether the peer instance received it and which instance answered. Only a session with a running agent receives directly; a persisted one can be woken by setting resume, and when delivery fails the result reports the reason (for example "session-not-live") and interconnect_list shows which sessions are live there. The sending instance and session ids are attached automatically, so the receiver can reply with interconnect_reply; do not pass a sender parameter.',
1778
+ description: 'Deliver one text message to a live session on another DSH instance (same machine, another machine, or another session). Returns whether the peer instance received it and which instance answered. Only a session with a running agent receives directly; a persisted one can be woken by setting resume, and when delivery fails the result reports the reason (for example "session-not-live") and interconnect_list shows which sessions are live there. The sending instance and session ids are attached automatically, so the receiver can reply with interconnect_reply; do not pass a sender parameter.',
47
1779
  parameters: {
48
1780
  instanceId: {
49
1781
  type: "string",
50
1782
  required: true,
51
- description: "The peer instance id to deliver to, as configured under this instance's interconnect peers map. Deliveries go over the persistent link to that instance."
1783
+ description: "The peer instance id to deliver to, as configured for this instance's interconnect peers."
52
1784
  },
53
1785
  sessionId: {
54
1786
  type: "string",
@@ -115,12 +1847,12 @@ function apply(ctx) {
115
1847
  }));
116
1848
  ctx.tools.register(defineTool({
117
1849
  name: "interconnect_ping",
118
- description: "Probe a peer DSH instance for liveness and identity over the shared-secret channel. Reports reachable with the peer instance id, or unreachable when the link is down or unauthorized.",
1850
+ description: "Probe a peer DSH instance for liveness and identity. Reports reachable with the peer instance id, or unreachable when the peer does not answer.",
119
1851
  parameters: {
120
1852
  instanceId: {
121
1853
  type: "string",
122
1854
  required: true,
123
- description: "The peer instance id to probe, as configured under this instance's interconnect peers map."
1855
+ description: "The peer instance id to probe, as configured for this instance's interconnect peers."
124
1856
  }
125
1857
  },
126
1858
  output: {
@@ -145,12 +1877,12 @@ function apply(ctx) {
145
1877
  }));
146
1878
  ctx.tools.register(defineTool({
147
1879
  name: "interconnect_list",
148
- description: "List the live sessions on a peer DSH instance, so a message can be addressed without knowing a session id in advance. Every returned sessionId is a valid interconnect_send target at the time of the call. Only sessions with a running agent appear; reaching a persisted one that is not listed requires interconnect_send with resume set.",
1880
+ description: "List the live sessions on a peer DSH instance, so a message can be addressed without knowing a session id in advance. Every returned sessionId is a valid interconnect_send target at the time of the call. Only sessions with a running agent appear, and a long listing stops at a size bound, so a live session missing from it may simply not fit; reaching a persisted session requires interconnect_send with resume set.",
149
1881
  parameters: {
150
1882
  instanceId: {
151
1883
  type: "string",
152
1884
  required: true,
153
- description: "The peer instance id to list, as configured under this instance's interconnect peers map."
1885
+ description: "The peer instance id to list, as configured for this instance's interconnect peers."
154
1886
  }
155
1887
  },
156
1888
  output: {
@@ -185,6 +1917,9 @@ function apply(ctx) {
185
1917
  const status = session.status === void 0 ? "" : ` [${session.status}]`;
186
1918
  return `${session.sessionId}${title}${status}`;
187
1919
  });
1920
+ if (sessions.length === MAX_LISTED_SESSIONS) {
1921
+ lines.push(`(${String(MAX_LISTED_SESSIONS)} of possibly more live sessions; a missing target may still be live)`);
1922
+ }
188
1923
  return [{ type: "text", text: lines.join("\n") }];
189
1924
  }
190
1925
  },
@@ -234,7 +1969,7 @@ function apply(ctx) {
234
1969
  }
235
1970
  const text = (() => {
236
1971
  if (value.reason === "no-sender-known") {
237
- return "not delivered: this session never received an interconnect message with a sender identity";
1972
+ return "not delivered: no sender identity is known to reply to";
238
1973
  }
239
1974
  if (value.reason === "session-owned-by-subagent") {
240
1975
  return "not delivered: the recorded sender's session belongs to a subagent and its parent agent owns delivery";