@agentex/agent 0.0.33 → 0.0.34

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.
@@ -138,8 +138,19 @@ interface TrackedBackgroundTask {
138
138
  summary: string | null;
139
139
  parentTaskId: string | null;
140
140
  terminal: boolean;
141
+ generation: number;
142
+ activeTurnId: string | null;
141
143
  }
142
144
 
145
+ interface CodexBackgroundTaskState {
146
+ terminal: boolean;
147
+ status: "running" | "completed" | "failed" | "stopped";
148
+ summary: string | null;
149
+ }
150
+
151
+ const BACKGROUND_TASK_POLL_INTERVAL_MS = 2_000;
152
+ const BACKGROUND_TASK_READ_TIMEOUT_MS = 5_000;
153
+
143
154
  /** Identity latch for the root turn currently represented by this session. */
144
155
  interface ActiveTurnReady {
145
156
  promise: Promise<string | null>;
@@ -179,6 +190,32 @@ function asObj(parent: Record<string, unknown>, key: string): Record<string, unk
179
190
  : {};
180
191
  }
181
192
 
193
+ function obj(value: unknown): Record<string, unknown> {
194
+ return typeof value === "object" && value !== null && !Array.isArray(value)
195
+ ? (value as Record<string, unknown>)
196
+ : {};
197
+ }
198
+
199
+ function stringArray(value: unknown): string[] {
200
+ return Array.isArray(value)
201
+ ? value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
202
+ : [];
203
+ }
204
+
205
+ function codexAgentState(value: unknown): CodexBackgroundTaskState {
206
+ const state = obj(value);
207
+ const nativeStatus = str(state, "status");
208
+ const summary = str(state, "message") || null;
209
+ if (nativeStatus === "completed") return { terminal: true, status: "completed", summary };
210
+ if (nativeStatus === "errored" || nativeStatus === "notFound") {
211
+ return { terminal: true, status: "failed", summary };
212
+ }
213
+ if (nativeStatus === "interrupted" || nativeStatus === "shutdown") {
214
+ return { terminal: true, status: "stopped", summary };
215
+ }
216
+ return { terminal: false, status: "running", summary };
217
+ }
218
+
182
219
  /** Discriminated incoming message from the Codex CLI. */
183
220
  type IncomingMessage =
184
221
  | { kind: "response"; id: number; result?: Record<string, unknown>; error?: { code: number; message: string } }
