pi-fabric 0.8.2 → 0.9.1
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/dist/actors/manager.d.ts +21 -0
- package/dist/actors/manager.d.ts.map +1 -1
- package/dist/actors/manager.js +197 -115
- package/dist/actors/manager.js.map +1 -1
- package/dist/commands/fabric.d.ts.map +1 -1
- package/dist/commands/fabric.js +38 -1
- package/dist/commands/fabric.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/agents-provider.d.ts.map +1 -1
- package/dist/providers/agents-provider.js +38 -0
- package/dist/providers/agents-provider.js.map +1 -1
- package/dist/ui/controller.d.ts.map +1 -1
- package/dist/ui/controller.js +9 -0
- package/dist/ui/controller.js.map +1 -1
- package/dist/ui/dashboard.d.ts +6 -0
- package/dist/ui/dashboard.d.ts.map +1 -1
- package/dist/ui/dashboard.js +59 -5
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/fabric-host-event-selector.d.ts +30 -0
- package/dist/ui/fabric-host-event-selector.d.ts.map +1 -0
- package/dist/ui/fabric-host-event-selector.js +89 -0
- package/dist/ui/fabric-host-event-selector.js.map +1 -0
- package/package.json +1 -1
package/dist/actors/manager.d.ts
CHANGED
|
@@ -33,6 +33,19 @@ export declare class ActorManager {
|
|
|
33
33
|
* actor inherits the Fabric default (subagents.thinking, default "medium").
|
|
34
34
|
*/
|
|
35
35
|
setThinking(id: string, thinking: string | undefined): Promise<FabricActorInfo>;
|
|
36
|
+
/**
|
|
37
|
+
* Replace an existing actor's host-event subscriptions. Already-queued work
|
|
38
|
+
* for a removed event still runs, but future dispatches respect the new set.
|
|
39
|
+
* Pass an empty array to pause host-event reactivity while keeping the actor
|
|
40
|
+
* alive and reachable by direct messages and mesh topics.
|
|
41
|
+
*/
|
|
42
|
+
setEvents(id: string, events: FabricActorHostEvent[]): Promise<FabricActorInfo>;
|
|
43
|
+
/**
|
|
44
|
+
* Clear an actor's recorded inbox/outbox history. The actor keeps running;
|
|
45
|
+
* only its bounded message log is reset — useful to declutter a long mailbox
|
|
46
|
+
* from the dashboard without stopping the actor.
|
|
47
|
+
*/
|
|
48
|
+
clearMessages(id: string): Promise<FabricActorInfo>;
|
|
36
49
|
tell(id: string, message: string, data?: unknown): {
|
|
37
50
|
queued: true;
|
|
38
51
|
messageId: string;
|
|
@@ -46,6 +59,14 @@ export declare class ActorManager {
|
|
|
46
59
|
}): FabricActorLog;
|
|
47
60
|
dispatchHostEvent(event: FabricActorHostEvent, payload: unknown): number;
|
|
48
61
|
stop(id: string): Promise<FabricActorInfo>;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the stop-the-world gate is currently armed. haltAll() arms it
|
|
64
|
+
* (ESC stop-the-world) and the "input" host event lifts it when the user
|
|
65
|
+
* resumes with a new message. Read-only view of the private gate so the
|
|
66
|
+
* ESC handler can treat a repeated lone Esc while already halted as a
|
|
67
|
+
* no-op rather than re-arming and re-notifying.
|
|
68
|
+
*/
|
|
69
|
+
get halted(): boolean;
|
|
49
70
|
/**
|
|
50
71
|
* Interrupt every non-stopped actor: abort its in-flight run (if any) and
|
|
51
72
|
* reject every queued message so subsequent execution is cancelled. Unlike
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/actors/manager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAA2B,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAkB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,OAAO,KAAK,EAEV,0BAA0B,EAE1B,oBAAoB,EACpB,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAGnB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/actors/manager.ts"],"names":[],"mappings":"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"}
|
package/dist/actors/manager.js
CHANGED
|
@@ -15,10 +15,6 @@ const HOST_EVENTS = new Set([
|
|
|
15
15
|
"session_compact",
|
|
16
16
|
]);
|
|
17
17
|
const MESSAGE_HISTORY_LIMIT = 100;
|
|
18
|
-
// After a user-triggered halt (ESC), ignore host-event dispatches for a short
|
|
19
|
-
// window so the interrupt's own turn_end / agent_settled events do not
|
|
20
|
-
// immediately re-arm the actors that were just stopped.
|
|
21
|
-
const HOST_EVENT_HALT_COOLDOWN_MS = 1_000;
|
|
22
18
|
const atomicWrite = (filePath, value) => {
|
|
23
19
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
24
20
|
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -108,7 +104,11 @@ export class ActorManager {
|
|
|
108
104
|
#meshOffset;
|
|
109
105
|
#polling = false;
|
|
110
106
|
#closing = false;
|
|
111
|
-
|
|
107
|
+
// Stop-the-world gate armed by haltAll() (ESC): while true, host-event and
|
|
108
|
+
// mesh dispatch are frozen so interrupted actors are not re-armed by the
|
|
109
|
+
// interrupt's own turn_end / agent_settled events. Lifted when the user
|
|
110
|
+
// resumes by sending a new message (the "input" host event).
|
|
111
|
+
#halted = false;
|
|
112
112
|
constructor(sessionId, identity, mesh, meshConfig, subagents, onDeliver, options = {}) {
|
|
113
113
|
this.sessionId = sessionId;
|
|
114
114
|
this.identity = identity;
|
|
@@ -174,6 +174,7 @@ export class ActorManager {
|
|
|
174
174
|
...(request.timeoutMs ? { timeoutMs: request.timeoutMs } : {}),
|
|
175
175
|
sessionFile: path.join(actorDirectory, "session.jsonl"),
|
|
176
176
|
queue: [],
|
|
177
|
+
draining: false,
|
|
177
178
|
messages: [],
|
|
178
179
|
createdAt: Date.now(),
|
|
179
180
|
updatedAt: Date.now(),
|
|
@@ -239,6 +240,38 @@ export class ActorManager {
|
|
|
239
240
|
await this.#publishPresence(actor);
|
|
240
241
|
return this.#publicInfo(actor);
|
|
241
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Replace an existing actor's host-event subscriptions. Already-queued work
|
|
245
|
+
* for a removed event still runs, but future dispatches respect the new set.
|
|
246
|
+
* Pass an empty array to pause host-event reactivity while keeping the actor
|
|
247
|
+
* alive and reachable by direct messages and mesh topics.
|
|
248
|
+
*/
|
|
249
|
+
async setEvents(id, events) {
|
|
250
|
+
const actor = this.#requireActor(id);
|
|
251
|
+
const next = [...new Set(events)];
|
|
252
|
+
for (const event of next) {
|
|
253
|
+
if (!HOST_EVENTS.has(event))
|
|
254
|
+
throw new Error(`Unsupported Fabric actor event: ${event}`);
|
|
255
|
+
}
|
|
256
|
+
actor.events = next;
|
|
257
|
+
actor.updatedAt = Date.now();
|
|
258
|
+
this.#saveActors();
|
|
259
|
+
await this.#publishPresence(actor);
|
|
260
|
+
return this.#publicInfo(actor);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Clear an actor's recorded inbox/outbox history. The actor keeps running;
|
|
264
|
+
* only its bounded message log is reset — useful to declutter a long mailbox
|
|
265
|
+
* from the dashboard without stopping the actor.
|
|
266
|
+
*/
|
|
267
|
+
async clearMessages(id) {
|
|
268
|
+
const actor = this.#requireActor(id);
|
|
269
|
+
actor.messages = [];
|
|
270
|
+
actor.updatedAt = Date.now();
|
|
271
|
+
this.#saveActors();
|
|
272
|
+
await this.#publishPresence(actor);
|
|
273
|
+
return this.#publicInfo(actor);
|
|
274
|
+
}
|
|
242
275
|
tell(id, message, data) {
|
|
243
276
|
this.#validateDirectMessage(message, data);
|
|
244
277
|
const actor = this.#requireActiveActor(id);
|
|
@@ -337,9 +370,12 @@ export class ActorManager {
|
|
|
337
370
|
dispatchHostEvent(event, payload) {
|
|
338
371
|
if (this.#closing || !this.meshConfig.enabled)
|
|
339
372
|
return 0;
|
|
340
|
-
|
|
373
|
+
// The user sending a new message ends a stop-the-world halt: lift the gate
|
|
374
|
+
// before dispatching so input-subscribed actors receive this event.
|
|
375
|
+
if (event === "input")
|
|
376
|
+
this.#halted = false;
|
|
377
|
+
if (this.#halted)
|
|
341
378
|
return 0;
|
|
342
|
-
}
|
|
343
379
|
let delivered = 0;
|
|
344
380
|
for (const actor of this.#actors.values()) {
|
|
345
381
|
if (actor.status === "stopped" || !actor.events.includes(event))
|
|
@@ -374,6 +410,16 @@ export class ActorManager {
|
|
|
374
410
|
.catch(() => undefined);
|
|
375
411
|
return this.#publicInfo(actor);
|
|
376
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Whether the stop-the-world gate is currently armed. haltAll() arms it
|
|
415
|
+
* (ESC stop-the-world) and the "input" host event lifts it when the user
|
|
416
|
+
* resumes with a new message. Read-only view of the private gate so the
|
|
417
|
+
* ESC handler can treat a repeated lone Esc while already halted as a
|
|
418
|
+
* no-op rather than re-arming and re-notifying.
|
|
419
|
+
*/
|
|
420
|
+
get halted() {
|
|
421
|
+
return this.#halted;
|
|
422
|
+
}
|
|
377
423
|
/**
|
|
378
424
|
* Interrupt every non-stopped actor: abort its in-flight run (if any) and
|
|
379
425
|
* reject every queued message so subsequent execution is cancelled. Unlike
|
|
@@ -387,7 +433,11 @@ export class ActorManager {
|
|
|
387
433
|
if (!this.meshConfig.enabled)
|
|
388
434
|
return { halted: 0 };
|
|
389
435
|
let halted = 0;
|
|
390
|
-
|
|
436
|
+
// Arm stop-the-world: freeze host-event and mesh dispatch until the user
|
|
437
|
+
// resumes with a new message. Always arm the gate (even with no active
|
|
438
|
+
// work) so an idle-but-subscribed actor is not re-armed by the interrupt's
|
|
439
|
+
// own settle events.
|
|
440
|
+
this.#halted = true;
|
|
391
441
|
for (const actor of this.#actors.values()) {
|
|
392
442
|
if (actor.status === "stopped")
|
|
393
443
|
continue;
|
|
@@ -458,6 +508,7 @@ export class ActorManager {
|
|
|
458
508
|
if (existing) {
|
|
459
509
|
existing.payload = payload;
|
|
460
510
|
existing.createdAt = Date.now();
|
|
511
|
+
this.#ensureDrain(actor);
|
|
461
512
|
return existing;
|
|
462
513
|
}
|
|
463
514
|
}
|
|
@@ -486,123 +537,149 @@ export class ActorManager {
|
|
|
486
537
|
data: structuredClone(payload),
|
|
487
538
|
});
|
|
488
539
|
void this.#publishPresence(actor);
|
|
489
|
-
|
|
490
|
-
delete actor.drain;
|
|
491
|
-
});
|
|
540
|
+
this.#ensureDrain(actor);
|
|
492
541
|
return item;
|
|
493
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Ensure exactly one drain loop is processing the actor's queue. The loop
|
|
545
|
+
* clears `actor.draining` synchronously when it exits, so a host-event
|
|
546
|
+
* enqueue that lands in the microtask window between the loop exiting and
|
|
547
|
+
* this drain's promise settling still observes `draining === false` and
|
|
548
|
+
* starts a fresh drain — preventing a queued item from being stranded with
|
|
549
|
+
* no drain to process it (the "stuck at queue:1" race).
|
|
550
|
+
*/
|
|
551
|
+
#ensureDrain(actor) {
|
|
552
|
+
if (actor.draining || actor.status === "stopped" || this.#closing)
|
|
553
|
+
return;
|
|
554
|
+
actor.draining = true;
|
|
555
|
+
const drain = this.#drain(actor);
|
|
556
|
+
actor.drain = drain;
|
|
557
|
+
const release = () => {
|
|
558
|
+
if (actor.drain === drain)
|
|
559
|
+
delete actor.drain;
|
|
560
|
+
};
|
|
561
|
+
drain.then(release, release);
|
|
562
|
+
}
|
|
494
563
|
async #drain(actor) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
564
|
+
try {
|
|
565
|
+
while (actor.queue.length > 0 && actor.status !== "stopped" && !this.#closing) {
|
|
566
|
+
const item = actor.queue.shift();
|
|
567
|
+
if (!item)
|
|
568
|
+
break;
|
|
569
|
+
actor.status = "running";
|
|
570
|
+
actor.updatedAt = Date.now();
|
|
571
|
+
delete actor.lastError;
|
|
572
|
+
const abortController = new AbortController();
|
|
573
|
+
actor.abortController = abortController;
|
|
574
|
+
await this.#publishPresence(actor);
|
|
575
|
+
let runId;
|
|
576
|
+
const previousRunId = actor.lastRunId;
|
|
577
|
+
let runCompleted = false;
|
|
578
|
+
try {
|
|
579
|
+
const result = await this.subagents.run(this.#runRequest(actor, item), abortController.signal);
|
|
580
|
+
runId = result.id;
|
|
581
|
+
actor.lastRunId = result.id;
|
|
582
|
+
runCompleted = result.status === "completed";
|
|
583
|
+
if (result.status !== "completed") {
|
|
584
|
+
if (actor.responseMode === "directive") {
|
|
585
|
+
// A failed directive run is non-fatal: stay silent and keep the
|
|
586
|
+
// actor ambient instead of erroring out. Record the run error for
|
|
587
|
+
// debugging; the failed run itself is retained (see finally) so
|
|
588
|
+
// agents.status(actor.lastRunId) can inspect the full output.
|
|
589
|
+
const silent = {
|
|
590
|
+
id: randomUUID(),
|
|
591
|
+
actorId: actor.id,
|
|
592
|
+
actorName: actor.name,
|
|
593
|
+
direction: "out",
|
|
594
|
+
source: item.source,
|
|
595
|
+
createdAt: Date.now(),
|
|
596
|
+
action: "silent",
|
|
597
|
+
data: { runError: result.error || `Actor run ${result.status}`, runId: result.id },
|
|
598
|
+
runId: result.id,
|
|
599
|
+
usage: result.usage,
|
|
600
|
+
};
|
|
601
|
+
this.#recordMessage(actor, silent);
|
|
602
|
+
item.resolve?.(structuredClone(silent));
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
throw new Error(result.error || `Actor run ${result.status}`);
|
|
534
606
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
607
|
+
const message = this.#outgoingMessage(actor, item, result);
|
|
608
|
+
this.#recordMessage(actor, message);
|
|
609
|
+
await this.mesh
|
|
610
|
+
.publish({
|
|
611
|
+
topic: "fabric.actor.output",
|
|
612
|
+
kind: message.action ?? "message",
|
|
613
|
+
from: { id: actor.id, name: actor.name, kind: "actor", sessionId: this.sessionId },
|
|
614
|
+
...(message.text ? { text: message.text } : {}),
|
|
615
|
+
...(message.data !== undefined ? { data: message.data } : {}),
|
|
616
|
+
})
|
|
617
|
+
.catch(() => undefined);
|
|
618
|
+
if ((message.action === "message" || message.action === "stop") &&
|
|
619
|
+
message.text &&
|
|
620
|
+
actor.delivery !== "mailbox") {
|
|
621
|
+
try {
|
|
622
|
+
this.onDeliver({
|
|
623
|
+
actor: this.#publicInfo(actor),
|
|
624
|
+
message: structuredClone(message),
|
|
625
|
+
delivery: actor.delivery,
|
|
626
|
+
triggerTurn: actor.triggerTurn,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch { /* skip non-cloneable or undeliverable message */ }
|
|
630
|
+
}
|
|
631
|
+
item.resolve?.(structuredClone(message));
|
|
632
|
+
if (message.action === "stop") {
|
|
633
|
+
actor.status = "stopped";
|
|
634
|
+
actor.queue.splice(0).forEach((queued) => queued.reject?.(new Error("Actor stopped")));
|
|
558
635
|
}
|
|
559
|
-
catch { /* skip non-cloneable or undeliverable message */ }
|
|
560
|
-
}
|
|
561
|
-
item.resolve?.(structuredClone(message));
|
|
562
|
-
if (message.action === "stop") {
|
|
563
|
-
actor.status = "stopped";
|
|
564
|
-
actor.queue.splice(0).forEach((queued) => queued.reject?.(new Error("Actor stopped")));
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
569
|
-
actor.lastError = message;
|
|
570
|
-
const failed = {
|
|
571
|
-
id: randomUUID(),
|
|
572
|
-
actorId: actor.id,
|
|
573
|
-
actorName: actor.name,
|
|
574
|
-
direction: "out",
|
|
575
|
-
source: item.source,
|
|
576
|
-
createdAt: Date.now(),
|
|
577
|
-
error: message,
|
|
578
|
-
};
|
|
579
|
-
this.#recordMessage(actor, failed);
|
|
580
|
-
item.reject?.(new Error(message));
|
|
581
|
-
}
|
|
582
|
-
finally {
|
|
583
|
-
// Retain a durable copy of the run's event log + status in the
|
|
584
|
-
// actor's directory so agents.log / /fabric log can inspect what the
|
|
585
|
-
// actor sent to and received from its model, even after a successful
|
|
586
|
-
// run cleans up the in-memory handle and tmp run directory. Failed
|
|
587
|
-
// runs stay in the subagent registry for agents.status(lastRunId).
|
|
588
|
-
if (runId) {
|
|
589
|
-
await this.#retainRunLog(actor, runId).catch(() => undefined);
|
|
590
636
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
637
|
+
catch (error) {
|
|
638
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
639
|
+
actor.lastError = message;
|
|
640
|
+
const failed = {
|
|
641
|
+
id: randomUUID(),
|
|
642
|
+
actorId: actor.id,
|
|
643
|
+
actorName: actor.name,
|
|
644
|
+
direction: "out",
|
|
645
|
+
source: item.source,
|
|
646
|
+
createdAt: Date.now(),
|
|
647
|
+
error: message,
|
|
648
|
+
};
|
|
649
|
+
this.#recordMessage(actor, failed);
|
|
650
|
+
item.reject?.(new Error(message));
|
|
595
651
|
}
|
|
596
|
-
|
|
597
|
-
|
|
652
|
+
finally {
|
|
653
|
+
// Retain a durable copy of the run's event log + status in the
|
|
654
|
+
// actor's directory so agents.log / /fabric log can inspect what the
|
|
655
|
+
// actor sent to and received from its model, even after a successful
|
|
656
|
+
// run cleans up the in-memory handle and tmp run directory. Failed
|
|
657
|
+
// runs stay in the subagent registry for agents.status(lastRunId).
|
|
658
|
+
if (runId) {
|
|
659
|
+
await this.#retainRunLog(actor, runId).catch(() => undefined);
|
|
660
|
+
}
|
|
661
|
+
// Release the in-memory handle and tmp run dir for completed runs;
|
|
662
|
+
// failed runs are retained for agents.status(actor.lastRunId).
|
|
663
|
+
if (previousRunId && previousRunId !== runId) {
|
|
664
|
+
await this.subagents.cleanup(previousRunId).catch(() => ({ cleaned: false }));
|
|
665
|
+
}
|
|
666
|
+
if (runId && runCompleted) {
|
|
667
|
+
await this.subagents.cleanup(runId).catch(() => ({ cleaned: false }));
|
|
668
|
+
}
|
|
669
|
+
delete actor.abortController;
|
|
670
|
+
actor.updatedAt = Date.now();
|
|
671
|
+
if (actor.status !== "stopped")
|
|
672
|
+
actor.status = actor.queue.length > 0 ? "queued" : "idle";
|
|
673
|
+
await this.#publishPresence(actor);
|
|
598
674
|
}
|
|
599
|
-
delete actor.abortController;
|
|
600
|
-
actor.updatedAt = Date.now();
|
|
601
|
-
if (actor.status !== "stopped")
|
|
602
|
-
actor.status = actor.queue.length > 0 ? "queued" : "idle";
|
|
603
|
-
await this.#publishPresence(actor);
|
|
604
675
|
}
|
|
605
676
|
}
|
|
677
|
+
finally {
|
|
678
|
+
// Mark the drain inactive the moment its loop exits (or throws) so a
|
|
679
|
+
// concurrent #ensureDrain observes `draining === false` and starts a
|
|
680
|
+
// fresh drain instead of stranding a just-enqueued item.
|
|
681
|
+
actor.draining = false;
|
|
682
|
+
}
|
|
606
683
|
}
|
|
607
684
|
#runRequest(actor, item) {
|
|
608
685
|
return {
|
|
@@ -678,6 +755,10 @@ export class ActorManager {
|
|
|
678
755
|
async #pollMesh() {
|
|
679
756
|
if (this.#polling || this.#closing || !this.meshConfig.enabled)
|
|
680
757
|
return;
|
|
758
|
+
// Stop-the-world: do not consume mesh events while halted, so deferred
|
|
759
|
+
// events are preserved and dispatched after the user resumes.
|
|
760
|
+
if (this.#halted)
|
|
761
|
+
return;
|
|
681
762
|
this.#polling = true;
|
|
682
763
|
try {
|
|
683
764
|
const tail = this.mesh.tail(this.#meshOffset, this.meshConfig.maxReadEvents);
|
|
@@ -888,6 +969,7 @@ export class ActorManager {
|
|
|
888
969
|
...(typeof record.timeoutMs === "number" ? { timeoutMs: record.timeoutMs } : {}),
|
|
889
970
|
sessionFile: path.join(this.#actorRoot, record.id, "session.jsonl"),
|
|
890
971
|
queue: [],
|
|
972
|
+
draining: false,
|
|
891
973
|
messages: [],
|
|
892
974
|
createdAt: record.createdAt,
|
|
893
975
|
updatedAt: Date.now(),
|