dsh-interconnect 0.11.8 → 0.11.10

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,1741 @@
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
+ /**
1291
+ * Surface one remote notification to local listeners and the log.
1292
+ *
1293
+ * `ctx.parallel` is used instead of `ctx.emit`: `emit` maps listeners without
1294
+ * awaiting them, so a listener that returns a rejected promise would surface
1295
+ * as an unhandled rejection, while `parallel` settles every listener and
1296
+ * aggregates their failures.
1297
+ */
1298
+ receiveEvent(eventPayload) {
1299
+ const { notification, sender } = eventPayload;
1300
+ this.ctx.logger.info(`interconnect: remote event ${notification.kind} from ${sender}`);
1301
+ void this.ctx.parallel("interconnect/event", notification, sender).catch((error) => {
1302
+ const reasons = error instanceof AggregateError ? error.errors : [error];
1303
+ const detail = reasons.map((reason) => reason instanceof Error ? reason.message : String(reason)).join("; ");
1304
+ this.ctx.logger.warn(`interconnect: listener for a ${notification.kind} event from ${sender} threw: ${detail}`);
1305
+ });
1306
+ }
1307
+ /**
1308
+ * Summarize the live local sessions so a sender can discover valid targets
1309
+ * instead of having to know a session id already. Only live agents are listed
1310
+ * because `send` can reach exactly those, and the answer carries at most
1311
+ * {@link MAX_LISTED_SESSIONS} rows so a large live set cannot push the
1312
+ * `query-result` frame past the link's frame cap.
1313
+ *
1314
+ * Title and status are best-effort: the title projection is an optional
1315
+ * service, and a receiver without it still returns the ids. A projection that
1316
+ * throws degrades that one row rather than failing the whole listing, which
1317
+ * matches how the Host's own session listing treats its projection column.
1318
+ */
1319
+ listSessions() {
1320
+ const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(this.ctx, agent.session, agent));
1321
+ const sessions = [];
1322
+ let used = 0;
1323
+ for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
1324
+ const row = this.sessionRow(agent);
1325
+ const sized = used + rowBytes(row);
1326
+ if (sized > MAX_LIST_ROWS_BYTES) {
1327
+ const untitled = {
1328
+ sessionId: row.sessionId,
1329
+ ...row.status === void 0 ? {} : { status: row.status }
1330
+ };
1331
+ const untitledSized = used + rowBytes(untitled);
1332
+ if (untitledSized > MAX_LIST_ROWS_BYTES) break;
1333
+ sessions.push(untitled);
1334
+ used = untitledSized;
1335
+ continue;
1336
+ }
1337
+ sessions.push(row);
1338
+ used = sized;
1339
+ }
1340
+ return { sessions, instance: this.instanceId };
1341
+ }
1342
+ /** One listing row for a live agent; the title projection is best-effort. */
1343
+ sessionRow(agent) {
1344
+ let title;
1345
+ try {
1346
+ const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
1347
+ const value = snapshot?.values.title;
1348
+ if (typeof value === "string" && value !== "") title = value;
1349
+ } catch {
1350
+ title = void 0;
1351
+ }
1352
+ return {
1353
+ sessionId: agent.id,
1354
+ ...title === void 0 ? {} : { title },
1355
+ ...typeof agent.status === "string" ? { status: agent.status } : {}
1356
+ };
1357
+ }
1358
+ /**
1359
+ * Resolve a session that is not currently live, when the sender asked to wake
1360
+ * it. Returns the agent, or the reason it stays undelivered.
1361
+ *
1362
+ * The resume is delegated to the Host's configured `agent` lookup rather than
1363
+ * calling `ctx.agents.resume()` here, and that is the whole point: a handle
1364
+ * from `resume()` is owned by the CALLING context, so resuming on this
1365
+ * plugin's fiber would tear the session down again the moment the plugin
1366
+ * unloads (measured: the same call through the root context leaves it alive).
1367
+ * The Host's resolver owns it instead, and it also composes the preset the
1368
+ * session recorded — so a woken agent comes back with the toolset its history
1369
+ * was produced under, not an empty one.
1370
+ */
1371
+ async wake(payload) {
1372
+ if (payload.resume !== true) return { reason: "session-not-live" };
1373
+ if (!this.allowResume) return { reason: "resume-refused" };
1374
+ const lookup = this.ctx.get("typert")?.lookups.get("agent");
1375
+ if (lookup === void 0) return { reason: "session-not-live" };
1376
+ try {
1377
+ const resolved = await lookup.resolve(payload.sessionId);
1378
+ if (resolved === void 0 || resolved === null) return { reason: "session-not-live" };
1379
+ return { agent: resolved };
1380
+ } catch (error) {
1381
+ this.ctx.logger.info(
1382
+ `interconnect: resume refused for ${payload.sessionId}: ${error instanceof Error ? error.message : String(error)}`
1383
+ );
1384
+ return { reason: "resume-failed" };
1385
+ }
1386
+ }
1387
+ /**
1388
+ * Deliver one message to a live local session, waking a persisted one only
1389
+ * when the sender asked and this receiver allows it.
1390
+ */
1391
+ async deliver(payload) {
1392
+ if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
1393
+ let agent = this.ctx.agents.get(payload.sessionId);
1394
+ if (agent === void 0) {
1395
+ const woken = await this.wake(payload);
1396
+ if (this.disposed) return { delivered: false, instance: this.instanceId, reason: "unreachable" };
1397
+ if ("reason" in woken) {
1398
+ return { delivered: false, instance: this.instanceId, reason: woken.reason };
1399
+ }
1400
+ agent = woken.agent;
1401
+ }
1402
+ if (isSessionOwnedBySubagent(this.ctx, agent.session, agent)) {
1403
+ return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
1404
+ }
1405
+ const sender = (() => {
1406
+ const candidate = payload.sender;
1407
+ if (typeof candidate !== "object" || candidate === null) return void 0;
1408
+ const record = candidate;
1409
+ if (typeof record.instanceId !== "string" || typeof record.sessionId !== "string") return void 0;
1410
+ return { instanceId: record.instanceId, sessionId: record.sessionId };
1411
+ })();
1412
+ const message = createUserMessage({
1413
+ source: {
1414
+ kind: "plugin",
1415
+ plugin: PLUGIN_SOURCE,
1416
+ form: "notice",
1417
+ summary: boundContextSummary(
1418
+ sender === void 0 ? `interconnect handoff delivered on instance ${this.instanceId}` : `interconnect handoff from ${sender.instanceId} (session ${sender.sessionId}) delivered on instance ${this.instanceId}`
1419
+ )
1420
+ },
1421
+ content: [{ type: "text", text: payload.text }]
1422
+ });
1423
+ const mode = payload.delivery ?? this.delivery;
1424
+ switch (mode) {
1425
+ case "steer":
1426
+ agent.steer(message);
1427
+ break;
1428
+ case "inject":
1429
+ agent.inject(message);
1430
+ break;
1431
+ case "followup":
1432
+ agent.followup(message);
1433
+ break;
1434
+ /* v8 ignore next -- closed-union exhaustiveness guard */
1435
+ default:
1436
+ assertNever(mode);
1437
+ }
1438
+ if (sender !== void 0) {
1439
+ this.senders.set(payload.sessionId, sender);
1440
+ } else {
1441
+ this.senders.delete(payload.sessionId);
1442
+ }
1443
+ return { delivered: true, instance: this.instanceId, delivery: mode };
1444
+ }
1445
+ async resolveToken() {
1446
+ const credential = await this.ctx.credentials.resolve(credentialRef(INTERCONNECT_TOKEN_REF));
1447
+ return credential === void 0 || credential.value.length === 0 ? void 0 : credential.value;
1448
+ }
1449
+ /**
1450
+ * Answer one refused upgrade and close its socket. `socket.end()` alone only
1451
+ * half-closes the connection: a client that keeps its own writing side open
1452
+ * (a socket created with `allowHalfOpen`) leaves the connection live, and a
1453
+ * refused socket never enters the link pool, so nothing else would reap it.
1454
+ * @param socket - the refused upgrade socket.
1455
+ * @param response - the complete HTTP response sent to the client.
1456
+ */
1457
+ refuseUpgrade(socket, response) {
1458
+ socket.end(response, () => {
1459
+ socket.destroy();
1460
+ });
1461
+ }
1462
+ /** Inbound WebSocket upgrade: authenticate the bearer header, then accept. */
1463
+ async handleUpgrade(req, socket, head) {
1464
+ let token;
1465
+ try {
1466
+ token = await this.resolveToken();
1467
+ } catch (error) {
1468
+ this.ctx.logger.warn(`interconnect: upgrade token read failed: ${error instanceof Error ? error.message : String(error)}`);
1469
+ this.refuseUpgrade(socket, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 9\r\n\r\nforbidden");
1470
+ return;
1471
+ }
1472
+ if (token === void 0) {
1473
+ this.refuseUpgrade(socket, "HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 9\r\n\r\nforbidden");
1474
+ return;
1475
+ }
1476
+ if (this.disposed) {
1477
+ this.refuseUpgrade(socket, "HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 11\r\n\r\nunavailable");
1478
+ return;
1479
+ }
1480
+ const expected = `Bearer ${token}`;
1481
+ const header = req.headers.authorization;
1482
+ if (header === void 0 || !timingSafeEqual(header, expected)) {
1483
+ this.refuseUpgrade(socket, "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 12\r\n\r\nunauthorized");
1484
+ return;
1485
+ }
1486
+ this.server.handleUpgrade(req, socket, head, (websocket) => {
1487
+ this.attachSocket(websocket);
1488
+ });
1489
+ }
1490
+ /**
1491
+ * Install frame + liveness handling on one socket and add it to the live
1492
+ * pool. Used by both the server half (accepted upgrade) and the client half
1493
+ * (outbound dial), so a single socket carries events both directions. Not a
1494
+ * model-facing entry point; the outbound dial reaches it through a private
1495
+ * closure.
1496
+ * @param websocket - the accepted or dialed socket to install handlers on.
1497
+ */
1498
+ attachSocket(websocket) {
1499
+ this.sockets.add(websocket);
1500
+ websocket.isAlive = true;
1501
+ websocket.on("pong", () => {
1502
+ ;
1503
+ websocket.isAlive = true;
1504
+ });
1505
+ websocket.on("message", (data, isBinary) => {
1506
+ if (isBinary) return;
1507
+ this.handleFrame(websocket, data);
1508
+ });
1509
+ websocket.on("close", () => {
1510
+ this.sockets.delete(websocket);
1511
+ this.peerOf.delete(websocket);
1512
+ });
1513
+ websocket.on("error", () => {
1514
+ this.sockets.delete(websocket);
1515
+ });
1516
+ websocket.send(JSON.stringify({ type: "hello", sender: this.instanceId }));
1517
+ }
1518
+ /** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
1519
+ handleFrame(socket, data) {
1520
+ const byteLength = typeof data === "string" ? Buffer.byteLength(data) : Array.isArray(data) ? data.reduce((total, part) => total + part.byteLength, 0) : data.byteLength;
1521
+ if (byteLength > MAX_LINK_FRAME_BYTES) {
1522
+ this.ctx.logger.warn("interconnect: dropping oversized link frame");
1523
+ return;
1524
+ }
1525
+ 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");
1526
+ let parsed;
1527
+ try {
1528
+ parsed = withoutNullFields(Schema.resolve(JSON.parse(text), linkFrameSchema, {})[0]);
1529
+ } catch {
1530
+ this.ctx.logger.warn("interconnect: dropping malformed link frame");
1531
+ return;
1532
+ }
1533
+ if (parsed === null || typeof parsed !== "object") {
1534
+ this.ctx.logger.warn("interconnect: dropping malformed link frame");
1535
+ return;
1536
+ }
1537
+ const frame = parsed;
1538
+ if (frame.type === "hello") {
1539
+ this.peerOf.set(socket, frame.sender);
1540
+ return;
1541
+ }
1542
+ if (frame.type === "event") {
1543
+ this.receiveEvent({ sender: this.peerOf.get(socket) ?? "unknown-peer", notification: frame.notification });
1544
+ return;
1545
+ }
1546
+ if (frame.type === "msg-result") {
1547
+ const pending = this.pendingMessages.get(frame.reqId);
1548
+ if (pending !== void 0 && pending.kind === "msg") {
1549
+ ;
1550
+ pending.resolve?.(projectMsgResult(frame.result));
1551
+ }
1552
+ return;
1553
+ }
1554
+ if (frame.type === "query-result") {
1555
+ const pending = this.pendingMessages.get(frame.reqId);
1556
+ if (pending !== void 0 && pending.kind === "query") {
1557
+ const answer = pending.accept(frame.result);
1558
+ if (answer !== void 0) pending.resolve?.(answer);
1559
+ }
1560
+ return;
1561
+ }
1562
+ if (frame.type === "msg") {
1563
+ void Promise.resolve().then(async () => {
1564
+ await this.handleMsgFrame(socket, frame.reqId, frame.message);
1565
+ }).catch((error) => {
1566
+ this.ctx.logger.warn(`interconnect: msg ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
1567
+ void this.sendFrame(socket, {
1568
+ type: "msg-result",
1569
+ reqId: frame.reqId,
1570
+ result: { delivered: false, instance: this.instanceId, reason: "unreachable" }
1571
+ });
1572
+ });
1573
+ return;
1574
+ }
1575
+ void Promise.resolve().then(() => {
1576
+ this.handleQueryFrame(socket, frame.reqId, frame.query);
1577
+ }).catch((error) => {
1578
+ this.ctx.logger.warn(`interconnect: query ${frame.reqId} handler threw: ${error instanceof Error ? error.message : String(error)}`);
1579
+ });
1580
+ }
1581
+ /** Write one frame to a socket, returning false when the socket cannot take it. */
1582
+ sendFrame(socket, frame) {
1583
+ if (socket.readyState !== WebSocket.OPEN) return false;
1584
+ socket.send(JSON.stringify(frame));
1585
+ return true;
1586
+ }
1587
+ };
1588
+ var LinkState = class {
1589
+ constructor(attachSocket, logger, resolveToken, instanceId, origin) {
1590
+ this.attachSocket = attachSocket;
1591
+ this.logger = logger;
1592
+ this.resolveToken = resolveToken;
1593
+ this.instanceId = instanceId;
1594
+ this.peer = origin;
1595
+ }
1596
+ socket;
1597
+ dialEpoch = 0;
1598
+ closed = false;
1599
+ retry = 0;
1600
+ reconnectTimer;
1601
+ warnedNoToken = false;
1602
+ /** Dial-registered handlers, removed by reference on close/reroute so the pool-cleanup handlers attachSocket added stay alive. */
1603
+ dialListeners;
1604
+ // `peer` is mutable so `reroute` can point the link at a new origin.
1605
+ peer;
1606
+ instanceId;
1607
+ /** Point this link at a different origin; re-dials immediately. */
1608
+ reroute(origin) {
1609
+ if (origin === this.peer) return;
1610
+ this.peer = origin;
1611
+ this.dialEpoch += 1;
1612
+ if (this.reconnectTimer !== void 0) {
1613
+ clearTimeout(this.reconnectTimer);
1614
+ this.reconnectTimer = void 0;
1615
+ }
1616
+ const socket = this.socket;
1617
+ if (socket !== void 0) {
1618
+ this.removeDialListeners(socket);
1619
+ socket.on("error", () => {
1620
+ });
1621
+ socket.terminate();
1622
+ this.socket = void 0;
1623
+ }
1624
+ this.dial();
1625
+ }
1626
+ /** Open the socket; reconnect is scheduled by the close handler. */
1627
+ dial() {
1628
+ if (this.closed) return;
1629
+ const epoch = this.dialEpoch;
1630
+ let url;
1631
+ try {
1632
+ url = linkUrl(this.peer);
1633
+ } catch {
1634
+ this.logger.warn(`interconnect: invalid peer origin ${this.peer}; link to ${this.instanceId} stays down`);
1635
+ return;
1636
+ }
1637
+ void this.resolveToken().then((token) => {
1638
+ if (token === void 0) {
1639
+ if (!this.warnedNoToken) {
1640
+ this.warnedNoToken = true;
1641
+ this.logger.warn(`interconnect: no shared token configured; peer ${this.instanceId} link stays down, retrying until one is set`);
1642
+ }
1643
+ if (this.closed || epoch !== this.dialEpoch) return;
1644
+ this.scheduleReconnect();
1645
+ return;
1646
+ }
1647
+ if (this.closed || epoch !== this.dialEpoch) return;
1648
+ const socket = new WebSocket(url, {
1649
+ headers: { authorization: `Bearer ${token}` },
1650
+ maxPayload: MAX_LINK_FRAME_BYTES,
1651
+ // A peer that accepts TCP but never completes the upgrade would leave
1652
+ // the socket in CONNECTING forever: no open (so no heartbeat) and no
1653
+ // close (so no reconnect). ws aborts the connection past the handshake
1654
+ // window, so its own error+close fire and the close handler schedules
1655
+ // a reconnect — and ws owns the timer, so nothing leaks on teardown.
1656
+ handshakeTimeout: LINK_HANDSHAKE_TIMEOUT_MS
1657
+ });
1658
+ this.socket = socket;
1659
+ const onOpen = () => {
1660
+ this.retry = 0;
1661
+ this.attachSocket(socket);
1662
+ };
1663
+ const onClose = () => {
1664
+ if (this.closed) return;
1665
+ this.scheduleReconnect();
1666
+ };
1667
+ const onError = () => {
1668
+ };
1669
+ this.dialListeners = { open: onOpen, close: onClose, error: onError };
1670
+ socket.once("open", onOpen);
1671
+ socket.once("close", onClose);
1672
+ socket.on("error", onError);
1673
+ }).catch(() => {
1674
+ if (this.closed || epoch !== this.dialEpoch) return;
1675
+ this.scheduleReconnect();
1676
+ });
1677
+ }
1678
+ /** Whether this link has been closed; a closed state is never reused by `link()`. */
1679
+ isClosed() {
1680
+ return this.closed;
1681
+ }
1682
+ /** Whether this link currently holds an open socket that can carry frames. */
1683
+ writable() {
1684
+ return this.socket !== void 0 && this.socket.readyState === WebSocket.OPEN;
1685
+ }
1686
+ /** Write one frame over this link's outbound socket; false when not open. */
1687
+ sendFrame(frame) {
1688
+ const socket = this.socket;
1689
+ if (socket === void 0 || socket.readyState !== WebSocket.OPEN) return false;
1690
+ socket.send(JSON.stringify(frame));
1691
+ return true;
1692
+ }
1693
+ close() {
1694
+ this.closed = true;
1695
+ if (this.reconnectTimer !== void 0) clearTimeout(this.reconnectTimer);
1696
+ const socket = this.socket;
1697
+ if (socket !== void 0) {
1698
+ this.removeDialListeners(socket);
1699
+ socket.on("error", () => {
1700
+ });
1701
+ socket.terminate();
1702
+ }
1703
+ }
1704
+ /** Detach the dial-registered handlers from a socket before terminating it. */
1705
+ removeDialListeners(socket) {
1706
+ const listeners = this.dialListeners;
1707
+ if (listeners === void 0) return;
1708
+ socket.removeListener("open", listeners.open);
1709
+ socket.removeListener("close", listeners.close);
1710
+ socket.removeListener("error", listeners.error);
1711
+ this.dialListeners = void 0;
1712
+ }
1713
+ scheduleReconnect() {
1714
+ const delay = Math.min(3e4, 1e3 * 2 ** this.retry);
1715
+ this.retry += 1;
1716
+ this.reconnectTimer = setTimeout(() => {
1717
+ this.reconnectTimer = void 0;
1718
+ this.dial();
1719
+ }, delay);
1720
+ }
1721
+ };
1722
+ function timingSafeEqual(a, b) {
1723
+ return constantTimeEqual(createHash("sha256").update(a, "utf8").digest(), createHash("sha256").update(b, "utf8").digest());
1724
+ }
1725
+ function linkUrl(origin) {
1726
+ const url = new URL("/interconnect/link", origin);
1727
+ if (url.protocol === "http:") url.protocol = "ws:";
1728
+ else if (url.protocol === "https:") url.protocol = "wss:";
1729
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
1730
+ throw new TypeError(`interconnect: unsupported peer origin protocol ${url.protocol}`);
1731
+ }
1732
+ return url;
1733
+ }
1734
+ function trimBase(baseUrl) {
1735
+ return baseUrl.replace(/\/+$/, "");
1736
+ }
1737
+
1738
+ // src/tool-interconnect/index.ts
3
1739
  var name = "tool-interconnect";