@@ -387,6 +424,16 @@ export class CodexSessionImpl implements AgentSession {
387
424
  /** Child-agent lifecycle is informational and never participates in root turn settlement. */
388
425
  private readonly _backgroundTasks = new Map<string, TrackedBackgroundTask>();
389
426
  private readonly _backgroundTaskIdsByPath = new Map<string, string>();
427
+ /** One protocol-native thread/read poller per child when Codex does not multiplex its notifications. */
428
+ private readonly _backgroundTaskPollers = new Map<string, {
429
+ generation: number;
430
+ promise: Promise<void>;
431
+ }>();
432
+ /** Cancellable retry delay for each poller. Cleared when the session closes. */
433
+ private readonly _backgroundTaskPollTimers = new Map<string, {
434
+ timer: ReturnType<typeof setTimeout>;
435
+ resolve: () => void;
436
+ }>();
390
437
 
391
438
  /**
392
439
  * Serial dispatch chain for `onEvent`. Each dispatched event appends a
@@ -456,7 +503,14 @@ export class CodexSessionImpl implements AgentSession {
456
503
  proc.on("exit", (code, signal) => {
457
504
  if (this._state !== "closed") {
458
505
  this._state = "closed";
459
- const err = new Error(`Codex process exited unexpectedly (code=${code}, signal=${signal})`);
506
+ const message = `Codex process exited unexpectedly (code=${code}, signal=${signal})`;
507
+ this.finalizeActiveBackgroundTasks("failed", message, {
508
+ method: "process/exit",
509
+ code,
510
+ signal,
511
+ });
512
+ this.cancelBackgroundTaskPollTimers();
513
+ const err = new Error(message);
460
514
  this.rejectAllPending(err);
461
515
  }
462
516
  });
@@ -464,11 +518,43 @@ export class CodexSessionImpl implements AgentSession {
464
518
  proc.on("error", (err) => {
465
519
  if (this._state !== "closed") {
466
520
  this._state = "closed";
521
+ this.finalizeActiveBackgroundTasks("failed", err.message, {
522
+ method: "process/error",
523
+ message: err.message,
524
+ });
525
+ this.cancelBackgroundTaskPollTimers();
467
526
  this.rejectAllPending(err);
468
527
  }
469
528
  });
470
529
  }
471
530
 
531
+ private cancelBackgroundTaskPollTimers(): void {
532
+ for (const pending of this._backgroundTaskPollTimers.values()) {
533
+ clearTimeout(pending.timer);
534
+ pending.resolve();
535
+ }
536
+ this._backgroundTaskPollTimers.clear();
537
+ }
538
+
539
+ private finalizeActiveBackgroundTasks(
540
+ status: "failed" | "stopped",
541
+ summary: string,
542
+ raw: Record<string, unknown>,
543
+ ): void {
544
+ for (const task of this._backgroundTasks.values()) {
545
+ if (task.terminal) continue;
546
+ this.dispatchEvent(this.backgroundTaskEvent(task.taskId, "completed", status, {
547
+ description: task.description,
548
+ summary,
549
+ parentTaskId: task.parentTaskId,
550
+ eventId: this._threadId
551
+ ? `codex:${this._threadId}:background-task:${task.taskId}:session-${status}:completed`
552
+ : null,
553
+ raw,
554
+ }));
555
+ }
556
+ }
557
+
472
558
  /** Reject every pending send() Promise and outgoing JSON-RPC call. */
473
559
  private rejectAllPending(err: Error): void {
474
560
  const pending = this._pendingResults.splice(0);
@@ -584,6 +670,35 @@ export class CodexSessionImpl implements AgentSession {
584
670
  });
585
671
  }
586
672
 
