@forgeax/engine-intelligence 0.1.6 → 0.1.7

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.
@@ -127,4 +127,762 @@ describe('intelligence MessagePort boundary', () => {
127
127
  await client.close();
128
128
  await host.close();
129
129
  });
130
+
131
+ it('recovers optimistic capacity after an asynchronous Host rejection', async () => {
132
+ const sinks = new Map<string, ActivitySink>();
133
+ const starts: string[] = [];
134
+ const provider: IntelligenceProvider = {
135
+ id: 'host.limit.drift.provider',
136
+ start(submission, sink) {
137
+ starts.push(submission.input);
138
+ sinks.set(String(submission.id), sink);
139
+ return ok(undefined);
140
+ },
141
+ cancel(id) {
142
+ sinks.get(String(id))?.cancelled();
143
+ sinks.delete(String(id));
144
+ return ok(undefined);
145
+ },
146
+ async close() {},
147
+ };
148
+ const runtime = createIntelligenceRuntime(provider, {
149
+ limits: { maxConcurrentActivities: 1 },
150
+ });
151
+ const channel = new MessageChannel();
152
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
153
+ let identity = 0;
154
+ const client = createIntelligencePortClient(
155
+ provider.id,
156
+ channel.port2 as unknown as IntelligenceMessagePort,
157
+ {
158
+ limits: { maxConcurrentActivities: 2 },
159
+ createActivityId: () => {
160
+ identity += 1;
161
+ return activityId(`host-limit-drift-${identity}`);
162
+ },
163
+ createSessionId: () => 'host-limit-drift-session',
164
+ },
165
+ );
166
+
167
+ try {
168
+ const first = client.submit({ input: 'keep-live' });
169
+ const rejected = client.submit({ input: 'host-reject' });
170
+ expect(first.ok).toBe(true);
171
+ expect(rejected.ok).toBe(true);
172
+ if (!first.ok || !rejected.ok) return;
173
+
174
+ const rejectionEvents: ReturnType<typeof client.poll>[number][] = [];
175
+ for (let attempt = 0; attempt < 40 && rejectionEvents.length === 0; attempt += 1) {
176
+ rejectionEvents.push(
177
+ ...client.poll().filter((event) => event.activityId === rejected.value.id),
178
+ );
179
+ if (rejectionEvents.length === 0) await new Promise((resolve) => setTimeout(resolve, 5));
180
+ }
181
+ expect(starts).toEqual(['keep-live']);
182
+ expect(rejectionEvents).toHaveLength(1);
183
+ expect(rejectionEvents[0]).toEqual({
184
+ type: 'failed',
185
+ activityId: rejected.value.id,
186
+ sequence: 1,
187
+ error: expect.objectContaining({
188
+ code: 'intelligence-capacity-exceeded',
189
+ detail: { limit: 1 },
190
+ }),
191
+ });
192
+ const cancelRejected = client.cancel(rejected.value.id);
193
+ expect(cancelRejected.ok).toBe(false);
194
+ if (!cancelRejected.ok)
195
+ expect(cancelRejected.error.code).toBe('intelligence-activity-not-found');
196
+
197
+ sinks.get(String(first.value.id))?.complete('keep-live-ok');
198
+ let firstEvent: ReturnType<typeof client.poll>[number] | undefined;
199
+ for (let attempt = 0; attempt < 40 && firstEvent === undefined; attempt += 1) {
200
+ firstEvent = client.poll().find((event) => event.activityId === first.value.id);
201
+ if (firstEvent === undefined) await new Promise((resolve) => setTimeout(resolve, 5));
202
+ }
203
+ expect(firstEvent).toEqual(
204
+ expect.objectContaining({
205
+ type: 'completed',
206
+ activityId: first.value.id,
207
+ output: 'keep-live-ok',
208
+ }),
209
+ );
210
+
211
+ const retry = client.submit({ input: 'retry', session: rejected.value.session });
212
+ expect(retry.ok).toBe(true);
213
+ if (!retry.ok) return;
214
+ expect(retry.value.session).toEqual(rejected.value.session);
215
+
216
+ for (let attempt = 0; attempt < 40 && !starts.includes('retry'); attempt += 1) {
217
+ await new Promise((resolve) => setTimeout(resolve, 5));
218
+ }
219
+ expect(starts).toEqual(['keep-live', 'retry']);
220
+ sinks.get(String(retry.value.id))?.complete('retry-ok');
221
+
222
+ let retryEvent: ReturnType<typeof client.poll>[number] | undefined;
223
+ for (let attempt = 0; attempt < 40 && retryEvent === undefined; attempt += 1) {
224
+ retryEvent = client.poll().find((event) => event.activityId === retry.value.id);
225
+ if (retryEvent === undefined) await new Promise((resolve) => setTimeout(resolve, 5));
226
+ }
227
+ expect(retryEvent).toEqual(
228
+ expect.objectContaining({
229
+ type: 'completed',
230
+ activityId: retry.value.id,
231
+ session: rejected.value.session,
232
+ output: 'retry-ok',
233
+ }),
234
+ );
235
+ } finally {
236
+ await client.close();
237
+ await host.close();
238
+ }
239
+ });
240
+
241
+ it('rejects a cross-provider SessionRef before transport or identity side effects', async () => {
242
+ const createProvider = (id: string) => {
243
+ const sinks = new Map<string, ActivitySink>();
244
+ const starts: string[] = [];
245
+ let closeCalls = 0;
246
+ let closed = false;
247
+ const provider: IntelligenceProvider = {
248
+ id,
249
+ start(submission, sink) {
250
+ starts.push(submission.input);
251
+ sinks.set(String(submission.id), sink);
252
+ return ok(undefined);
253
+ },
254
+ cancel(activity) {
255
+ sinks.get(String(activity))?.cancelled();
256
+ sinks.delete(String(activity));
257
+ return ok(undefined);
258
+ },
259
+ async close() {
260
+ closeCalls += 1;
261
+ closed = true;
262
+ sinks.clear();
263
+ },
264
+ };
265
+ return {
266
+ provider,
267
+ sinks,
268
+ starts,
269
+ get closeCalls() {
270
+ return closeCalls;
271
+ },
272
+ get closed() {
273
+ return closed;
274
+ },
275
+ };
276
+ };
277
+ const providerA = createProvider('transport.provider.a');
278
+ const providerB = createProvider('transport.provider.b');
279
+ const channelA = new MessageChannel();
280
+ const channelB = new MessageChannel();
281
+ const submittedA: string[] = [];
282
+ const submittedB: string[] = [];
283
+ const observeSubmissions = (port: MessagePort, target: string[]): void => {
284
+ port.addEventListener('message', (event: MessageEvent<unknown>) => {
285
+ const message = event.data;
286
+ if (typeof message !== 'object' || message === null) return;
287
+ const value = message as {
288
+ readonly kind?: unknown;
289
+ readonly submission?: { readonly input?: unknown };
290
+ };
291
+ if (value.kind === 'intelligence-submit' && typeof value.submission?.input === 'string') {
292
+ target.push(value.submission.input);
293
+ }
294
+ });
295
+ port.start();
296
+ };
297
+ observeSubmissions(channelA.port1, submittedA);
298
+ observeSubmissions(channelB.port1, submittedB);
299
+ const hostA = bindIntelligencePort(
300
+ channelA.port1 as unknown as IntelligenceMessagePort,
301
+ createIntelligenceRuntime(providerA.provider, { limits: { maxConcurrentActivities: 1 } }),
302
+ );
303
+ const hostB = bindIntelligencePort(
304
+ channelB.port1 as unknown as IntelligenceMessagePort,
305
+ createIntelligenceRuntime(providerB.provider, { limits: { maxConcurrentActivities: 1 } }),
306
+ );
307
+ let activityCallsA = 0;
308
+ let sessionCallsA = 0;
309
+ let activityCallsB = 0;
310
+ let sessionCallsB = 0;
311
+ const clientA = createIntelligencePortClient(
312
+ providerA.provider.id,
313
+ channelA.port2 as unknown as IntelligenceMessagePort,
314
+ {
315
+ createActivityId: () => activityId(`transport-a-${++activityCallsA}`),
316
+ createSessionId: () => `transport-a-session-${++sessionCallsA}`,
317
+ },
318
+ );
319
+ const clientB = createIntelligencePortClient(
320
+ providerB.provider.id,
321
+ channelB.port2 as unknown as IntelligenceMessagePort,
322
+ {
323
+ limits: { maxConcurrentActivities: 1 },
324
+ createActivityId: () => activityId(`transport-b-${++activityCallsB}`),
325
+ createSessionId: () => `transport-b-session-${++sessionCallsB}`,
326
+ },
327
+ );
328
+ const waitForEvent = async (
329
+ client: typeof clientA,
330
+ predicate: (event: ReturnType<typeof clientA.poll>[number]) => boolean,
331
+ ): Promise<ReturnType<typeof clientA.poll>[number] | undefined> => {
332
+ for (let attempt = 0; attempt < 40; attempt += 1) {
333
+ const event = client.poll().find(predicate);
334
+ if (event !== undefined) return event;
335
+ await new Promise((resolve) => setTimeout(resolve, 5));
336
+ }
337
+ return undefined;
338
+ };
339
+
340
+ try {
341
+ const first = clientA.submit({ input: 'a-first' });
342
+ expect(first.ok).toBe(true);
343
+ if (!first.ok) return;
344
+ for (let attempt = 0; attempt < 40 && providerA.starts.length === 0; attempt += 1) {
345
+ await new Promise((resolve) => setTimeout(resolve, 5));
346
+ }
347
+ expect(providerA.starts).toEqual(['a-first']);
348
+ providerA.sinks.get(String(first.value.id))?.complete('a-first-ok');
349
+ expect(
350
+ (await waitForEvent(clientA, (event) => event.activityId === first.value.id))?.type,
351
+ ).toBe('completed');
352
+
353
+ const mismatch = clientB.submit({ input: 'b-mismatch', session: first.value.session });
354
+ expect(mismatch.ok).toBe(false);
355
+ if (!mismatch.ok) {
356
+ expect(mismatch.error.code).toBe('intelligence-session-provider-mismatch');
357
+ expect(mismatch.error.detail).toEqual({
358
+ expectedProviderId: providerB.provider.id,
359
+ receivedProviderId: providerA.provider.id,
360
+ });
361
+ }
362
+ expect(activityCallsB).toBe(0);
363
+ expect(sessionCallsB).toBe(0);
364
+ expect(submittedB).toEqual([]);
365
+ expect(providerB.starts).toEqual([]);
366
+
367
+ const validB = clientB.submit({ input: 'b-valid' });
368
+ expect(validB.ok).toBe(true);
369
+ if (!validB.ok) return;
370
+ for (let attempt = 0; attempt < 40 && providerB.starts.length === 0; attempt += 1) {
371
+ await new Promise((resolve) => setTimeout(resolve, 5));
372
+ }
373
+ expect(providerB.starts).toEqual(['b-valid']);
374
+ providerB.sinks.get(String(validB.value.id))?.complete('b-valid-ok');
375
+ expect(
376
+ (await waitForEvent(clientB, (event) => event.activityId === validB.value.id))?.type,
377
+ ).toBe('completed');
378
+
379
+ const retryA = clientA.submit({ input: 'a-retry', session: first.value.session });
380
+ expect(retryA.ok).toBe(true);
381
+ if (!retryA.ok) return;
382
+ expect(retryA.value.session).toEqual(first.value.session);
383
+ for (let attempt = 0; attempt < 40 && providerA.starts.length < 2; attempt += 1) {
384
+ await new Promise((resolve) => setTimeout(resolve, 5));
385
+ }
386
+ expect(providerA.starts).toEqual(['a-first', 'a-retry']);
387
+ providerA.sinks.get(String(retryA.value.id))?.complete('a-retry-ok');
388
+ expect(
389
+ (await waitForEvent(clientA, (event) => event.activityId === retryA.value.id))?.type,
390
+ ).toBe('completed');
391
+
392
+ expect(activityCallsA).toBe(2);
393
+ expect(sessionCallsA).toBe(1);
394
+ expect(activityCallsB).toBe(1);
395
+ expect(sessionCallsB).toBe(1);
396
+ expect(submittedA).toEqual(['a-first', 'a-retry']);
397
+ expect(submittedB).toEqual(['b-valid']);
398
+ } finally {
399
+ await Promise.all([clientA.close(), clientB.close()]);
400
+ await Promise.all([hostA.close(), hostB.close()]);
401
+ }
402
+
403
+ expect(providerA.closeCalls).toBe(1);
404
+ expect(providerB.closeCalls).toBe(1);
405
+ expect(providerA.sinks).toHaveLength(0);
406
+ expect(providerB.sinks).toHaveLength(0);
407
+ expect(providerA.closed).toBe(true);
408
+ expect(providerB.closed).toBe(true);
409
+ });
410
+
411
+ it('drains an interleaved backlog with one poll credit and releases terminal capacity', async () => {
412
+ const sinks = new Map<string, ActivitySink>();
413
+ const starts: string[] = [];
414
+ const provider: IntelligenceProvider = {
415
+ id: 'port.poll.credit.provider',
416
+ start(submission, sink) {
417
+ starts.push(submission.input);
418
+ sinks.set(String(submission.id), sink);
419
+ return ok(undefined);
420
+ },
421
+ cancel(id) {
422
+ sinks.get(String(id))?.cancelled();
423
+ return ok(undefined);
424
+ },
425
+ async close() {},
426
+ };
427
+ const runtime = createIntelligenceRuntime(provider, {
428
+ limits: { maxConcurrentActivities: 2, maxPollEvents: 1 },
429
+ });
430
+ const channel = new MessageChannel();
431
+ const rawHostPort = channel.port1;
432
+ let pollRequests = 0;
433
+ let pollResponses = 0;
434
+ let pollOutstanding = 0;
435
+ let maxPollOutstanding = 0;
436
+ rawHostPort.addEventListener('message', (event: MessageEvent<unknown>) => {
437
+ const message = event.data;
438
+ if (typeof message !== 'object' || message === null) return;
439
+ if ((message as { readonly kind?: unknown }).kind !== 'intelligence-poll') return;
440
+ pollRequests += 1;
441
+ pollOutstanding += 1;
442
+ maxPollOutstanding = Math.max(maxPollOutstanding, pollOutstanding);
443
+ });
444
+ const hostPort: IntelligenceMessagePort = {
445
+ postMessage(message) {
446
+ if (message.kind === 'intelligence-events') {
447
+ pollResponses += 1;
448
+ pollOutstanding -= 1;
449
+ }
450
+ rawHostPort.postMessage(message);
451
+ },
452
+ addEventListener(type, listener) {
453
+ rawHostPort.addEventListener(type, listener as EventListener);
454
+ },
455
+ removeEventListener(type, listener) {
456
+ rawHostPort.removeEventListener(type, listener as EventListener);
457
+ },
458
+ start() {
459
+ rawHostPort.start();
460
+ },
461
+ close() {
462
+ rawHostPort.close();
463
+ },
464
+ };
465
+ const host = bindIntelligencePort(hostPort, runtime);
466
+ const client = createIntelligencePortClient(
467
+ provider.id,
468
+ channel.port2 as unknown as IntelligenceMessagePort,
469
+ {
470
+ limits: { maxConcurrentActivities: 2, maxPollEvents: 1 },
471
+ createActivityId: (() => {
472
+ let identity = 0;
473
+ return () => activityId(`poll-credit-${++identity}`);
474
+ })(),
475
+ },
476
+ );
477
+
478
+ try {
479
+ const first = client.submit({ input: 'first' });
480
+ const second = client.submit({ input: 'second' });
481
+ expect(first.ok).toBe(true);
482
+ expect(second.ok).toBe(true);
483
+ if (!first.ok || !second.ok) return;
484
+
485
+ for (let attempt = 0; attempt < 40 && sinks.size < 2; attempt += 1) {
486
+ await new Promise((resolve) => setTimeout(resolve, 1));
487
+ }
488
+ expect(starts).toEqual(['first', 'second']);
489
+ const firstSink = sinks.get(String(first.value.id));
490
+ const secondSink = sinks.get(String(second.value.id));
491
+ expect(firstSink).toBeDefined();
492
+ expect(secondSink).toBeDefined();
493
+ if (firstSink === undefined || secondSink === undefined) return;
494
+
495
+ const emitted: string[] = [];
496
+ emitted.push('first:text-1');
497
+ firstSink.text('first-1');
498
+ emitted.push('second:text-1');
499
+ secondSink.text('second-1');
500
+ emitted.push('first:text-2');
501
+ firstSink.text('first-2');
502
+ emitted.push('second:text-2');
503
+ secondSink.text('second-2');
504
+ emitted.push('first:terminal');
505
+ firstSink.complete('first-ok');
506
+ emitted.push('second:terminal');
507
+ secondSink.complete('second-ok');
508
+ expect(emitted).toEqual([
509
+ 'first:text-1',
510
+ 'second:text-1',
511
+ 'first:text-2',
512
+ 'second:text-2',
513
+ 'first:terminal',
514
+ 'second:terminal',
515
+ ]);
516
+
517
+ client.poll();
518
+ client.poll();
519
+ for (let attempt = 0; attempt < 40 && pollRequests < 1; attempt += 1) {
520
+ await new Promise((resolve) => setTimeout(resolve, 1));
521
+ }
522
+ expect(pollRequests).toBe(1);
523
+ expect(maxPollOutstanding).toBe(1);
524
+
525
+ const received: ReturnType<typeof client.poll>[number][] = [];
526
+ for (let attempt = 0; attempt < 200 && received.length < 6; attempt += 1) {
527
+ received.push(...client.poll());
528
+ await new Promise((resolve) => setTimeout(resolve, 1));
529
+ }
530
+ expect(received).toHaveLength(6);
531
+ for (const id of [first.value.id, second.value.id]) {
532
+ const activityEvents = received.filter((event) => event.activityId === id);
533
+ expect(activityEvents.map((event) => event.sequence)).toEqual([1, 2, 3]);
534
+ expect(activityEvents.at(-1)?.type).toBe('completed');
535
+ }
536
+ expect(new Set(received.map((event) => String(event.activityId)))).toEqual(
537
+ new Set([String(first.value.id), String(second.value.id)]),
538
+ );
539
+ for (let attempt = 0; attempt < 40 && pollOutstanding !== 0; attempt += 1) {
540
+ await new Promise((resolve) => setTimeout(resolve, 1));
541
+ }
542
+ expect(pollOutstanding).toBe(0);
543
+ expect(pollResponses).toBe(pollRequests);
544
+ expect(maxPollOutstanding).toBe(1);
545
+
546
+ const requestsBeforeRecovery = pollRequests;
547
+ const after = client.submit({ input: 'after terminal' });
548
+ expect(after.ok).toBe(true);
549
+ if (!after.ok) return;
550
+ for (let attempt = 0; attempt < 40 && !starts.includes('after terminal'); attempt += 1) {
551
+ await new Promise((resolve) => setTimeout(resolve, 1));
552
+ }
553
+ expect(starts).toEqual(['first', 'second', 'after terminal']);
554
+ sinks.get(String(after.value.id))?.complete('after-ok');
555
+
556
+ const afterEvents: ReturnType<typeof client.poll>[number][] = [];
557
+ for (let attempt = 0; attempt < 100 && afterEvents.length === 0; attempt += 1) {
558
+ afterEvents.push(...client.poll());
559
+ await new Promise((resolve) => setTimeout(resolve, 1));
560
+ }
561
+ expect(afterEvents).toEqual([
562
+ expect.objectContaining({
563
+ type: 'completed',
564
+ activityId: after.value.id,
565
+ sequence: 1,
566
+ output: 'after-ok',
567
+ }),
568
+ ]);
569
+ for (let attempt = 0; attempt < 40 && pollOutstanding !== 0; attempt += 1) {
570
+ await new Promise((resolve) => setTimeout(resolve, 1));
571
+ }
572
+ expect(pollOutstanding).toBe(0);
573
+ expect(pollRequests - requestsBeforeRecovery).toBe(2);
574
+ expect(maxPollOutstanding).toBe(1);
575
+ } finally {
576
+ await client.close();
577
+ await host.close();
578
+ }
579
+ });
580
+
581
+ it('rejects invalid input synchronously before identity, capacity, port dispatch, or provider start', async () => {
582
+ const sinks = new Map<string, ActivitySink>();
583
+ const submittedInputs: string[] = [];
584
+ let activityIdentityCalls = 0;
585
+ let sessionIdentityCalls = 0;
586
+ let providerStarts = 0;
587
+ const provider: IntelligenceProvider = {
588
+ id: 'port.input.boundary.provider',
589
+ start(submission, sink) {
590
+ providerStarts += 1;
591
+ sinks.set(submission.id, sink);
592
+ return ok(undefined);
593
+ },
594
+ cancel() {
595
+ return ok(undefined);
596
+ },
597
+ async close() {},
598
+ };
599
+ const runtime = createIntelligenceRuntime(provider, {
600
+ limits: { maxInputChars: 4, maxConcurrentActivities: 1 },
601
+ });
602
+ const channel = new MessageChannel();
603
+ channel.port1.addEventListener('message', (event: MessageEvent<unknown>) => {
604
+ const message = event.data;
605
+ if (typeof message !== 'object' || message === null) return;
606
+ const value = message as {
607
+ readonly kind?: unknown;
608
+ readonly submission?: { readonly input?: unknown };
609
+ };
610
+ if (value.kind === 'intelligence-submit' && typeof value.submission?.input === 'string') {
611
+ submittedInputs.push(value.submission.input);
612
+ }
613
+ });
614
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
615
+ const client = createIntelligencePortClient(
616
+ 'port.input.boundary.provider',
617
+ channel.port2 as unknown as IntelligenceMessagePort,
618
+ {
619
+ limits: { maxInputChars: 4, maxConcurrentActivities: 1 },
620
+ createActivityId: () => {
621
+ activityIdentityCalls += 1;
622
+ return activityId('port-input-boundary-activity');
623
+ },
624
+ createSessionId: () => {
625
+ sessionIdentityCalls += 1;
626
+ return 'port-input-boundary-session';
627
+ },
628
+ },
629
+ );
630
+
631
+ const exact = client.submit({ input: 'abcd' });
632
+ expect(exact.ok).toBe(true);
633
+ if (!exact.ok) return;
634
+ expect(activityIdentityCalls).toBe(1);
635
+ expect(sessionIdentityCalls).toBe(1);
636
+ expect(providerStarts).toBe(0);
637
+
638
+ const empty = client.submit({ input: '' });
639
+ expect(empty.ok).toBe(false);
640
+ if (!empty.ok) {
641
+ expect(empty.error.code).toBe('intelligence-invalid-request');
642
+ expect(empty.error.detail).toEqual({ field: 'input', reason: 'input is empty' });
643
+ }
644
+
645
+ const overLimit = client.submit({ input: 'abcde' });
646
+ expect(overLimit.ok).toBe(false);
647
+ if (!overLimit.ok) {
648
+ expect(overLimit.error.code).toBe('intelligence-invalid-request');
649
+ expect(overLimit.error.detail).toEqual({
650
+ field: 'input',
651
+ reason: 'input exceeds 4 characters',
652
+ });
653
+ }
654
+
655
+ expect(activityIdentityCalls).toBe(1);
656
+ expect(sessionIdentityCalls).toBe(1);
657
+ expect(providerStarts).toBe(0);
658
+
659
+ for (let attempt = 0; attempt < 10 && providerStarts === 0; attempt += 1) {
660
+ await new Promise((resolve) => setTimeout(resolve, 5));
661
+ }
662
+ expect(providerStarts).toBe(1);
663
+ expect(submittedInputs).toEqual(['abcd']);
664
+ sinks.get(exact.value.id)?.complete('done');
665
+
666
+ let received: readonly ReturnType<typeof client.poll>[number][] = [];
667
+ for (let attempt = 0; attempt < 10 && received.length === 0; attempt += 1) {
668
+ received = client.poll();
669
+ await new Promise((resolve) => setTimeout(resolve, 5));
670
+ }
671
+ expect(received).toEqual([
672
+ {
673
+ type: 'completed',
674
+ activityId: exact.value.id,
675
+ sequence: 1,
676
+ session: exact.value.session,
677
+ output: 'done',
678
+ },
679
+ ]);
680
+
681
+ await client.close();
682
+ await host.close();
683
+ });
684
+
685
+ it('settles client close after a rejecting Host provider and permits a fresh binding', async () => {
686
+ let closeCalls = 0;
687
+ let starts = 0;
688
+ const provider: IntelligenceProvider = {
689
+ id: 'port.close.rejection.provider',
690
+ start(_submission, sink) {
691
+ starts += 1;
692
+ sink.complete('closed-run');
693
+ return ok(undefined);
694
+ },
695
+ cancel() {
696
+ return ok(undefined);
697
+ },
698
+ async close() {
699
+ closeCalls += 1;
700
+ throw new Error('sentinel Host close rejection');
701
+ },
702
+ };
703
+ const runtime = createIntelligenceRuntime(provider);
704
+ const channel = new MessageChannel();
705
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
706
+ const client = createIntelligencePortClient(
707
+ 'port.close.rejection.provider',
708
+ channel.port2 as unknown as IntelligenceMessagePort,
709
+ );
710
+
711
+ try {
712
+ expect(client.submit({ input: 'first binding' }).ok).toBe(true);
713
+ for (let attempt = 0; attempt < 20 && starts === 0; attempt += 1) {
714
+ await new Promise((resolve) => setTimeout(resolve, 5));
715
+ }
716
+ expect(starts).toBe(1);
717
+
718
+ const closeSettled = await Promise.race([
719
+ client.close().then(
720
+ () => true,
721
+ () => false,
722
+ ),
723
+ new Promise<false>((resolve) => setTimeout(() => resolve(false), 250)),
724
+ ]);
725
+ expect(closeSettled).toBe(true);
726
+ expect(closeCalls).toBe(1);
727
+
728
+ const freshChannel = new MessageChannel();
729
+ const freshProvider: IntelligenceProvider = {
730
+ id: 'port.close.recovery.provider',
731
+ start(_submission, sink) {
732
+ sink.complete('fresh-run');
733
+ return ok(undefined);
734
+ },
735
+ cancel() {
736
+ return ok(undefined);
737
+ },
738
+ async close() {},
739
+ };
740
+ const freshHost = bindIntelligencePort(
741
+ freshChannel.port1 as unknown as IntelligenceMessagePort,
742
+ createIntelligenceRuntime(freshProvider),
743
+ );
744
+ const freshClient = createIntelligencePortClient(
745
+ 'port.close.recovery.provider',
746
+ freshChannel.port2 as unknown as IntelligenceMessagePort,
747
+ );
748
+ try {
749
+ const freshStarted = freshClient.submit({ input: 'fresh binding' });
750
+ expect(freshStarted.ok).toBe(true);
751
+ let freshEvents: readonly ReturnType<typeof freshClient.poll>[number][] = [];
752
+ for (let attempt = 0; attempt < 20 && freshEvents.length === 0; attempt += 1) {
753
+ freshEvents = freshClient.poll();
754
+ await new Promise((resolve) => setTimeout(resolve, 5));
755
+ }
756
+ expect(freshEvents).toEqual([
757
+ expect.objectContaining({ type: 'completed', output: 'fresh-run' }),
758
+ ]);
759
+ await freshClient.close();
760
+ } finally {
761
+ await freshHost.close().catch(() => undefined);
762
+ }
763
+ } finally {
764
+ await host.close().catch(() => undefined);
765
+ }
766
+ });
767
+
768
+ it('propagates direct Host binding close to a live realm client', async () => {
769
+ let sink: ActivitySink | undefined;
770
+ let starts = 0;
771
+ let closeCalls = 0;
772
+ const provider: IntelligenceProvider = {
773
+ id: 'port.host-close.provider',
774
+ start(_submission, nextSink) {
775
+ starts += 1;
776
+ sink = nextSink;
777
+ return ok(undefined);
778
+ },
779
+ cancel() {
780
+ return ok(undefined);
781
+ },
782
+ async close() {
783
+ closeCalls += 1;
784
+ },
785
+ };
786
+ const runtime = createIntelligenceRuntime(provider, {
787
+ limits: { maxConcurrentActivities: 1 },
788
+ });
789
+ const channel = new MessageChannel();
790
+ const rawClientPort = channel.port2;
791
+ let closedMessageObserved = false;
792
+ rawClientPort.addEventListener('message', (event: MessageEvent<unknown>) => {
793
+ const message = event.data;
794
+ if (typeof message === 'object' && message !== null && 'kind' in message) {
795
+ if ((message as { readonly kind?: unknown }).kind === 'intelligence-closed') {
796
+ closedMessageObserved = true;
797
+ }
798
+ }
799
+ });
800
+ rawClientPort.start();
801
+ const clientCommands: string[] = [];
802
+ const clientPort: IntelligenceMessagePort = {
803
+ postMessage(message) {
804
+ if (
805
+ message.kind === 'intelligence-submit' ||
806
+ message.kind === 'intelligence-poll' ||
807
+ message.kind === 'intelligence-cancel' ||
808
+ message.kind === 'intelligence-close'
809
+ ) {
810
+ clientCommands.push(message.kind);
811
+ }
812
+ rawClientPort.postMessage(message);
813
+ },
814
+ addEventListener(type, listener) {
815
+ rawClientPort.addEventListener(type, listener as EventListener);
816
+ },
817
+ removeEventListener(type, listener) {
818
+ rawClientPort.removeEventListener(type, listener as EventListener);
819
+ },
820
+ start() {
821
+ rawClientPort.start();
822
+ },
823
+ close() {
824
+ rawClientPort.close();
825
+ },
826
+ };
827
+ const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
828
+ let activityIdentityCalls = 0;
829
+ let sessionIdentityCalls = 0;
830
+ const client = createIntelligencePortClient(provider.id, clientPort, {
831
+ limits: { maxConcurrentActivities: 1 },
832
+ createActivityId: () => {
833
+ activityIdentityCalls += 1;
834
+ return activityId(`host-close-${activityIdentityCalls}`);
835
+ },
836
+ createSessionId: () => {
837
+ sessionIdentityCalls += 1;
838
+ return `host-close-session-${sessionIdentityCalls}`;
839
+ },
840
+ });
841
+
842
+ try {
843
+ const started = client.submit({ input: 'live' });
844
+ expect(started.ok).toBe(true);
845
+ if (!started.ok) return;
846
+ for (let attempt = 0; attempt < 20 && starts === 0; attempt += 1) {
847
+ await new Promise((resolve) => setTimeout(resolve, 5));
848
+ }
849
+ expect(starts).toBe(1);
850
+ client.poll();
851
+
852
+ await host.close();
853
+ for (let attempt = 0; attempt < 20 && !closedMessageObserved; attempt += 1) {
854
+ await new Promise((resolve) => setTimeout(resolve, 5));
855
+ }
856
+ expect(closedMessageObserved).toBe(true);
857
+
858
+ const afterClose = client.submit({ input: 'after-close' });
859
+ expect(afterClose.ok).toBe(false);
860
+ if (!afterClose.ok) expect(afterClose.error.code).toBe('intelligence-closed');
861
+ const cancelled = client.cancel(started.value.id);
862
+ expect(cancelled.ok).toBe(false);
863
+ if (!cancelled.ok) expect(cancelled.error.code).toBe('intelligence-closed');
864
+ expect(client.poll()).toEqual([]);
865
+ expect(activityIdentityCalls).toBe(1);
866
+ expect(sessionIdentityCalls).toBe(1);
867
+ expect(starts).toBe(1);
868
+ expect(closeCalls).toBe(1);
869
+ expect(clientCommands).toEqual(['intelligence-submit', 'intelligence-poll']);
870
+
871
+ const closeSettled = await Promise.race([
872
+ client.close().then(
873
+ () => true,
874
+ () => false,
875
+ ),
876
+ new Promise<false>((resolve) => setTimeout(() => resolve(false), 250)),
877
+ ]);
878
+ expect(closeSettled).toBe(true);
879
+
880
+ sink?.text('late');
881
+ sink?.complete('late');
882
+ expect(client.poll()).toEqual([]);
883
+ } finally {
884
+ await host.close();
885
+ clientPort.close();
886
+ }
887
+ });
130
888
  });