4
1740
  var inject = ["interconnect", "tools"];
5
1741
  var deliveryOutcomeSchema = {
@@ -12,7 +1748,7 @@ var deliveryOutcomeSchema = {
12
1748
  reason: { type: "string" }
13
1749
  }
14
1750
  };
15
- var raceSignal = (signal, promise) => new Promise((resolve, reject) => {
1751
+ var raceSignal = (signal, promise) => new Promise((resolve2, reject) => {
16
1752
  const abort = () => {
17
1753
  const reason = signal.reason;
18
1754
  reject(reason instanceof Error ? reason : new Error(typeof reason === "string" ? reason : "aborted"));
@@ -25,7 +1761,7 @@ var raceSignal = (signal, promise) => new Promise((resolve, reject) => {
25
1761
  promise.then(
26
1762
  (value) => {
27
1763
  signal.removeEventListener("abort", abort);
28
- resolve(value);
1764
+ resolve2(value);
29
1765
  },
30
1766
  (error) => {
31
1767
  signal.removeEventListener("abort", abort);
@@ -82,7 +1818,7 @@ function apply(ctx) {
82
1818
  const text = (() => {
83
1819
  switch (value.reason) {
84
1820
  case "unreachable":
85
- return `not delivered: ${value.instance} did not answer (unreachable or unauthorized)`;
1821
+ return `not delivered: ${value.instance} did not answer (unreachable)`;
86
1822
  case "resume-refused":
87
1823
  return `not delivered: ${value.instance} does not allow waking persisted sessions`;
88
1824
  case "resume-failed":
@@ -115,12 +1851,12 @@ function apply(ctx) {
115
1851
  }));
116
1852
  ctx.tools.register(defineTool({
117
1853
  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.",
1854
+ 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
1855
  parameters: {
120
1856
  instanceId: {
121
1857
  type: "string",
122
1858
  required: true,
123
- description: "The peer instance id to probe, as configured under this instance's interconnect peers map."
1859
+ description: "The peer instance id to probe, as configured for this instance's interconnect peers."
124
1860
  }
125
1861
  },
126
1862
  output: {
@@ -134,7 +1870,7 @@ function apply(ctx) {
134
1870
  },
135
1871
  render: (_args, value) => [{
136
1872
  type: "text",
137
- text: value.reachable ? `reachable: ${value.instance ?? "(unknown instance)"}` : "unreachable or unauthorized"
1873
+ text: value.reachable ? `reachable: ${value.instance ?? "(unknown instance)"}` : "unreachable"
138
1874
  }]
139
1875
  },
140
1876
  async execute(args, exec) {
@@ -145,12 +1881,12 @@ function apply(ctx) {
145
1881
  }));
146
1882
  ctx.tools.register(defineTool({
147
1883
  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, 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.",
1884
+ 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 live sessions 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
1885
  parameters: {
150
1886
  instanceId: {
151
1887
  type: "string",
152
1888
  required: true,
153
- description: "The peer instance id to list, as configured under this instance's interconnect peers map."
1889
+ description: "The peer instance id to list, as configured for this instance's interconnect peers."
154
1890
  }
155
1891
  },
156
1892
  output: {
@@ -175,7 +1911,7 @@ function apply(ctx) {
175
1911
  }
176
1912
  },
177
1913
  render: (_args, value) => {
178
- if (!value.reachable) return [{ type: "text", text: "unreachable or unauthorized" }];
1914
+ if (!value.reachable) return [{ type: "text", text: "unreachable" }];
179
1915
  const sessions = value.sessions ?? [];
180
1916
  if (sessions.length === 0) {
181
1917
  return [{ type: "text", text: `no live sessions on ${value.instance ?? "(unknown instance)"}` }];
@@ -185,6 +1921,9 @@ function apply(ctx) {
185
1921
  const status = session.status === void 0 ? "" : ` [${session.status}]`;
186
1922
  return `${session.sessionId}${title}${status}`;
187
1923
  });
1924
+ if (sessions.length === MAX_LISTED_SESSIONS) {
1925
+ lines.push(`(${String(MAX_LISTED_SESSIONS)} of possibly more live sessions; a missing target may still be live)`);
1926
+ }
188
1927
  return [{ type: "text", text: lines.join("\n") }];
189
1928
  }
190
1929
  },
@@ -240,7 +1979,7 @@ function apply(ctx) {
240
1979
  return "not delivered: the recorded sender's session belongs to a subagent and its parent agent owns delivery";
241
1980
  }
242
1981
  if (value.reason === "unreachable") {
243
- return "not delivered: the recorded sender did not answer (unreachable or unauthorized)";
1982
+ return "not delivered: the recorded sender did not answer (unreachable)";
244
1983
  }
245
1984
  if (value.reason === "resume-refused") {
246
1985
  return "not delivered: the recorded sender does not allow waking persisted sessions";