673
+ /** A bounded RPC whose pending-map entry is removed if Codex never replies. */
674
+ private boundedRpcRequest(
675
+ method: string,
676
+ params: Record<string, unknown>,
677
+ timeoutMs = BACKGROUND_TASK_READ_TIMEOUT_MS,
678
+ ): Promise<Record<string, unknown>> {
679
+ const id = this._nextId++;
680
+ this.proc.stdin!.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
681
+
682
+ return new Promise((resolve, reject) => {
683
+ const timer = setTimeout(() => {
684
+ if (!this._pendingRpc.delete(id)) return;
685
+ reject(new Error(`codex ${method} timed out`));
686
+ }, timeoutMs);
687
+ if (typeof timer.unref === "function") timer.unref();
688
+
689
+ this._pendingRpc.set(id, {
690
+ resolve: (result) => {
691
+ clearTimeout(timer);
692
+ resolve(result);
693
+ },
694
+ reject: (err) => {
695
+ clearTimeout(timer);
696
+ reject(err);
697
+ },
698
+ });
699
+ });
700
+ }
701
+
587
702
  /**
588
703
  * Bounded RPC for experimental, best-effort methods (the `thread/goal/*`
589
704
  * family). An app-server build that doesn't recognize the method may never
@@ -953,6 +1068,10 @@ export class CodexSessionImpl implements AgentSession {
953
1068
  async close(): Promise<void> {
954
1069
  if (this._state === "closed") return;
955
1070
  this._state = "closed";
1071
+ this.finalizeActiveBackgroundTasks("stopped", "Codex session closed", {
1072
+ method: "session/closed",
1073
+ });
1074
+ this.cancelBackgroundTaskPollTimers();
956
1075
  this.rejectAllPending(new Error("Codex session closed"));
957
1076
 
958
1077
  this.proc.stdin!.end();
@@ -1175,6 +1294,276 @@ export class CodexSessionImpl implements AgentSession {
1175
1294
  return null;
1176
1295
  }
1177
1296
 
1297
+ private backgroundTaskEvent(
1298
+ taskId: string,
1299
+ phase: "started" | "progress" | "completed",
1300
+ status: "running" | "completed" | "failed" | "stopped",
1301
+ options: {
1302
+ description?: string | null;
1303
+ summary?: string | null;
1304
+ parentTaskId?: string | null;
1305
+ turnId?: string | null;
1306
+ eventId?: string | null;
1307
+ raw: Record<string, unknown>;
1308
+ },
1309
+ ): Extract<StreamEvent, { type: "background_task" }> {
1310
+ const rootThreadId = this._threadId ?? this._expectedThreadId;
1311
+ return {
1312
+ type: "background_task",
1313
+ taskId,
1314
+ taskType: "subagent",
1315
+ phase,
1316
+ status,
1317
+ description: options.description ?? null,
1318
+ summary: options.summary ?? null,
1319
+ parentTaskId: options.parentTaskId ?? null,
1320
+ timestamp: new Date().toISOString(),
1321
+ providerType: "codex",
1322
+ sessionId: rootThreadId,
1323
+ messageId: null,
1324
+ eventId: options.eventId ?? null,
1325
+ turnId: options.turnId ?? null,
1326
+ parentToolCallId: null,
1327
+ raw: options.raw,
1328
+ };
1329
+ }
1330
+
1331
+ private reconciledBackgroundTaskRaw(
1332
+ threadId: string,
1333
+ turn: Record<string, unknown> | null,
1334
+ ): Record<string, unknown> {
1335
+ const error = turn ? str(asObj(turn, "error"), "message") || null : null;
1336
+ return {
1337
+ method: "thread/read",
1338
+ reconciled: true,
1339
+ threadId,
1340
+ turnId: turn ? str(turn, "id") || null : null,
1341
+ status: turn ? str(turn, "status") || null : null,
1342
+ error,
1343
+ };
1344
+ }
1345
+
1346
+ /**
1347
+ * Codex 0.144+ represents collaboration as a root collabAgentToolCall item.
1348
+ * A completed spawn call only means the child was created. Register every
1349
+ * receiver and then follow the child thread itself for the real terminus.
1350
+ */
1351
+ private handleCollabAgentToolCall(
1352
+ item: Record<string, unknown>,
1353
+ raw: Record<string, unknown>,
1354
+ parentTaskId: string | null,
1355
+ ): void {
1356
+ if (this._state === "closed" || !this.ctx.onEvent) return;
1357
+
1358
+ const tool = str(item, "tool");
1359
+ const states = asObj(item, "agentsStates");
1360
+ const receiverIds = stringArray(item["receiverThreadIds"]);
1361
+ const taskIds = [...new Set([...receiverIds, ...Object.keys(states)])];
1362
+ const canStart = tool === "spawnAgent";
1363
+ const metadataOnly = tool === "resumeAgent" || tool === "sendInput";
1364
+ const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
1365
+
1366
+ for (const taskId of taskIds) {
1367
+ let previous = this._backgroundTasks.get(taskId);
1368
+ const description = str(item, "prompt") || previous?.description || null;
1369
+ const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
1370
+
1371
+ // The child thread's own turn/started notification is the only
1372
+ // authoritative reactivation edge. Root resume/send calls can arrive
1373
+ // before or after that child turn, so they only enrich an active task.
1374
+ if (metadataOnly) {
1375
+ if (!previous || previous.terminal) continue;
1376
+ if (description && description !== previous.description) {
1377
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
1378
+ description,
1379
+ summary: previous.summary,
1380
+ parentTaskId: resolvedParentTaskId,
1381
+ eventId: this._threadId
1382
+ ? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
1383
+ : null,
1384
+ raw,
1385
+ }));
1386
+ }
1387
+ continue;
1388
+ }
1389
+
1390
+ if (!previous && !canStart) continue;
1391
+ if (!canStart && !canStopWithoutState && !(taskId in states)) continue;
1392
+
1393
+ const hasState = taskId in states;
1394
+ const state = !hasState && tool === "closeAgent" && str(item, "status") === "completed"
1395
+ ? { terminal: true, status: "stopped" as const, summary: null }
1396
+ : codexAgentState(states[taskId]);
1397
+
1398
+ if (!previous) {
1399
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "started", "running", {
1400
+ description,
1401
+ summary: null,
1402
+ parentTaskId: resolvedParentTaskId,
1403
+ eventId: this._threadId
1404
+ ? `codex:${this._threadId}:background-task:${taskId}:started`
1405
+ : null,
1406
+ raw,
1407
+ }));
1408
+ previous = this._backgroundTasks.get(taskId);
1409
+ } else if (previous.terminal) {
1410
+ continue;
1411
+ } else if (description && description !== previous.description) {
1412
+ // thread/started can beat the richer root collab item. Preserve one
1413
+ // start edge, then publish the prompt as ordinary progress metadata.
1414
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "progress", "running", {
1415
+ description,
1416
+ summary: previous.summary,
1417
+ parentTaskId: resolvedParentTaskId,
1418
+ eventId: this._threadId
1419
+ ? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "metadata"}:progress`
1420
+ : null,
1421
+ raw,
1422
+ }));
1423
+ }
1424
+
1425
+ if (state.terminal) {
1426
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", state.status, {
1427
+ description,
1428
+ summary: state.summary,
1429
+ parentTaskId: resolvedParentTaskId,
1430
+ eventId: this._threadId
1431
+ ? `codex:${this._threadId}:background-task:${taskId}:${str(item, "id") || "state"}:completed`
1432
+ : null,
1433
+ raw,
1434
+ }));
1435
+ } else {
1436
+ this.startBackgroundTaskPoller(taskId);
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ private waitForBackgroundTaskPoll(pollKey: string): Promise<void> {
1442
+ if (this._state === "closed") return Promise.resolve();
1443
+ return new Promise((resolve) => {
1444
+ const timer = setTimeout(() => {
1445
+ this._backgroundTaskPollTimers.delete(pollKey);
1446
+ resolve();
1447
+ }, BACKGROUND_TASK_POLL_INTERVAL_MS);
1448
+ if (typeof timer.unref === "function") timer.unref();
1449
+ this._backgroundTaskPollTimers.set(pollKey, { timer, resolve });
1450
+ });
1451
+ }
1452
+
1453
+ private startBackgroundTaskPoller(taskId: string): void {
1454
+ if (this._state === "closed") return;
1455
+ const task = this._backgroundTasks.get(taskId);
1456
+ if (!task || task.terminal) return;
1457
+ const existing = this._backgroundTaskPollers.get(taskId);
1458
+ if (existing?.generation === task.generation) return;
1459
+
1460
+ const generation = task.generation;
1461
+ const pollKey = `${taskId}:${generation}`;
1462
+ let poller!: Promise<void>;
1463
+ poller = this.pollBackgroundTask(taskId, generation, pollKey).finally(() => {
1464
+ if (this._backgroundTaskPollers.get(taskId)?.promise === poller) {
1465
+ this._backgroundTaskPollers.delete(taskId);
1466
+ }
1467
+ const pending = this._backgroundTaskPollTimers.get(pollKey);
1468
+ if (pending) clearTimeout(pending.timer);
1469
+ this._backgroundTaskPollTimers.delete(pollKey);
1470
+ });
1471
+ this._backgroundTaskPollers.set(taskId, { generation, promise: poller });
1472
+ }
1473
+
1474
+ private async pollBackgroundTask(
1475
+ taskId: string,
1476
+ generation: number,
1477
+ pollKey: string,
1478
+ ): Promise<void> {
1479
+ while (this._state !== "closed") {
1480
+ const task = this._backgroundTasks.get(taskId);
1481
+ if (!task || task.generation !== generation || task.terminal) return;
1482
+
1483
+ try {
1484
+ const response = await this.boundedRpcRequest("thread/read", {
1485
+ threadId: taskId,
1486
+ includeTurns: true,
1487
+ });
1488
+ if (this.observeBackgroundTaskThread(response, taskId, generation)) return;
1489
+ } catch {
1490
+ // A child can briefly be pendingInit before thread/read can load it.
1491
+ }
1492
+
1493
+ const current = this._backgroundTasks.get(taskId);
1494
+ if (!current || current.generation !== generation || current.terminal) return;
1495
+ await this.waitForBackgroundTaskPoll(pollKey);
1496
+ }
1497
+ }
1498
+
1499
+ /** Return true once thread/read proves that the child's latest turn ended. */
1500
+ private observeBackgroundTaskThread(
1501
+ response: Record<string, unknown>,
1502
+ taskId: string,
1503
+ generation: number,
1504
+ ): boolean {
1505
+ const task = this._backgroundTasks.get(taskId);
1506
+ if (!task || task.generation !== generation || task.terminal) return true;
1507
+
1508
+ const thread = asObj(response, "thread");
1509
+ const turns = Array.isArray(thread["turns"])
1510
+ ? thread["turns"].map(obj)
1511
+ : [];
1512
+
1513
+ const latestTurn = turns[turns.length - 1];
1514
+ if (!latestTurn) {
1515
+ const threadStatus = str(asObj(thread, "status"), "type");
1516
+ if (threadStatus !== "systemError") return false;
1517
+ const failureTurn = { status: "systemError", error: { message: "Subagent failed to initialize" } };
1518
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", "failed", {
1519
+ description: task.description,
1520
+ summary: "Subagent failed to initialize",
1521
+ parentTaskId: task.parentTaskId,
1522
+ eventId: this._threadId
1523
+ ? `codex:${this._threadId}:background-task:${taskId}:system-error:completed`
1524
+ : null,
1525
+ raw: this.reconciledBackgroundTaskRaw(taskId, failureTurn),
1526
+ }));
1527
+ return true;
1528
+ }
1529
+
1530
+ const nativeStatus = str(latestTurn, "status");
1531
+ if (nativeStatus === "inProgress" || !nativeStatus) return false;
1532
+
1533
+ const turnId = str(latestTurn, "id") || null;
1534
+ // The child turn/started notification is authoritative for a reactivated
1535
+ // generation. thread/read can briefly lag and still end at an older turn.
1536
+ if (task.activeTurnId && turnId !== task.activeTurnId) return false;
1537
+
1538
+ const items = Array.isArray(latestTurn["items"])
1539
+ ? latestTurn["items"].map(obj)
1540
+ : [];
1541
+ let summary: string | null = null;
1542
+ for (let index = items.length - 1; index >= 0; index--) {
1543
+ const item = items[index]!;
1544
+ if (str(item, "type") !== "agentMessage") continue;
1545
+ summary = this.agentMessageText(item);
1546
+ if (summary) break;
1547
+ }
1548
+ const errorMessage = str(asObj(latestTurn, "error"), "message") || null;
1549
+ const status = nativeStatus === "failed"
1550
+ ? "failed"
1551
+ : nativeStatus === "interrupted" || nativeStatus === "cancelled"
1552
+ ? "stopped"
1553
+ : "completed";
1554
+ this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", status, {
1555
+ description: task.description,
1556
+ summary: summary ?? errorMessage,
1557
+ parentTaskId: task.parentTaskId,
1558
+ turnId,
1559
+ eventId: this._threadId && turnId
1560
+ ? `codex:${this._threadId}:background-task:${taskId}:${turnId}:completed`
1561
+ : null,
1562
+ raw: this.reconciledBackgroundTaskRaw(taskId, latestTurn),
1563
+ }));
1564
+ return true;
1565
+ }
1566
+
1178
1567
  /**
1179
1568
  * Maintain just enough child metadata to turn a later foreign-thread
1180
1569
  * terminal notification into one provider-neutral task event. This reducer
@@ -1188,6 +1577,9 @@ export class CodexSessionImpl implements AgentSession {
1188
1577
  // a task that already reached a terminal state. A later child turn/started
1189
1578
  // explicitly reactivates the record below in handleForeignNotification.
1190
1579
  if (previous?.terminal) return false;
1580
+ // collabAgentToolCall and child thread/started can both announce the same
1581
+ // spawn. The first edge is sufficient and gives hosts one durable start.
1582
+ if (previous && event.phase === "started") return false;
1191
1583
 
1192
1584
  const description = event.description ?? previous?.description ?? null;
1193
1585
  const summary = event.summary ?? previous?.summary ?? null;
@@ -1205,6 +1597,10 @@ export class CodexSessionImpl implements AgentSession {
1205
1597
  summary,
1206
1598
  parentTaskId,
1207
1599
  terminal: event.phase === "completed",
1600
+ generation: previous?.generation ?? 0,
1601
+ activeTurnId: event.phase === "completed"
1602
+ ? null
1603
+ : event.turnId ?? previous?.activeTurnId ?? null,
1208
1604
  });
1209
1605
  if (description) this._backgroundTaskIdsByPath.set(description, event.taskId);
1210
1606
  return true;
@@ -1265,6 +1661,7 @@ export class CodexSessionImpl implements AgentSession {
1265
1661
  parentToolCallId: null,
1266
1662
  raw: parseJson(rawLine) ?? params,
1267
1663
  });
1664
+ this.startBackgroundTaskPoller(childThreadId);
1268
1665
  return;
1269
1666
  }
1270
1667
 
@@ -1275,6 +1672,7 @@ export class CodexSessionImpl implements AgentSession {
1275
1672
  if (!task.terminal) return;
1276
1673
  task.terminal = false;
1277
1674
  task.summary = null;
1675
+ task.generation += 1;
1278
1676
  const turn = asObj(params, "turn");
1279
1677
  const turnId = str(turn, "id") || str(params, "turnId") || null;
1280
1678
  const rootThreadId = this._threadId ?? this._expectedThreadId;
@@ -1298,12 +1696,17 @@ export class CodexSessionImpl implements AgentSession {
1298
1696
  parentToolCallId: null,
1299
1697
  raw: parseJson(rawLine) ?? params,
1300
1698
  });
1699
+ this.startBackgroundTaskPoller(childThreadId);
1301
1700
  return;
1302
1701
  }
1303
1702
 
1304
1703
  if (method === "item/completed") {
1305
1704
  const item = asObj(params, "item");
1306
1705
  const itemType = str(item, "type");
1706
+ if (itemType === "collabAgentToolCall") {
1707
+ this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, childThreadId);
1708
+ return;
1709
+ }
1307
1710
  if ((itemType === "agentMessage" || itemType === "agent_message") && str(item, "phase") !== "commentary") {
1308
1711
  const summary = this.agentMessageText(item);
1309
1712
  if (summary) task.summary = summary;
@@ -1344,11 +1747,19 @@ export class CodexSessionImpl implements AgentSession {
1344
1747
  : null,
1345
1748
  turnId,
1346
1749
  parentToolCallId: null,
1347
- raw: parseJson(rawLine) ?? params,
1750
+ raw: {
1751
+ method,
1752
+ reconciled: false,
1753
+ threadId: childThreadId,
1754
+ turnId,
1755
+ status: nativeStatus || null,
1756
+ error: errorMessage || null,
1757
+ },
1348
1758
  });
1349
1759
  }
1350
1760
 
1351
1761
  private handleNotification(method: string, params: Record<string, unknown>, rawLine: string): void {
1762
+ if (this._state === "closed") return;
1352
1763
  // codex/event — legacy wrapper
1353
1764
  if (method === "codex/event") {
1354
1765
  const innerMsg = str(params, "msg");
@@ -1394,6 +1805,11 @@ export class CodexSessionImpl implements AgentSession {
1394
1805
  if (method === "item/completed") {
1395
1806
  this._state = "thinking";
1396
1807
  this.extractSummaryFromItem(params);
1808
+ const item = asObj(params, "item");
1809
+ if (str(item, "type") === "collabAgentToolCall") {
1810
+ this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null);
1811
+ return;
1812
+ }
1397
1813
  this.emitStreamEvent(rawLine);
1398
1814
  return;
1399
1815
  }
@@ -1434,6 +1850,7 @@ export class CodexSessionImpl implements AgentSession {
1434
1850
  // -------------------------------------------------------------------------
1435
1851
 
1436
1852
  private handleLegacyEvent(event: Record<string, unknown>, rawLine: string): void {
1853
+ if (this._state === "closed") return;
1437
1854
  const type = str(event, "type");
1438
1855
  const eventThreadId =
1439
1856
  str(event, "thread_id") || str(event, "threadId") || str(event, "session_id") || null;