@xmanrui/dsh-im 3.0.7 → 3.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/lib/client.js +1 -1
- package/lib/index.js +214 -214
- package/package.json +1 -1
- package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +3 -11
- package/plugin-src/host/channels/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -13
- package/plugin-src/host/channels/office/production.mjs +3 -3
- package/plugin-src/host/channels/qq/index.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +3 -11
- package/plugin-src/host/channels/shared/production.mjs +3 -11
- package/plugin-src/host/channels/slack/index.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +4 -3
- package/plugin-src/host/channels/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +3 -11
- package/plugin-src/host/channels/weixin/index.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +3 -11
- package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +3 -11
- package/plugin-src/host/harness-connection.mjs +15 -0
- package/plugin-src/host/index.mjs +1 -1
- package/src/channels/shared/harness-client.mjs +239 -281
- package/src/channels/shared/harness-mux.mjs +123 -0
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
} from './inbound-file.mjs';
|
|
10
10
|
import { outboundArtifactRegistry } from './semantic/artifact.mjs';
|
|
11
11
|
import { t } from './i18n.mjs';
|
|
12
|
+
import { watchHarnessMux } from './harness-mux.mjs';
|
|
12
13
|
|
|
13
14
|
// Every channel plugin runs in the same Host process. Sharing ownership by
|
|
14
|
-
//
|
|
15
|
+
// Host identity (or an explicitly configured HTTP origin) prevents clients
|
|
16
|
+
// bound to one Session
|
|
15
17
|
// from claiming or cancelling each other's interactions.
|
|
16
18
|
const interactionRegistries = new Map();
|
|
19
|
+
const hostInteractionRegistries = new WeakMap();
|
|
17
20
|
const MAX_ERROR_CLASSIFICATION_BYTES = 64;
|
|
18
21
|
|
|
19
22
|
async function smallResponseText(response) {
|
|
@@ -74,8 +77,9 @@ async function harnessHttpErrorCode(response, hostname) {
|
|
|
74
77
|
return 'harness-http-failed';
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
function interactionRegistry(
|
|
78
|
-
|
|
80
|
+
function interactionRegistry(scope) {
|
|
81
|
+
const registries = typeof scope === 'string' ? interactionRegistries : hostInteractionRegistries;
|
|
82
|
+
let registry = registries.get(scope);
|
|
79
83
|
if (!registry) {
|
|
80
84
|
registry = {
|
|
81
85
|
ownerships: new Map(),
|
|
@@ -83,11 +87,38 @@ function interactionRegistry(origin) {
|
|
|
83
87
|
controls: new WeakMap(),
|
|
84
88
|
nextOrder: 0,
|
|
85
89
|
};
|
|
86
|
-
|
|
90
|
+
registries.set(scope, registry);
|
|
87
91
|
}
|
|
88
92
|
return registry;
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
// A Host RPC may not accept cancellation itself. Bound the caller's wait
|
|
96
|
+
// without retrying an operation that the Host may already have accepted.
|
|
97
|
+
function callWithSignal(call, signal) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const cleanup = () => signal.removeEventListener('abort', handleAbort);
|
|
100
|
+
const handleAbort = () => {
|
|
101
|
+
cleanup();
|
|
102
|
+
reject(signal.reason);
|
|
103
|
+
};
|
|
104
|
+
if (signal.aborted) {
|
|
105
|
+
handleAbort();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
signal.addEventListener('abort', handleAbort, { once: true });
|
|
109
|
+
Promise.resolve().then(() => {
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
return call();
|
|
112
|
+
}).then((value) => {
|
|
113
|
+
cleanup();
|
|
114
|
+
resolve(value);
|
|
115
|
+
}, (error) => {
|
|
116
|
+
cleanup();
|
|
117
|
+
reject(error);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
91
122
|
function normalizeControl(control) {
|
|
92
123
|
const ownerType = typeof control?.owner;
|
|
93
124
|
if ((ownerType !== 'object' && ownerType !== 'function')
|
|
@@ -569,6 +600,7 @@ function harnessTurnSucceeded(reason) {
|
|
|
569
600
|
|
|
570
601
|
export class HarnessClient {
|
|
571
602
|
#baseUrl;
|
|
603
|
+
#apiProxy;
|
|
572
604
|
#workspace;
|
|
573
605
|
#agentPreset;
|
|
574
606
|
#autostart;
|
|
@@ -590,6 +622,8 @@ export class HarnessClient {
|
|
|
590
622
|
|
|
591
623
|
constructor({
|
|
592
624
|
baseUrl,
|
|
625
|
+
apiProxy,
|
|
626
|
+
interactionScope = apiProxy,
|
|
593
627
|
workspace,
|
|
594
628
|
agentPreset,
|
|
595
629
|
autostart = false,
|
|
@@ -629,11 +663,19 @@ export class HarnessClient {
|
|
|
629
663
|
if (fileIngressExecutor !== undefined && typeof fileIngressExecutor !== 'function') {
|
|
630
664
|
throw new TypeError('fileIngressExecutor must be a function');
|
|
631
665
|
}
|
|
632
|
-
this.#baseUrl = new URL(baseUrl);
|
|
666
|
+
this.#baseUrl = baseUrl === undefined ? null : new URL(baseUrl);
|
|
667
|
+
this.#apiProxy = this.#baseUrl ? null : apiProxy;
|
|
668
|
+
if (!this.#baseUrl && (!this.#apiProxy || typeof this.#apiProxy !== 'object')) {
|
|
669
|
+
throw new TypeError('HarnessClient requires the current Host apiProxy or an explicit baseUrl');
|
|
670
|
+
}
|
|
671
|
+
if (this.#apiProxy && (!interactionScope
|
|
672
|
+
|| !['object', 'function'].includes(typeof interactionScope))) {
|
|
673
|
+
throw new TypeError('interactionScope must identify the current Host');
|
|
674
|
+
}
|
|
633
675
|
this.#workspace = workspace;
|
|
634
676
|
// Keep an omitted preset absent so session.create resolves the Host's current default.
|
|
635
677
|
this.#agentPreset = agentPreset ?? undefined;
|
|
636
|
-
this.#autostart = autostart;
|
|
678
|
+
this.#autostart = Boolean(this.#baseUrl && autostart);
|
|
637
679
|
this.#dshBin = dshBin;
|
|
638
680
|
this.#fetch = fetchImpl;
|
|
639
681
|
this.#createWebSocket = createWebSocket;
|
|
@@ -644,7 +686,7 @@ export class HarnessClient {
|
|
|
644
686
|
this.#controlExecutor = controlExecutor;
|
|
645
687
|
this.#sessionMaintenanceExecutor = sessionMaintenanceExecutor;
|
|
646
688
|
this.#fileIngressExecutor = fileIngressExecutor;
|
|
647
|
-
this.#interactionRegistry = interactionRegistry(this.#baseUrl
|
|
689
|
+
this.#interactionRegistry = interactionRegistry(this.#baseUrl?.origin ?? interactionScope);
|
|
648
690
|
this.#interactionOwnerships = this.#interactionRegistry.ownerships;
|
|
649
691
|
this.#interactionClaims = this.#interactionRegistry.claims;
|
|
650
692
|
this.#controlOwnerships = this.#interactionRegistry.controls;
|
|
@@ -656,34 +698,46 @@ export class HarnessClient {
|
|
|
656
698
|
const signal = options.signal
|
|
657
699
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
658
700
|
: timeoutSignal;
|
|
659
|
-
let
|
|
701
|
+
let body;
|
|
660
702
|
try {
|
|
661
|
-
|
|
662
|
-
method
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
703
|
+
if (this.#apiProxy) {
|
|
704
|
+
const [domain, action, extra] = method.split('.');
|
|
705
|
+
const namespace = { host: 'host', workspace: 'workspace', session: 'sessions', llm: 'llm' }[domain];
|
|
706
|
+
const api = namespace && this.#apiProxy[namespace];
|
|
707
|
+
if (extra !== undefined || !Object.hasOwn(api ?? {}, action)
|
|
708
|
+
|| typeof api[action] !== 'function') {
|
|
709
|
+
throw new HarnessTransportError('harness-api-not-found', method);
|
|
710
|
+
}
|
|
711
|
+
const response = await callWithSignal(() => api[action]({ rpcId, payload }, signal), signal);
|
|
712
|
+
body = { type: 'server-response', ...response };
|
|
713
|
+
} else {
|
|
714
|
+
const response = await this.#fetch(new URL(`/api/${method}`, this.#baseUrl), {
|
|
715
|
+
method: 'POST',
|
|
716
|
+
headers: { 'content-type': 'application/json' },
|
|
717
|
+
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
|
718
|
+
signal,
|
|
719
|
+
});
|
|
720
|
+
if (!response.ok) {
|
|
721
|
+
const code = await harnessHttpErrorCode(response, this.#baseUrl.hostname);
|
|
722
|
+
throw new HarnessTransportError(code, method, { status: response.status });
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
body = await response.json();
|
|
726
|
+
} catch (error) {
|
|
727
|
+
throw new HarnessTransportError('harness-response-invalid', method, { cause: error });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
667
730
|
} catch (error) {
|
|
668
731
|
// Preserve an explicit caller cancellation; it is control flow, not a
|
|
669
732
|
// Harness availability diagnosis.
|
|
670
733
|
if (options.signal?.aborted) throw error;
|
|
734
|
+
if (error instanceof HarnessTransportError) throw error;
|
|
671
735
|
throw new HarnessTransportError(
|
|
672
736
|
timeoutSignal.aborted ? 'harness-timeout' : 'harness-connect-failed',
|
|
673
737
|
method,
|
|
674
738
|
{ cause: error },
|
|
675
739
|
);
|
|
676
740
|
}
|
|
677
|
-
if (!response.ok) {
|
|
678
|
-
const code = await harnessHttpErrorCode(response, this.#baseUrl.hostname);
|
|
679
|
-
throw new HarnessTransportError(code, method, { status: response.status });
|
|
680
|
-
}
|
|
681
|
-
let body;
|
|
682
|
-
try {
|
|
683
|
-
body = await response.json();
|
|
684
|
-
} catch (error) {
|
|
685
|
-
throw new HarnessTransportError('harness-response-invalid', method, { cause: error });
|
|
686
|
-
}
|
|
687
741
|
if (body?.type !== 'server-response' || body?.rpcId !== rpcId) {
|
|
688
742
|
throw new HarnessTransportError('harness-response-invalid', method, {
|
|
689
743
|
cause: new Error(`Harness returned an invalid response for ${method}`),
|
|
@@ -876,16 +930,22 @@ export class HarnessClient {
|
|
|
876
930
|
const signal = options.signal
|
|
877
931
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
878
932
|
: timeoutSignal;
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
933
|
+
const envelope = { type: 'client-response', rpcId, result };
|
|
934
|
+
let receipt;
|
|
935
|
+
if (this.#apiProxy) {
|
|
936
|
+
receipt = await callWithSignal(() => this.#apiProxy.respond(envelope), signal);
|
|
937
|
+
} else {
|
|
938
|
+
const response = await this.#fetch(new URL('/api/respond', this.#baseUrl), {
|
|
939
|
+
method: 'POST',
|
|
940
|
+
headers: { 'content-type': 'application/json' },
|
|
941
|
+
body: JSON.stringify(envelope),
|
|
942
|
+
signal,
|
|
943
|
+
});
|
|
944
|
+
if (!response.ok) {
|
|
945
|
+
throw new Error(`Harness transport respond failed: HTTP ${response.status}`);
|
|
946
|
+
}
|
|
947
|
+
receipt = await response.json();
|
|
887
948
|
}
|
|
888
|
-
const receipt = await response.json();
|
|
889
949
|
if (receipt?.accepted === true) return receipt;
|
|
890
950
|
if (receipt?.accepted !== false
|
|
891
951
|
|| (receipt.reason !== 'bad-response' && receipt.reason !== 'not-pending')) {
|
|
@@ -921,7 +981,7 @@ export class HarnessClient {
|
|
|
921
981
|
|
|
922
982
|
while (!signal.aborted) {
|
|
923
983
|
try {
|
|
924
|
-
await this.#
|
|
984
|
+
await this.#watchInteractionStream(sessionId, {
|
|
925
985
|
signal,
|
|
926
986
|
onInteraction,
|
|
927
987
|
onResolved,
|
|
@@ -1360,191 +1420,135 @@ export class HarnessClient {
|
|
|
1360
1420
|
}
|
|
1361
1421
|
}
|
|
1362
1422
|
|
|
1363
|
-
#
|
|
1423
|
+
async #watchInteractionStream(sessionId, {
|
|
1364
1424
|
signal,
|
|
1365
1425
|
onInteraction,
|
|
1366
1426
|
onResolved,
|
|
1367
1427
|
onOpen,
|
|
1368
1428
|
ownership,
|
|
1369
1429
|
}) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1430
|
+
let settled = false;
|
|
1431
|
+
let callbackFailure = null;
|
|
1432
|
+
let callbackTail = Promise.resolve();
|
|
1433
|
+
let ownershipReady = ownership === undefined || ownership === null;
|
|
1434
|
+
const bufferedEnvelopes = [];
|
|
1435
|
+
let close = () => {};
|
|
1436
|
+
const handleOpen = (closeStream) => {
|
|
1437
|
+
close = closeStream;
|
|
1438
|
+
if (ownership) ownership.reconnect = close;
|
|
1375
1439
|
try {
|
|
1376
|
-
|
|
1440
|
+
onOpen?.();
|
|
1377
1441
|
} catch (error) {
|
|
1378
|
-
|
|
1442
|
+
console.warn(`[${this.#logPrefix}] ignored an interaction open callback failure:`, error.message);
|
|
1443
|
+
}
|
|
1444
|
+
if (ownership) {
|
|
1445
|
+
void this.#refreshInteractionOwnerships(sessionId, signal).then(() => {
|
|
1446
|
+
if (settled) return;
|
|
1447
|
+
ownershipReady = true;
|
|
1448
|
+
for (const envelope of bufferedEnvelopes.splice(0)) processEnvelope(envelope);
|
|
1449
|
+
}).catch((error) => {
|
|
1450
|
+
callbackFailure ??= error;
|
|
1451
|
+
close();
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const dispatch = (callback, value) => {
|
|
1456
|
+
if (!callback) return;
|
|
1457
|
+
callbackTail = callbackTail
|
|
1458
|
+
.then(() => callback(value))
|
|
1459
|
+
.catch((error) => {
|
|
1460
|
+
callbackFailure ??= error;
|
|
1461
|
+
close();
|
|
1462
|
+
});
|
|
1463
|
+
};
|
|
1464
|
+
const processEnvelope = (envelope) => {
|
|
1465
|
+
const payload = envelope.payload;
|
|
1466
|
+
if (ownership && payload.type === 'session/event') {
|
|
1467
|
+
this.#consumeInteractionOwnerships(sessionId, [payload.event]);
|
|
1379
1468
|
return;
|
|
1380
1469
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
let ownershipReady = ownership === undefined || ownership === null;
|
|
1386
|
-
const bufferedEnvelopes = [];
|
|
1387
|
-
const finish = (error) => {
|
|
1388
|
-
if (settled) return;
|
|
1389
|
-
settled = true;
|
|
1390
|
-
socket.removeEventListener('open', handleOpen);
|
|
1391
|
-
socket.removeEventListener('message', handleMessage);
|
|
1392
|
-
socket.removeEventListener('close', handleClose);
|
|
1393
|
-
socket.removeEventListener('error', handleError);
|
|
1394
|
-
signal.removeEventListener('abort', handleAbort);
|
|
1395
|
-
if (ownership?.reconnect === close) ownership.reconnect = null;
|
|
1396
|
-
if (signal.aborted) {
|
|
1397
|
-
resolve();
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
void callbackTail.then(() => {
|
|
1401
|
-
const failure = error ?? callbackFailure;
|
|
1402
|
-
if (failure) reject(failure);
|
|
1403
|
-
else resolve();
|
|
1404
|
-
}, reject);
|
|
1405
|
-
};
|
|
1406
|
-
const close = () => {
|
|
1407
|
-
try {
|
|
1408
|
-
if (socket.readyState === 0 || socket.readyState === 1) socket.close();
|
|
1409
|
-
} catch {
|
|
1410
|
-
// Cleanup must still settle the watcher if a WebSocket rejects close while connecting.
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
const handleOpen = () => {
|
|
1414
|
-
opened = true;
|
|
1415
|
-
if (ownership) ownership.reconnect = close;
|
|
1416
|
-
try {
|
|
1417
|
-
onOpen?.();
|
|
1418
|
-
} catch (error) {
|
|
1419
|
-
console.warn(`[${this.#logPrefix}] ignored an interaction open callback failure:`, error.message);
|
|
1420
|
-
}
|
|
1470
|
+
if (payload.type === 'question/requested' || payload.type === 'approval/requested') {
|
|
1471
|
+
const kind = payload.type === 'question/requested' ? 'question' : 'approval';
|
|
1472
|
+
const interactionId = kind === 'question' ? envelope.rpcId : payload.approvalId;
|
|
1473
|
+
const claimKey = `${kind}:${interactionId}`;
|
|
1421
1474
|
if (ownership) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
for (const envelope of bufferedEnvelopes.splice(0)) processEnvelope(envelope);
|
|
1426
|
-
}).catch((error) => {
|
|
1427
|
-
callbackFailure ??= error;
|
|
1428
|
-
close();
|
|
1429
|
-
finish(error);
|
|
1430
|
-
});
|
|
1475
|
+
const claim = this.#interactionOwner(sessionId, claimKey, kind);
|
|
1476
|
+
if (claim?.ownership !== ownership) return;
|
|
1477
|
+
this.#interactionClaims.set(claimKey, claim);
|
|
1431
1478
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
rpcId: envelope.rpcId,
|
|
1465
|
-
sessionId,
|
|
1466
|
-
payload,
|
|
1467
|
-
recovered: ownership
|
|
1468
|
-
? this.#interactionClaims.get(claimKey)?.recovered === true
|
|
1469
|
-
: false,
|
|
1470
|
-
...(toolCall ? { toolCall } : {}),
|
|
1471
|
-
reconnect: close,
|
|
1472
|
-
respond: (result, options = {}) => this.respondInteraction(
|
|
1473
|
-
envelope.rpcId,
|
|
1474
|
-
result,
|
|
1475
|
-
{ ...options, signal: options.signal ?? signal },
|
|
1476
|
-
),
|
|
1477
|
-
}));
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
if (payload.type === 'question/resolved' || payload.type === 'approval/resolved') {
|
|
1481
|
-
const kind = payload.type === 'question/resolved' ? 'question' : 'approval';
|
|
1482
|
-
const interactionId = kind === 'question'
|
|
1483
|
-
? payload.questionRpcId
|
|
1484
|
-
: payload.approvalId;
|
|
1485
|
-
const claimKey = `${kind}:${interactionId}`;
|
|
1486
|
-
if (ownership) {
|
|
1487
|
-
const claim = this.#interactionClaims.get(claimKey);
|
|
1488
|
-
if (claim?.ownership !== ownership) return;
|
|
1489
|
-
this.#interactionClaims.delete(claimKey);
|
|
1490
|
-
}
|
|
1491
|
-
dispatch(onResolved, Object.freeze({
|
|
1492
|
-
kind,
|
|
1493
|
-
interactionId,
|
|
1494
|
-
sessionId,
|
|
1495
|
-
outcome: payload.outcome,
|
|
1496
|
-
payload,
|
|
1497
|
-
}));
|
|
1479
|
+
const toolCall = kind === 'approval' && ownership && typeof payload.callId === 'string'
|
|
1480
|
+
? this.#interactionClaims.get(claimKey)?.ownership.toolCalls.get(payload.callId)
|
|
1481
|
+
: undefined;
|
|
1482
|
+
dispatch(onInteraction, Object.freeze({
|
|
1483
|
+
kind,
|
|
1484
|
+
interactionId,
|
|
1485
|
+
rpcId: envelope.rpcId,
|
|
1486
|
+
sessionId,
|
|
1487
|
+
payload,
|
|
1488
|
+
recovered: ownership
|
|
1489
|
+
? this.#interactionClaims.get(claimKey)?.recovered === true
|
|
1490
|
+
: false,
|
|
1491
|
+
...(toolCall ? { toolCall } : {}),
|
|
1492
|
+
reconnect: close,
|
|
1493
|
+
respond: (result, options = {}) => this.respondInteraction(
|
|
1494
|
+
envelope.rpcId,
|
|
1495
|
+
result,
|
|
1496
|
+
{ ...options, signal: options.signal ?? signal },
|
|
1497
|
+
),
|
|
1498
|
+
}));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (payload.type === 'question/resolved' || payload.type === 'approval/resolved') {
|
|
1502
|
+
const kind = payload.type === 'question/resolved' ? 'question' : 'approval';
|
|
1503
|
+
const interactionId = kind === 'question'
|
|
1504
|
+
? payload.questionRpcId
|
|
1505
|
+
: payload.approvalId;
|
|
1506
|
+
const claimKey = `${kind}:${interactionId}`;
|
|
1507
|
+
if (ownership) {
|
|
1508
|
+
const claim = this.#interactionClaims.get(claimKey);
|
|
1509
|
+
if (claim?.ownership !== ownership) return;
|
|
1510
|
+
this.#interactionClaims.delete(claimKey);
|
|
1498
1511
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1512
|
+
dispatch(onResolved, Object.freeze({
|
|
1513
|
+
kind,
|
|
1514
|
+
interactionId,
|
|
1515
|
+
sessionId,
|
|
1516
|
+
outcome: payload.outcome,
|
|
1517
|
+
payload,
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
const handleEnvelope = (envelope) => {
|
|
1522
|
+
try {
|
|
1523
|
+
const payload = envelope?.payload;
|
|
1524
|
+
if (envelope?.type !== 'server-request'
|
|
1525
|
+
|| typeof envelope.rpcId !== 'string'
|
|
1526
|
+
|| !payload || typeof payload !== 'object'
|
|
1527
|
+
|| envelope.method !== payload.type) {
|
|
1528
|
+
throw new Error('invalid server-request envelope');
|
|
1516
1529
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
)
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
socket.addEventListener('open', handleOpen);
|
|
1533
|
-
socket.addEventListener('message', handleMessage);
|
|
1534
|
-
socket.addEventListener('close', handleClose, { once: true });
|
|
1535
|
-
socket.addEventListener('error', handleError, { once: true });
|
|
1536
|
-
signal.addEventListener('abort', handleAbort, { once: true });
|
|
1537
|
-
if (signal.aborted) handleAbort();
|
|
1538
|
-
});
|
|
1530
|
+
if (payload.sessionId !== sessionId) return;
|
|
1531
|
+
if (!ownershipReady) bufferedEnvelopes.push(envelope);
|
|
1532
|
+
else processEnvelope(envelope);
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
console.warn(`[${this.#logPrefix}] ignored a malformed Harness interaction frame:`, error.message);
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
try {
|
|
1538
|
+
await this.#watchMux({ signal, onOpen: handleOpen, onEnvelope: handleEnvelope });
|
|
1539
|
+
} finally {
|
|
1540
|
+
settled = true;
|
|
1541
|
+
if (ownership?.reconnect === close) ownership.reconnect = null;
|
|
1542
|
+
if (!signal.aborted) await callbackTail;
|
|
1543
|
+
}
|
|
1544
|
+
if (!signal.aborted && callbackFailure) throw callbackFailure;
|
|
1539
1545
|
}
|
|
1540
1546
|
|
|
1541
1547
|
/**
|
|
1542
1548
|
* Watch the global Harness event mux (all sessions) until `signal`
|
|
1543
|
-
* aborts, reconnecting on drop.
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
* provided) fires after every (re)connection so callers can compensate
|
|
1547
|
-
* for events missed while offline.
|
|
1549
|
+
* aborts, reconnecting on drop. Both transports deliver the same envelopes;
|
|
1550
|
+
* only session/event payloads are forwarded. onReconnect fires after every
|
|
1551
|
+
* (re)connection so callers can compensate for events missed while offline.
|
|
1548
1552
|
*/
|
|
1549
1553
|
async watchHarnessEvents({ signal, onSessionEvent, onReconnect } = {}) {
|
|
1550
1554
|
if (typeof onSessionEvent !== 'function') {
|
|
@@ -1556,14 +1560,33 @@ export class HarnessClient {
|
|
|
1556
1560
|
if (onReconnect !== undefined && typeof onReconnect !== 'function') {
|
|
1557
1561
|
throw new TypeError('onReconnect must be a function');
|
|
1558
1562
|
}
|
|
1559
|
-
const url = new URL('/api/events.mux', this.#baseUrl);
|
|
1560
|
-
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1561
1563
|
while (!signal.aborted) {
|
|
1562
1564
|
try {
|
|
1563
|
-
await this.#
|
|
1565
|
+
await this.#watchMux({
|
|
1564
1566
|
signal,
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
+
onOpen: () => {
|
|
1568
|
+
try {
|
|
1569
|
+
onReconnect?.();
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
console.warn(`[${this.#logPrefix}] mux reconnect hook failed:`, error.message);
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
onEnvelope: (envelope) => {
|
|
1575
|
+
try {
|
|
1576
|
+
const payload = envelope?.payload;
|
|
1577
|
+
if (envelope?.type !== 'server-request'
|
|
1578
|
+
|| !payload
|
|
1579
|
+
|| typeof payload !== 'object'
|
|
1580
|
+
|| envelope.method !== payload.type
|
|
1581
|
+
|| payload.type !== 'session/event'
|
|
1582
|
+
|| typeof payload.sessionId !== 'string'
|
|
1583
|
+
|| !payload.event
|
|
1584
|
+
|| typeof payload.event !== 'object') return;
|
|
1585
|
+
onSessionEvent({ sessionId: payload.sessionId, event: payload.event });
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
console.warn(`[${this.#logPrefix}] ignored a malformed global mux frame:`, error.message);
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1567
1590
|
});
|
|
1568
1591
|
} catch (error) {
|
|
1569
1592
|
if (signal.aborted) return;
|
|
@@ -1579,81 +1602,16 @@ export class HarnessClient {
|
|
|
1579
1602
|
}
|
|
1580
1603
|
}
|
|
1581
1604
|
|
|
1582
|
-
#
|
|
1583
|
-
return
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
let finished = false;
|
|
1593
|
-
const close = () => {
|
|
1594
|
-
try {
|
|
1595
|
-
socket.close();
|
|
1596
|
-
} catch {
|
|
1597
|
-
// Already closed.
|
|
1598
|
-
}
|
|
1599
|
-
};
|
|
1600
|
-
const finish = (error) => {
|
|
1601
|
-
if (finished) return;
|
|
1602
|
-
finished = true;
|
|
1603
|
-
socket.removeEventListener('open', handleOpen);
|
|
1604
|
-
socket.removeEventListener('message', handleMessage);
|
|
1605
|
-
socket.removeEventListener('close', handleClose);
|
|
1606
|
-
socket.removeEventListener('error', handleError);
|
|
1607
|
-
signal.removeEventListener('abort', handleAbort);
|
|
1608
|
-
if (error) reject(error);
|
|
1609
|
-
else resolve();
|
|
1610
|
-
};
|
|
1611
|
-
const handleOpen = () => {
|
|
1612
|
-
opened = true;
|
|
1613
|
-
try {
|
|
1614
|
-
onReconnect?.();
|
|
1615
|
-
} catch (error) {
|
|
1616
|
-
console.warn(`[${this.#logPrefix}] mux reconnect hook failed:`, error.message);
|
|
1617
|
-
}
|
|
1618
|
-
};
|
|
1619
|
-
const handleMessage = (event) => {
|
|
1620
|
-
try {
|
|
1621
|
-
if (typeof event.data !== 'string') return;
|
|
1622
|
-
const envelope = JSON.parse(event.data);
|
|
1623
|
-
const payload = envelope?.payload;
|
|
1624
|
-
if (envelope?.type !== 'server-request'
|
|
1625
|
-
|| !payload
|
|
1626
|
-
|| typeof payload !== 'object'
|
|
1627
|
-
|| envelope.method !== payload.type
|
|
1628
|
-
|| payload.type !== 'session/event'
|
|
1629
|
-
|| typeof payload.sessionId !== 'string'
|
|
1630
|
-
|| !payload.event
|
|
1631
|
-
|| typeof payload.event !== 'object') return;
|
|
1632
|
-
onSessionEvent({ sessionId: payload.sessionId, event: payload.event });
|
|
1633
|
-
} catch (error) {
|
|
1634
|
-
console.warn(`[${this.#logPrefix}] ignored a malformed global mux frame:`, error.message);
|
|
1635
|
-
}
|
|
1636
|
-
};
|
|
1637
|
-
const handleClose = () => finish(opened ? null : new Error(
|
|
1638
|
-
'Harness event mux WebSocket closed before opening',
|
|
1639
|
-
));
|
|
1640
|
-
const handleError = () => {
|
|
1641
|
-
finish(new Error(opened
|
|
1642
|
-
? 'Harness event mux WebSocket failed'
|
|
1643
|
-
: 'Harness event mux WebSocket failed before opening'));
|
|
1644
|
-
close();
|
|
1645
|
-
};
|
|
1646
|
-
const handleAbort = () => {
|
|
1647
|
-
close();
|
|
1648
|
-
finish();
|
|
1649
|
-
};
|
|
1650
|
-
|
|
1651
|
-
socket.addEventListener('open', handleOpen);
|
|
1652
|
-
socket.addEventListener('message', handleMessage);
|
|
1653
|
-
socket.addEventListener('close', handleClose, { once: true });
|
|
1654
|
-
socket.addEventListener('error', handleError, { once: true });
|
|
1655
|
-
signal.addEventListener('abort', handleAbort, { once: true });
|
|
1656
|
-
if (signal.aborted) handleAbort();
|
|
1605
|
+
#watchMux(options) {
|
|
1606
|
+
return watchHarnessMux({
|
|
1607
|
+
apiProxy: this.#apiProxy,
|
|
1608
|
+
baseUrl: this.#baseUrl,
|
|
1609
|
+
createWebSocket: this.#createWebSocket,
|
|
1610
|
+
rpcId: `${this.#rpcIdPrefix}-${randomUUID()}`,
|
|
1611
|
+
...options,
|
|
1612
|
+
onMalformed: (error) => {
|
|
1613
|
+
console.warn(`[${this.#logPrefix}] ignored a malformed Harness mux frame:`, error.message);
|
|
1614
|
+
},
|
|
1657
1615
|
});
|
|
1658
1616
|
}
|
|
1659
1617
|
|