@workerdeck/core 0.16.0 → 0.18.0

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/build/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { getSessionInfo, getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
4
- import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, replayCoalesceKey, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
4
+ import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, contextReading, imagePartRef, replayCoalesceKey, replayRetains, snapshotRetains, transcriptActivity, transcriptContent } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
- import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { appendFileSync, existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createVfs, runScript } from "@workerdeck/sandbox";
9
9
  import { z } from "zod";
10
10
  import { lookup } from "node:dns/promises";
@@ -593,6 +593,496 @@ function staleReplaySeqs(events, afterSeq) {
593
593
  }
594
594
  return stale;
595
595
  }
596
+ /**
597
+ * The one replay body, and what a socket receives from it.
598
+ *
599
+ * Every runner had a byte-identical copy of this loop — three spellings of four
600
+ * rules, one of which ("never drop the highest-seq event, whatever the rule
601
+ * says") is load-bearing and was three copies of a comment. Not a base class:
602
+ * the runners share nothing else, and a base class would have to own `#emit`,
603
+ * the most engine-specific method each of them has.
604
+ *
605
+ * The rules, in the order they are applied:
606
+ *
607
+ * 1. `afterSeq` — the caller already holds everything at or below it.
608
+ * 2. `resetSeq` — transcript *content* strictly below the latest
609
+ * `conversation_reset` is skipped, so a re-attach cannot resurrect a cleared
610
+ * conversation while state events still replay. Claude's alone; the other
611
+ * engines pass 0.
612
+ * 3. `coalesceReplay` — last-write-wins state readings superseded later in the
613
+ * same replay (`staleReplaySeqs`), plus everything `replayRetains` says the
614
+ * reducer reads and discards. Opt-in, and only sound for a consumer whose
615
+ * handling of those events is last-write-wins.
616
+ * 4. `imageRefs` — a base64 image part is delivered as an `image_ref` address
617
+ * (protocol's {@link imagePartRef}), its bytes one REST fetch away. Applied
618
+ * **before** rule 5, because it stamps indices from the stored part array
619
+ * which rule 5 then reshapes. Unlike rule 5 this also applies to the live
620
+ * path (see `SubscriberSet`), which is the one place these two rules differ.
621
+ * 5. `truncateResults` — a huge `tool_result` block is delivered as its head
622
+ * plus the markers that say so. **Never mutates the stored event**: the live
623
+ * path, the parking snapshot and the fetch route all need the whole thing,
624
+ * so this builds a copy and the log stays the log.
625
+ *
626
+ * The highest-seq event is delivered whatever rules 2 and 3 say — a client's
627
+ * replay hold waits for `state.lastSeq` to reach the attach's and would
628
+ * otherwise hang forever — but it is still *truncated* when rule 4 applies. A
629
+ * session that ends on a `find /` puts its 641 KB frame exactly there.
630
+ */
631
+ function replaySlice(events, options) {
632
+ const { afterSeq, resetSeq = 0, coalesceReplay, truncateResults, imageRefs } = options;
633
+ const stale = coalesceReplay ? staleReplaySeqs(events, afterSeq) : void 0;
634
+ const lastSeq = events[events.length - 1]?.seq ?? 0;
635
+ const out = [];
636
+ for (const event of events) {
637
+ if (event.seq <= afterSeq) continue;
638
+ if (event.seq < resetSeq && transcriptContent(event)) continue;
639
+ if (stale?.has(event.seq)) continue;
640
+ if (coalesceReplay && event.seq !== lastSeq && !replayRetains(event)) continue;
641
+ let delivered = event;
642
+ if (imageRefs) delivered = refImageParts(delivered);
643
+ if (truncateResults) delivered = truncateResultBlocks(delivered);
644
+ out.push(delivered);
645
+ }
646
+ return out;
647
+ }
648
+ /**
649
+ * A copy of `event` whose oversized `tool_result` blocks carry their head and
650
+ * say so — or `event` itself, unchanged and un-copied, when nothing is over the
651
+ * budget. That identity matters: an attach is mostly small events, and a fresh
652
+ * object for every one of them would cost more than the feature saves.
653
+ *
654
+ * Blocks are measured and cut **individually**. A message answering three calls
655
+ * where one is a `find /` keeps the two small results whole, which is what makes
656
+ * the per-block marker (rather than a per-event one) honest.
657
+ */
658
+ function truncateResultBlocks(event) {
659
+ if (event.type !== "user_message") return event;
660
+ const content = event.message.content;
661
+ if (!Array.isArray(content)) return event;
662
+ let cut = false;
663
+ const blocks = content.map((block) => {
664
+ if (block.type !== "tool_result") return block;
665
+ const result = block;
666
+ if (result.truncated) return block;
667
+ const total = resultChars(result.content);
668
+ if (total <= TOOL_RESULT_HEAD_CHARS) return block;
669
+ cut = true;
670
+ return {
671
+ ...result,
672
+ content: headOf(result.content, TOOL_RESULT_HEAD_CHARS),
673
+ truncated: true,
674
+ total_chars: total
675
+ };
676
+ });
677
+ if (!cut) return event;
678
+ return {
679
+ ...event,
680
+ message: {
681
+ ...event.message,
682
+ content: blocks
683
+ }
684
+ };
685
+ }
686
+ /** Characters in a result's content, in the same terms a reader sees it: the
687
+ * string itself, or every text part of a block list joined by newlines — which
688
+ * is exactly what `blockText` in the reducer builds. Non-text parts (an image
689
+ * block) contribute nothing, because they are not what is large here and
690
+ * slicing them would corrupt them. */
691
+ function resultChars(content) {
692
+ if (typeof content === "string") return content.length;
693
+ if (!Array.isArray(content)) return 0;
694
+ return content.reduce((total, part, index) => total + (typeof part.text === "string" ? part.text.length + (index > 0 ? 1 : 0) : 0), 0);
695
+ }
696
+ /** The first `chars` characters, in the content's own shape — a string stays a
697
+ * string, a block list stays a block list (cut at the part that crosses the
698
+ * budget, with the remaining parts dropped). Shape-preserving on purpose: the
699
+ * reducer, both renderers and the copy button all read this the same way they
700
+ * read a whole one, so truncation is a shorter result and never a different
701
+ * kind of one. */
702
+ function headOf(content, chars) {
703
+ if (typeof content === "string") return content.slice(0, chars);
704
+ if (!Array.isArray(content)) return content;
705
+ const parts = [];
706
+ let used = 0;
707
+ for (const part of content) {
708
+ if (part.type === "image_ref") {
709
+ parts.push(part);
710
+ continue;
711
+ }
712
+ if (typeof part.text !== "string") continue;
713
+ if (used >= chars) continue;
714
+ const text = part.text.slice(0, chars - used);
715
+ parts.push({
716
+ ...part,
717
+ text
718
+ });
719
+ used += text.length + 1;
720
+ }
721
+ return parts;
722
+ }
723
+ /**
724
+ * A copy of `event` whose `tool_result` blocks carry `image_ref` addresses in
725
+ * place of their base64 image parts — or `event` itself, unchanged and
726
+ * un-copied, when it holds none. Same identity rule as
727
+ * {@link truncateResultBlocks}, and it matters more here: an event carrying an
728
+ * image at all is the exception, so the common path must not allocate.
729
+ *
730
+ * **Never mutates the stored event.** The log is what the parking snapshot
731
+ * embeds, what `Runner.eventAt` reads, and therefore what the fetch route
732
+ * serves the bytes back from — a drop that reached the log would 404 the very
733
+ * lazy-load this rule promises.
734
+ *
735
+ * Indices are stamped from the **stored** array, which is why this runs *before*
736
+ * truncation rather than after: `headOf` reshapes a block's parts, so an address
737
+ * computed on its output would name the wrong part of the stored block. That
738
+ * ordering is asserted in `replay-image-ref.test.ts`, not merely intended.
739
+ */
740
+ function refImageParts(event) {
741
+ if (event.type !== "user_message") return event;
742
+ const content = event.message.content;
743
+ if (!Array.isArray(content)) return event;
744
+ let changed = false;
745
+ const blocks = content.map((block) => {
746
+ if (block.type !== "tool_result") return block;
747
+ const result = block;
748
+ const parts = result.content;
749
+ if (!Array.isArray(parts)) return block;
750
+ let blockChanged = false;
751
+ const mapped = parts.map((part, index) => {
752
+ const ref = imagePartRef(part, index);
753
+ if (!ref) return part;
754
+ blockChanged = true;
755
+ return ref;
756
+ });
757
+ if (!blockChanged) return block;
758
+ changed = true;
759
+ return {
760
+ ...result,
761
+ content: mapped
762
+ };
763
+ });
764
+ if (!changed) return event;
765
+ return {
766
+ ...event,
767
+ message: {
768
+ ...event.message,
769
+ content: blocks
770
+ }
771
+ };
772
+ }
773
+ //#endregion
774
+ //#region src/lib/subscribers.ts
775
+ var SubscriberSet = class {
776
+ #listeners = /* @__PURE__ */ new Map();
777
+ /**
778
+ * Replay `events` to `listener` under `options`, then hold it for live
779
+ * delivery. Returns the unsubscribe.
780
+ *
781
+ * The replay runs *before* the listener joins the set, which is the ordering
782
+ * every runner already had and is load-bearing: joining first would deliver a
783
+ * live event emitted mid-replay ahead of the buffered events preceding it.
784
+ */
785
+ subscribe(events, listener, afterSeq = 0, options, resetSeq = 0) {
786
+ const asked = options ?? {};
787
+ for (const event of replaySlice(events, {
788
+ ...asked,
789
+ afterSeq,
790
+ resetSeq
791
+ })) listener(event);
792
+ this.#listeners.set(listener, asked);
793
+ return () => {
794
+ this.#listeners.delete(listener);
795
+ };
796
+ }
797
+ /** Drop every subscriber — a park, which ends the session's live stream. */
798
+ clear() {
799
+ this.#listeners.clear();
800
+ }
801
+ /** Fan one event out, transformed per subscriber. */
802
+ emit(event) {
803
+ for (const [listener, asked] of this.#listeners) try {
804
+ listener(asked.imageRefs ? refImageParts(event) : event);
805
+ } catch {}
806
+ }
807
+ };
808
+ //#endregion
809
+ //#region src/engines/claude/subagents.ts
810
+ /**
811
+ * The rollup behind `SessionInfo.subagents` — what a sessions list (which never
812
+ * attaches) can know about the sub-agents running inside a session. Fed from
813
+ * `SessionRunner.#emit`, the one chokepoint every event passes through, so the
814
+ * resume backfill — which replays history through the same path — reconstructs
815
+ * it with no persistence of its own. Grouping is by Task id throughout, never
816
+ * adjacency: parallel sub-agents interleave in the stream, the same fact that
817
+ * broke the terminal theme's positional row model.
818
+ *
819
+ * Three decisions live here rather than in the protocol doc:
820
+ *
821
+ * **What counts as a spawn.** A record opens when a *top-level* assistant
822
+ * message carries a `tool_use` named `Task` or `Agent` — the moment the
823
+ * sub-agent exists, so a just-spawned agent is visible before its first nested
824
+ * event, with the block's input in hand for its labels. Both names are observed
825
+ * SDK spellings (`Task` synchronous, `Agent` async), and the name is a
826
+ * convention, not a law — a session that spawned three `Agent`s under a tracker
827
+ * that only knew `Task` reported all three as label-less failures. So three
828
+ * more openers back the allowlist up: the CLI's own `task_started` system event
829
+ * (which positively names the `tool_use_id` an agent runs under, with the brief
830
+ * as labels), the launch acknowledgement (below), and — as before — any nested
831
+ * event whose `parentToolUseId` has no record: an id that events demonstrably
832
+ * nest under *is* a sub-agent, whatever the spawning call was named. A fallback
833
+ * record never saw an input, so it stays label-less until a named signal fills
834
+ * it in rather than resetting an accumulated count.
835
+ *
836
+ * **A background agent's `tool_result` is a launch receipt, not a verdict.**
837
+ * An async agent's spawn call resolves seconds after the spawn with "Async
838
+ * agent launched successfully. (This tool result is internal metadata …)" —
839
+ * long before the agent has done anything — and its actual outcome travels on
840
+ * a `task_notification` system event instead (`status: 'completed'` is `done`,
841
+ * any other way of stopping is `failed`: the report the notification exists to
842
+ * deliver never came). Settling on the receipt would read "0 of 3 agents
843
+ * running" while three agents burn tokens, so a non-error result on a record
844
+ * known to be background never settles it. Known how: the `task_started` event
845
+ * live, or the receipt's own wrapper text on a resume — the stored transcript
846
+ * carries none of the CLI's system events, so, exactly as
847
+ * `isSyntheticUserText` documents for the `<task-notification>` blob, the text
848
+ * is the only signal the replayed path has.
849
+ *
850
+ * **What an interrupted turn leaves behind.** A Task whose `tool_result` never
851
+ * arrives — interrupt, session error, a turn or budget cap — would otherwise
852
+ * read `running` on an idle session forever, a lie a list re-renders at every
853
+ * poll. So the end of a turn settles every still-running record as `failed`:
854
+ * the report never came, which is the one thing `done` could have claimed. The
855
+ * sweep keys on `turn_result`, on the status coming to rest (`idle` — which is
856
+ * how a resumed history that ends mid-Task settles, since the backfill replays
857
+ * no `turn_result` — or a terminal state), and on the session closing. A real
858
+ * verdict arriving anyway outranks the sweep's inference. The sweep's premise
859
+ * — "the turn ended, so anything still running was cut off" — is false for a
860
+ * background agent, which is *designed* to outlive its turn: the real session
861
+ * behind this file ended three turns while its agents ran, and every
862
+ * `turn_result` re-branded live, working agents as failures. So the turn and
863
+ * idle sweeps spare a record marked background by a **live** signal. They do
864
+ * not spare one whose only evidence is replayed: the backfill describes a
865
+ * process that is gone, and a background agent the old process died inside can
866
+ * never notify — `running` would be the forever-lie again. `session_closed`
867
+ * and the terminal statuses settle everything, background included, for the
868
+ * same reason: the process hosting those agents is gone.
869
+ */
870
+ var SubagentTracker = class {
871
+ #records = /* @__PURE__ */ new Map();
872
+ #settleCounter = 0;
873
+ /** Fold one emitted event body into the rollup, in log order. */
874
+ observe(body, ts) {
875
+ switch (body.type) {
876
+ case "assistant_message":
877
+ if (body.parentToolUseId != null) {
878
+ const record = this.#recordFor(body.parentToolUseId, ts);
879
+ record.toolCount += toolUseBlocks(body.message.content).length;
880
+ return;
881
+ }
882
+ for (const block of toolUseBlocks(body.message.content)) {
883
+ if (!SPAWNER_NAMES.has(block.name)) continue;
884
+ this.#open(block, ts);
885
+ }
886
+ return;
887
+ case "user_message": {
888
+ if (body.parentToolUseId != null) {
889
+ this.#recordFor(body.parentToolUseId, ts);
890
+ return;
891
+ }
892
+ const note = parseTaskNotification(firstText(body.message.content));
893
+ if (note) {
894
+ const record = this.#recordFor(note.toolUseId, ts);
895
+ const status = note.status === "completed" ? "done" : "failed";
896
+ if (record.status !== status) this.#settle(record, status);
897
+ return;
898
+ }
899
+ const content = body.message.content;
900
+ if (typeof content === "string") return;
901
+ for (const block of content) {
902
+ if (block.type !== "tool_result") continue;
903
+ const result = block;
904
+ if (typeof result.tool_use_id !== "string") continue;
905
+ if (result.is_error !== true && isLaunchAck(result.content)) {
906
+ const record = this.#recordFor(result.tool_use_id, ts);
907
+ if (record.background !== "live") record.background = body.replay === true ? "replay" : "live";
908
+ continue;
909
+ }
910
+ const record = this.#records.get(result.tool_use_id);
911
+ if (!record) continue;
912
+ if (result.is_error !== true && record.background !== void 0) continue;
913
+ const status = result.is_error === true ? "failed" : "done";
914
+ if (record.status === status) continue;
915
+ this.#settle(record, status);
916
+ }
917
+ return;
918
+ }
919
+ case "sdk_event": {
920
+ const p = body.payload;
921
+ if (p.type !== "system" || typeof p.tool_use_id !== "string") return;
922
+ if (p.subtype === "task_started") {
923
+ const record = this.#recordFor(p.tool_use_id, ts);
924
+ record.background = "live";
925
+ record.agentType ??= cleaned(p.subagent_type);
926
+ record.description ??= cleaned(p.description);
927
+ return;
928
+ }
929
+ if (p.subtype === "task_notification") {
930
+ const record = this.#recordFor(p.tool_use_id, ts);
931
+ const status = p.status === "completed" ? "done" : "failed";
932
+ if (record.status !== status) this.#settle(record, status);
933
+ return;
934
+ }
935
+ return;
936
+ }
937
+ case "turn_result":
938
+ this.#sweep(false);
939
+ return;
940
+ case "session_closed":
941
+ this.#sweep(true);
942
+ return;
943
+ case "status_changed":
944
+ if (body.status === "idle") this.#sweep(false);
945
+ else if (body.status === "failed" || body.status === "closed") this.#sweep(true);
946
+ return;
947
+ case "conversation_reset":
948
+ this.#records.clear();
949
+ return;
950
+ default: return;
951
+ }
952
+ }
953
+ /**
954
+ * The rollup as `SessionInfo.subagents` serves it: spawn order (the
955
+ * transcript's own), fresh objects, and `undefined` when there is nothing to
956
+ * say — absent and empty mean the same thing to a client, and an empty array
957
+ * on every row of a 1.2s-polled list is bytes spent saying nothing.
958
+ */
959
+ list() {
960
+ if (this.#records.size === 0) return void 0;
961
+ const out = [];
962
+ for (const r of this.#records.values()) out.push({
963
+ toolUseId: r.toolUseId,
964
+ agentType: r.agentType,
965
+ description: r.description,
966
+ status: r.status,
967
+ startedAt: r.startedAt,
968
+ toolCount: r.toolCount
969
+ });
970
+ return out;
971
+ }
972
+ #recordFor(toolUseId, ts) {
973
+ let record = this.#records.get(toolUseId);
974
+ if (!record) {
975
+ record = {
976
+ toolUseId,
977
+ status: "running",
978
+ startedAt: ts,
979
+ toolCount: 0
980
+ };
981
+ this.#records.set(toolUseId, record);
982
+ }
983
+ return record;
984
+ }
985
+ #open(block, ts) {
986
+ const record = this.#recordFor(block.id, ts);
987
+ const input = block.input;
988
+ record.agentType ??= cleaned(input?.subagent_type);
989
+ record.description ??= cleaned(input?.description);
990
+ }
991
+ /**
992
+ * End of turn (`final: false`): anything still running was cut off before
993
+ * its report — except a background agent the live process still hosts, which
994
+ * is designed to outlive the turn and settles by notification instead. End
995
+ * of session (`final: true`): everything, background included, because the
996
+ * process those agents lived in is gone.
997
+ */
998
+ #sweep(final) {
999
+ for (const record of this.#records.values()) {
1000
+ if (record.status !== "running") continue;
1001
+ if (!final && record.background === "live") continue;
1002
+ this.#settle(record, "failed");
1003
+ }
1004
+ }
1005
+ #settle(record, status) {
1006
+ record.status = status;
1007
+ record.settledOrder = ++this.#settleCounter;
1008
+ let settled = 0;
1009
+ for (const r of this.#records.values()) if (r.settledOrder !== void 0) settled++;
1010
+ while (settled > SUBAGENT_HISTORY) {
1011
+ let oldestId;
1012
+ let oldestOrder = Infinity;
1013
+ for (const r of this.#records.values()) {
1014
+ if (r.settledOrder === void 0 || r.settledOrder >= oldestOrder) continue;
1015
+ oldestId = r.toolUseId;
1016
+ oldestOrder = r.settledOrder;
1017
+ }
1018
+ if (oldestId === void 0) break;
1019
+ this.#records.delete(oldestId);
1020
+ settled--;
1021
+ }
1022
+ }
1023
+ };
1024
+ /** The spawner names observed in the wild: `Task` runs the agent inside the
1025
+ * turn, `Agent` launches it in the background. Deliberately just these two —
1026
+ * a third spelling is caught by `task_started`, the launch receipt, or the
1027
+ * nested-event fallback, so widening this to every tool would only turn
1028
+ * ordinary calls into phantom agents. */
1029
+ const SPAWNER_NAMES = new Set(["Task", "Agent"]);
1030
+ /** The async spawn's immediate `tool_result` — "Async agent launched
1031
+ * successfully. (This tool result is internal metadata …)" — recognized by its
1032
+ * wrapper text because on a resume that text is the only signal there is (the
1033
+ * `SYNTHETIC_USER_PREFIXES` argument; the CLI's system events are not stored).
1034
+ * Live, `task_started` marks the record first and this is redundant armor. */
1035
+ const isLaunchAck = (content) => {
1036
+ const text = typeof content === "string" ? content : firstText(Array.isArray(content) ? content : []);
1037
+ return typeof text === "string" && text.trimStart().startsWith("Async agent launched");
1038
+ };
1039
+ /** A background agent stopping, parsed from the `<task-notification>` wrapper
1040
+ * the CLI writes into the transcript. Field-tolerant on purpose: only the
1041
+ * `tool-use-id` (this rollup's key) and the `status` verdict are read. */
1042
+ const parseTaskNotification = (text) => {
1043
+ if (text === void 0 || !text.trimStart().startsWith("<task-notification>")) return void 0;
1044
+ const toolUseId = /<tool-use-id>\s*([^<\s]+)\s*<\/tool-use-id>/.exec(text)?.[1];
1045
+ if (toolUseId === void 0) return void 0;
1046
+ return {
1047
+ toolUseId,
1048
+ status: /<status>\s*([^<]*?)\s*<\/status>/.exec(text)?.[1] ?? ""
1049
+ };
1050
+ };
1051
+ /** The first text of a message body, however the content is spelled — the
1052
+ * stored transcript uses bare strings, the live stream uses blocks. */
1053
+ const firstText = (content) => {
1054
+ if (typeof content === "string") return content;
1055
+ for (const block of content) {
1056
+ const b = block;
1057
+ if (b?.type === "text" && typeof b.text === "string") return b.text;
1058
+ }
1059
+ };
1060
+ /** Trim, drop blank, clip at the same 80 the terminal theme's `taskLabel` uses.
1061
+ * Model-authored input rides every row of a polled sessions list, so it is
1062
+ * bounded here rather than trusted — a 10KB `description` would be paid for at
1063
+ * every poll. */
1064
+ const cleaned = (value) => {
1065
+ if (typeof value !== "string") return void 0;
1066
+ const text = value.trim();
1067
+ if (text === "") return void 0;
1068
+ return text.length > 80 ? text.slice(0, 79) + "…" : text;
1069
+ };
1070
+ /** The `tool_use` blocks of a message body, however the content is spelled. */
1071
+ function toolUseBlocks(content) {
1072
+ if (typeof content === "string") return [];
1073
+ const blocks = [];
1074
+ for (const block of content) {
1075
+ if (block.type !== "tool_use") continue;
1076
+ const b = block;
1077
+ if (typeof b.id !== "string" || typeof b.name !== "string") continue;
1078
+ blocks.push({
1079
+ id: b.id,
1080
+ name: b.name,
1081
+ input: b.input
1082
+ });
1083
+ }
1084
+ return blocks;
1085
+ }
596
1086
  //#endregion
597
1087
  //#region src/engines/claude/runner.ts
598
1088
  const DEFAULT_APPROVAL_TIMEOUT_MS$1 = 3e5;
@@ -608,8 +1098,15 @@ var SessionRunner = class {
608
1098
  /** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
609
1099
  #cwd;
610
1100
  #events = [];
611
- #listeners = /* @__PURE__ */ new Set();
1101
+ #subscribers = new SubscriberSet();
612
1102
  #seq = 0;
1103
+ /**
1104
+ * Latest context-window reading, retained from the last `context_usage` this
1105
+ * runner emitted so `GET /sessions` can answer it without an attach — see
1106
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1107
+ * construction the same number the transcript last drew.
1108
+ */
1109
+ #contextUsage;
613
1110
  #activityCount = 0;
614
1111
  /**
615
1112
  * Seq of the latest `conversation_reset` event, 0 when none. The log itself is
@@ -627,6 +1124,28 @@ var SessionRunner = class {
627
1124
  #apiKeySource;
628
1125
  #permissionMode;
629
1126
  #pending = /* @__PURE__ */ new Map();
1127
+ /**
1128
+ * The turn ended while an approval was standing, and nothing has started a
1129
+ * new one since.
1130
+ *
1131
+ * `awaiting_approval` rightly outranks `idle` for display, so a turn-over
1132
+ * signal arriving under a standing approval cannot be applied when it lands.
1133
+ * It used to be **discarded** for that reason, which is a different thing
1134
+ * from outranked: the settle path then asserted `running` on the assumption
1135
+ * that an answered approval means work resumes, and when the turn was already
1136
+ * over — an interrupt, a timeout — the session claimed to be running one that
1137
+ * had produced its result. Status is purely edge-driven here, with no poll and
1138
+ * no reconciliation anywhere, so that single dropped edge never came back and
1139
+ * every client rendered it faithfully for the life of the session.
1140
+ *
1141
+ * So the fact is *deferred* rather than dropped, and it is deliberately
1142
+ * cleared the moment work genuinely resumes — a turn-over belongs to the turn
1143
+ * that produced it and must not settle the next one.
1144
+ */
1145
+ #turnOverWhileBlocked = false;
1146
+ /** The read-time sub-agent rollup (`SessionInfo.subagents`), fed from #emit —
1147
+ * the one chokepoint — so the resume backfill reconstructs it for free. */
1148
+ #subagents = new SubagentTracker();
630
1149
  #totalCostUsd;
631
1150
  #numTurns;
632
1151
  #lastActivityAt;
@@ -682,7 +1201,9 @@ var SessionRunner = class {
682
1201
  createdAt: this.createdAt,
683
1202
  lastSeq: this.#seq,
684
1203
  activityCount: this.#activityCount,
1204
+ contextUsage: this.#contextUsage,
685
1205
  pendingPermissionCount: this.#pending.size,
1206
+ subagents: this.#subagents.list(),
686
1207
  meta: this.#config.meta,
687
1208
  scope: this.#config.scope,
688
1209
  title: this.#title(),
@@ -831,6 +1352,12 @@ var SessionRunner = class {
831
1352
  });
832
1353
  this.#setStatus("closed");
833
1354
  }
1355
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
1356
+ * "show everything" on one row, so a per-runner seq index would be a map
1357
+ * maintained on every emit to save a walk nobody makes twice a minute. */
1358
+ eventAt(seq) {
1359
+ return this.#events.find((event) => event.seq === seq);
1360
+ }
834
1361
  /**
835
1362
  * Replay buffered events with seq > afterSeq, then deliver live events.
836
1363
  * Returns an unsubscribe function.
@@ -845,15 +1372,7 @@ var SessionRunner = class {
845
1372
  * content below the newer one and are skipped with what they cleared.
846
1373
  */
847
1374
  subscribe(listener, afterSeq = 0, options) {
848
- const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
849
- for (const event of this.#events) {
850
- if (event.seq <= afterSeq) continue;
851
- if (event.seq < this.#resetSeq && transcriptContent(event)) continue;
852
- if (stale?.has(event.seq)) continue;
853
- listener(event);
854
- }
855
- this.#listeners.add(listener);
856
- return () => this.#listeners.delete(listener);
1375
+ return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
857
1376
  }
858
1377
  async #run() {
859
1378
  const queryFn = this.#config.queryFn ?? query;
@@ -944,6 +1463,7 @@ var SessionRunner = class {
944
1463
  forkSession: c.forkSession,
945
1464
  effort: c.reasoningEffort,
946
1465
  includePartialMessages: c.includePartialMessages ?? true,
1466
+ forwardSubagentText: true,
947
1467
  canUseTool: this.#canUseTool,
948
1468
  env: c.env,
949
1469
  pathToClaudeCodeExecutable: c.pathToClaudeCodeExecutable,
@@ -970,6 +1490,7 @@ var SessionRunner = class {
970
1490
  claudeCodeVersion: msg.claude_code_version,
971
1491
  mcpServers: msg.mcp_servers
972
1492
  });
1493
+ this.#turnOverWhileBlocked = false;
973
1494
  this.#setStatus("running");
974
1495
  this.#fetchCapabilities();
975
1496
  this.#fetchContextUsage();
@@ -978,7 +1499,11 @@ var SessionRunner = class {
978
1499
  return;
979
1500
  }
980
1501
  if (msg.type === "system" && msg.subtype === "session_state_changed") {
981
- if (this.#pending.size > 0) return;
1502
+ if (this.#pending.size > 0) {
1503
+ if (msg.state === "idle") this.#turnOverWhileBlocked = true;
1504
+ else if (msg.state === "running") this.#turnOverWhileBlocked = false;
1505
+ return;
1506
+ }
982
1507
  if (msg.state === "idle") this.#setStatus("idle");
983
1508
  else if (msg.state === "running") this.#setStatus("running");
984
1509
  return;
@@ -994,6 +1519,7 @@ var SessionRunner = class {
994
1519
  this.#totalCostUsd = body.totalCostUsd;
995
1520
  this.#numTurns = body.numTurns;
996
1521
  if (this.#pending.size === 0) this.#setStatus("idle");
1522
+ else this.#turnOverWhileBlocked = true;
997
1523
  this.#fetchContextUsage();
998
1524
  this.#fetchRateLimits();
999
1525
  this.#fetchEngineTitle();
@@ -1224,7 +1750,12 @@ var SessionRunner = class {
1224
1750
  resolvedBy,
1225
1751
  message: decision.behavior === "deny" ? decision.message ?? "Denied" : void 0
1226
1752
  });
1227
- if (this.#pending.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
1753
+ if (this.#pending.size === 0) {
1754
+ const endedWhileBlocked = this.#turnOverWhileBlocked;
1755
+ this.#turnOverWhileBlocked = false;
1756
+ if (endedWhileBlocked) this.#setStatus("idle");
1757
+ else if (this.#status === "awaiting_approval") this.#setStatus("running");
1758
+ }
1228
1759
  }
1229
1760
  #setStatus(status, detail) {
1230
1761
  if (this.#status === status && this.#statusDetail === detail) return;
@@ -1245,11 +1776,14 @@ var SessionRunner = class {
1245
1776
  };
1246
1777
  this.#lastActivityAt = event.ts;
1247
1778
  this.#activityCount += transcriptActivity(body);
1248
- if (body.type === "conversation_reset") this.#resetSeq = event.seq;
1779
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
1780
+ if (body.type === "conversation_reset") {
1781
+ this.#resetSeq = event.seq;
1782
+ this.#contextUsage = void 0;
1783
+ }
1784
+ this.#subagents.observe(body, event.ts);
1249
1785
  this.#events.push(event);
1250
- for (const listener of this.#listeners) try {
1251
- listener(event);
1252
- } catch {}
1786
+ this.#subscribers.emit(event);
1253
1787
  }
1254
1788
  };
1255
1789
  /** Answer each AskUserQuestion question with its first option's label — the tool's
@@ -1292,8 +1826,15 @@ var AiSdkRunner = class {
1292
1826
  #config;
1293
1827
  #model;
1294
1828
  #events = [];
1295
- #listeners = /* @__PURE__ */ new Set();
1829
+ #subscribers = new SubscriberSet();
1296
1830
  #seq = 0;
1831
+ /**
1832
+ * Latest context-window reading, retained from the last `context_usage` this
1833
+ * runner emitted so `GET /sessions` can answer it without an attach — see
1834
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
1835
+ * construction the same number the transcript last drew.
1836
+ */
1837
+ #contextUsage;
1297
1838
  #activityCount = 0;
1298
1839
  #status = "starting";
1299
1840
  #permissionMode;
@@ -1343,7 +1884,12 @@ var AiSdkRunner = class {
1343
1884
  if (!state || !Array.isArray(state.messages)) throw new Error("session snapshot is missing its provider-engine state");
1344
1885
  this.#seq = snapshot.seq;
1345
1886
  this.#events = [...snapshot.events];
1346
- this.#activityCount = this.#events.reduce((total, event) => total + transcriptActivity(event), 0);
1887
+ this.#activityCount = 0;
1888
+ for (const event of this.#events) {
1889
+ this.#activityCount += transcriptActivity(event);
1890
+ if (event.type === "conversation_reset") this.#contextUsage = void 0;
1891
+ else this.#contextUsage = contextReading(event) ?? this.#contextUsage;
1892
+ }
1347
1893
  this.#messages = [...state.messages];
1348
1894
  for (const call of state.pendingToolCalls) this.#pendingToolCalls.set(call.toolCallId, call);
1349
1895
  this.#dispatched = new Set(state.dispatched);
@@ -1394,6 +1940,7 @@ var AiSdkRunner = class {
1394
1940
  createdAt: this.createdAt,
1395
1941
  lastSeq: this.#seq,
1396
1942
  activityCount: this.#activityCount,
1943
+ contextUsage: this.#contextUsage,
1397
1944
  pendingPermissionCount: 0,
1398
1945
  meta: this.#config.meta,
1399
1946
  scope: this.#config.scope,
@@ -1405,10 +1952,7 @@ var AiSdkRunner = class {
1405
1952
  start() {
1406
1953
  if (this.#started) return this.#turnChain;
1407
1954
  this.#started = true;
1408
- if (this.#config.restore) {
1409
- if (this.#pendingToolCalls.size === 0) this.#scheduleTurn();
1410
- return this.#turnChain;
1411
- }
1955
+ if (this.#config.restore) return this.#turnChain;
1412
1956
  this.#setStatus("idle");
1413
1957
  if (this.#config.prompt) this.sendMessage(this.#config.prompt);
1414
1958
  return this.#turnChain;
@@ -1423,6 +1967,61 @@ var AiSdkRunner = class {
1423
1967
  if (this.#closed || this.#parked) return void 0;
1424
1968
  if (this.#abort || !this.#restingOnDeferred()) return void 0;
1425
1969
  this.#setStatus("parked");
1970
+ const snapshot = this.#buildSnapshot();
1971
+ this.#parked = true;
1972
+ this.#subscribers.clear();
1973
+ try {
1974
+ Promise.resolve(this.#config.onClose?.()).catch(() => {});
1975
+ } catch {}
1976
+ return snapshot;
1977
+ }
1978
+ /**
1979
+ * The same snapshot, taken without ending anything.
1980
+ *
1981
+ * `park()` and this are two operations that happen to produce the same value,
1982
+ * and the difference is the whole point: `park()` *ends* the live runner
1983
+ * (inert, listeners dropped, `onClose` called), which is right for deferred
1984
+ * execution — the session has nothing to do for possibly days — and wrong for
1985
+ * restart-survival, where the session is active and someone is mid-
1986
+ * conversation. This one changes nothing at all: no status emit, no listener
1987
+ * clear, no disposer. The host writes the value through to durable storage
1988
+ * after each turn and keeps the runner live and warm, so a restart rebuilds
1989
+ * from the last write through the existing `restore` path and the next message
1990
+ * costs no wake.
1991
+ *
1992
+ * The gate is `park()`'s minus the requirement that there be something parked:
1993
+ *
1994
+ * - `#abort` set is refused for the reason it always was — a `generate()` in
1995
+ * flight has produced messages that are not in the history yet, so the
1996
+ * snapshot would be of a turn that half-happened.
1997
+ * - Pending calls that are **not** all deferred are refused, which is
1998
+ * `park()`'s rule wearing a different hat. An in-process execution's result
1999
+ * is coming back to *this* runner and dies with the process; a restore would
2000
+ * wait on it forever, and `state.dispatched` is what would stop the rebuilt
2001
+ * runner from simply calling it again.
2002
+ * - Idle with nothing pending — the case `park()` exists to refuse — is
2003
+ * exactly the case this exists to allow.
2004
+ */
2005
+ snapshot() {
2006
+ if (this.#closed || this.#parked || this.#abort) return void 0;
2007
+ if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return void 0;
2008
+ return this.#buildSnapshot();
2009
+ }
2010
+ /**
2011
+ * The snapshot value itself, shared so a park and a write-through cannot
2012
+ * disagree about what a session *is*.
2013
+ *
2014
+ * The event log is filtered through {@link snapshotRetains} — the persisted
2015
+ * log drops stream deltas, which are superseded by the `assistant_message`
2016
+ * that flushes them and would otherwise be tens of times the size of the text
2017
+ * they spell. Parks get it too, and should: a park sits on disk for days.
2018
+ *
2019
+ * The `parked` list and `state.parkedAt` are honest under both callers. An
2020
+ * idle write-through has no pending calls, so `parked` is empty and the host
2021
+ * arms no watchdogs; `parkedAt` is "when this was taken", which is what
2022
+ * `#restore` needs to discount a turn's clock either way.
2023
+ */
2024
+ #buildSnapshot() {
1426
2025
  const parked = [...this.#pendingToolCalls.values()].map((call) => ({
1427
2026
  executionId: call.toolCallId,
1428
2027
  toolName: call.toolName,
@@ -1440,22 +2039,16 @@ var AiSdkRunner = class {
1440
2039
  lastActivityAt: this.#lastActivityAt,
1441
2040
  parkedAt: Date.now()
1442
2041
  };
1443
- const snapshot = {
2042
+ return {
1444
2043
  engine: "provider",
1445
2044
  id: this.id,
1446
2045
  createdAt: this.createdAt,
1447
2046
  seq: this.#seq,
1448
- events: [...this.#events],
2047
+ events: this.#events.filter((event) => snapshotRetains(event)),
1449
2048
  vfs: this.#config.vfs?.snapshot(),
1450
2049
  parked,
1451
2050
  state
1452
2051
  };
1453
- this.#parked = true;
1454
- this.#listeners.clear();
1455
- try {
1456
- Promise.resolve(this.#config.onClose?.()).catch(() => {});
1457
- } catch {}
1458
- return snapshot;
1459
2052
  }
1460
2053
  sendMessage(text, attachments) {
1461
2054
  if (this.#parked) throw new Error("session is parked");
@@ -1638,15 +2231,14 @@ var AiSdkRunner = class {
1638
2231
  Promise.resolve(this.#config.onClose?.()).catch(() => {});
1639
2232
  } catch {}
1640
2233
  }
2234
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
2235
+ * "show everything" on one row, so a per-runner seq index would be a map
2236
+ * maintained on every emit to save a walk nobody makes twice a minute. */
2237
+ eventAt(seq) {
2238
+ return this.#events.find((event) => event.seq === seq);
2239
+ }
1641
2240
  subscribe(listener, afterSeq = 0, options) {
1642
- const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
1643
- for (const event of this.#events) {
1644
- if (event.seq <= afterSeq) continue;
1645
- if (stale?.has(event.seq)) continue;
1646
- listener(event);
1647
- }
1648
- this.#listeners.add(listener);
1649
- return () => this.#listeners.delete(listener);
2241
+ return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
1650
2242
  }
1651
2243
  #scheduleTurn() {
1652
2244
  this.#turnChain = this.#turnChain.then(() => this.#runTurn());
@@ -2035,10 +2627,10 @@ var AiSdkRunner = class {
2035
2627
  };
2036
2628
  this.#lastActivityAt = event.ts;
2037
2629
  this.#activityCount += transcriptActivity(body);
2630
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
2631
+ if (body.type === "conversation_reset") this.#contextUsage = void 0;
2038
2632
  this.#events.push(event);
2039
- for (const listener of this.#listeners) try {
2040
- listener(event);
2041
- } catch {}
2633
+ this.#subscribers.emit(event);
2042
2634
  }
2043
2635
  };
2044
2636
  function turnUsage(accum) {
@@ -3257,8 +3849,6 @@ const claudeAdapter = {
3257
3849
  }));
3258
3850
  }
3259
3851
  };
3260
- //#endregion
3261
- //#region src/engines/codex/jsonrpc.ts
3262
3852
  /**
3263
3853
  * A JSON-RPC error response from the peer, or one we return to it. `code`
3264
3854
  * follows the JSON-RPC 2.0 reserved ranges (-32601 = method not found).
@@ -3290,9 +3880,12 @@ var JsonRpcStdioConnection = class {
3290
3880
  #buffer = "";
3291
3881
  #closed = false;
3292
3882
  #notificationHandler;
3883
+ /** Where {@link CODEX_TRACE_ENV} pointed, or undefined — read once. */
3884
+ #trace;
3293
3885
  #requestHandler;
3294
3886
  constructor(options) {
3295
3887
  this.#output = options.output;
3888
+ this.#trace = process.env["WORKERDECK_CODEX_TRACE"] || void 0;
3296
3889
  options.input.on("data", (chunk) => this.#feed(String(chunk)));
3297
3890
  options.input.on("error", () => {});
3298
3891
  options.output.on("error", () => {});
@@ -3353,9 +3946,28 @@ var JsonRpcStdioConnection = class {
3353
3946
  } catch {
3354
3947
  continue;
3355
3948
  }
3949
+ this.#traceLine(message);
3356
3950
  this.#dispatch(message);
3357
3951
  }
3358
3952
  }
3953
+ /**
3954
+ * Append one inbound message to the trace file, when the operator asked for
3955
+ * one. **Notifications and server→client requests only** — a response body is
3956
+ * not needed to answer the questions this exists for, and `account/*` results
3957
+ * are the one place app-server traffic can carry a masked credential
3958
+ * fragment, which nothing of ours writes to disk (see the auth red lines).
3959
+ * Best-effort and synchronous: a debug sink that loses lines proves nothing,
3960
+ * and a debug sink that throws must not take the session with it.
3961
+ */
3962
+ #traceLine(message) {
3963
+ if (!this.#trace) return;
3964
+ const method = message.method;
3965
+ if (typeof method !== "string") return;
3966
+ if (method.startsWith("account/") || method.startsWith("login")) return;
3967
+ try {
3968
+ appendFileSync(this.#trace, JSON.stringify(message) + "\n");
3969
+ } catch {}
3970
+ }
3359
3971
  #dispatch(message) {
3360
3972
  const { id, method } = message;
3361
3973
  if (typeof method === "string") {
@@ -3394,6 +4006,117 @@ var JsonRpcStdioConnection = class {
3394
4006
  }
3395
4007
  };
3396
4008
  //#endregion
4009
+ //#region src/engines/codex/subagents.ts
4010
+ /**
4011
+ * The codex side of `SessionInfo.subagents` — and the attribution table that
4012
+ * gives every event a spawned agent produces its `parentToolUseId`.
4013
+ *
4014
+ * Codex's signal is stronger than the claude engine's, so this is deliberately
4015
+ * NOT that tracker generalised (`engines/claude/subagents.ts` infers spawns
4016
+ * from tool names and verdicts from result-text sniffing, ~290 lines of module
4017
+ * doc explaining the inference). Here nothing is inferred: `subAgentActivity
4018
+ * {kind: 'started'}` on the owning thread positively announces an agent, names
4019
+ * it (`agentPath`), keys it (`agentThreadId` — the id every one of its later
4020
+ * notifications carries) and hands over the model's own `spawn_agent` call id;
4021
+ * the agent's end is its own thread's `turn/completed`, status included. So a
4022
+ * record is keyed by **thread id** — the wire's handle — while exposing a
4023
+ * **tool-use id** — the protocol's: `parentToolUseId` on nested events must
4024
+ * equal the anchor `tool_use`'s id for `subagentItems` (the frame membership
4025
+ * rule every client shares) to reassemble the sidechain, and this map is where
4026
+ * the two vocabularies meet.
4027
+ *
4028
+ * Two decisions worth their prose:
4029
+ *
4030
+ * **A record survives the runner's turns.** Codex agents are designed to
4031
+ * outlive the root turn that spawned them (`sendInput`/`resumeAgent` address a
4032
+ * thread that kept existing), so — unlike a pending approval — nothing here is
4033
+ * swept when a root turn ends. What does end every agent is the app-server
4034
+ * process itself: the runner calls {@link sweep} when the child dies or the
4035
+ * session closes, because an agent whose host process is gone can never report,
4036
+ * and `running` on a closed session would be a lie a polled list re-renders
4037
+ * forever (the claude tracker's argument, inherited whole).
4038
+ *
4039
+ * **The settled tail is bounded, running records never are** — the same
4040
+ * {@link SUBAGENT_HISTORY} discipline as the claude tracker, and enforced at
4041
+ * settle time for the same reason: a settle happens once per agent, `list()`
4042
+ * once per row of a 1.2s-polled sessions list.
4043
+ */
4044
+ var CodexAgentTracker = class {
4045
+ #byThread = /* @__PURE__ */ new Map();
4046
+ #settleCounter = 0;
4047
+ /** The record whose thread this is — the attribution lookup. */
4048
+ get(agentThreadId) {
4049
+ return this.#byThread.get(agentThreadId);
4050
+ }
4051
+ /** Open (or return) the record for a thread. Fill-in, never overwrite: a
4052
+ * label-less fallback record keeps its accumulated count and its already
4053
+ * published toolUseId when the announcing item arrives late. */
4054
+ open(agentThreadId, toolUseId, agentType, ts) {
4055
+ let record = this.#byThread.get(agentThreadId);
4056
+ if (!record) {
4057
+ record = {
4058
+ agentThreadId,
4059
+ toolUseId,
4060
+ status: "running",
4061
+ startedAt: ts,
4062
+ toolCount: 0,
4063
+ counted: /* @__PURE__ */ new Set()
4064
+ };
4065
+ this.#byThread.set(agentThreadId, record);
4066
+ }
4067
+ record.agentType ??= agentType;
4068
+ return record;
4069
+ }
4070
+ /** The agent's thread ran again (`kind: 'interacted'`, or a fresh
4071
+ * `turn/started` on its thread): a settled verdict no longer describes it. */
4072
+ revive(record) {
4073
+ record.status = "running";
4074
+ record.settledOrder = void 0;
4075
+ }
4076
+ #settle(record, status) {
4077
+ record.status = status;
4078
+ record.settledOrder = ++this.#settleCounter;
4079
+ let settled = 0;
4080
+ for (const r of this.#byThread.values()) if (r.settledOrder !== void 0) settled++;
4081
+ while (settled > SUBAGENT_HISTORY) {
4082
+ let oldest;
4083
+ for (const r of this.#byThread.values()) {
4084
+ if (r.settledOrder === void 0) continue;
4085
+ if (!oldest || r.settledOrder < oldest.settledOrder) oldest = r;
4086
+ }
4087
+ if (!oldest) break;
4088
+ this.#byThread.delete(oldest.agentThreadId);
4089
+ settled--;
4090
+ }
4091
+ }
4092
+ /** A real verdict for one agent — its thread's `turn/completed`, or the
4093
+ * `interrupted` activity edge. */
4094
+ settle(record, status) {
4095
+ if (record.status === status) return;
4096
+ this.#settle(record, status);
4097
+ }
4098
+ /** The process the agents lived in is gone (child death, session close):
4099
+ * everything still running is settled as failed — the report can never come. */
4100
+ sweep() {
4101
+ for (const record of this.#byThread.values()) if (record.status === "running") this.#settle(record, "failed");
4102
+ }
4103
+ /** The rollup as `SessionInfo.subagents` serves it — spawn order, fresh
4104
+ * objects, and `undefined` when there is nothing to say (absent and empty
4105
+ * mean the same thing to a client, and bytes on a polled list are paid for). */
4106
+ list() {
4107
+ if (this.#byThread.size === 0) return void 0;
4108
+ const out = [];
4109
+ for (const r of this.#byThread.values()) out.push({
4110
+ toolUseId: r.toolUseId,
4111
+ agentType: r.agentType,
4112
+ status: r.status,
4113
+ startedAt: r.startedAt,
4114
+ toolCount: r.toolCount
4115
+ });
4116
+ return out;
4117
+ }
4118
+ };
4119
+ //#endregion
3397
4120
  //#region src/engines/codex/runner.ts
3398
4121
  /**
3399
4122
  * thread/start's sandbox axis (string form) — our permission modes as codex
@@ -3434,16 +4157,27 @@ const GRANULAR_ASK = { granular: {
3434
4157
  request_permissions: true,
3435
4158
  skill_approval: true
3436
4159
  } };
4160
+ const GRANULAR_NEVER = { granular: {
4161
+ sandbox_approval: false,
4162
+ rules: false,
4163
+ mcp_elicitations: false,
4164
+ request_permissions: false,
4165
+ skill_approval: false
4166
+ } };
4167
+ /**
4168
+ * Notifications whose meaning is scoped to ONE thread, and which are therefore
4169
+ * only ever read off the session's own. Everything else (items, deltas) is
4170
+ * accepted from any thread on the connection — see `#handleNotification`.
4171
+ */
4172
+ const THREAD_SCOPED_NOTIFICATIONS = new Set([
4173
+ "turn/started",
4174
+ "turn/completed",
4175
+ "thread/tokenUsage/updated"
4176
+ ]);
3437
4177
  const APPROVAL_POLICY_BY_MODE = {
3438
4178
  default: GRANULAR_ASK,
3439
4179
  acceptEdits: GRANULAR_ASK,
3440
- bypassPermissions: { granular: {
3441
- sandbox_approval: false,
3442
- rules: false,
3443
- mcp_elicitations: false,
3444
- request_permissions: false,
3445
- skill_approval: false
3446
- } }
4180
+ bypassPermissions: GRANULAR_NEVER
3447
4181
  };
3448
4182
  /** Fallback timeout for a pending approval nobody answers — the SessionRunner
3449
4183
  * default, so unattended codex sessions land the same way Claude ones do. */
@@ -3454,6 +4188,47 @@ const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
3454
4188
  * host filesystem, an inline preview) off it.
3455
4189
  */
3456
4190
  const CODEX_IMAGE_TOOL = "CodexImageGeneration";
4191
+ /**
4192
+ * Tool name for a spawned agent's anchor `tool_use` — the claude engine's
4193
+ * `Task` in this engine's vocabulary. Codex never sends such a call: the model's
4194
+ * `spawn_agent` surfaces only as the `subAgentActivity` marker item, so the
4195
+ * runner authors the call itself, because everything downstream is built on a
4196
+ * top-level `tool_use` existing — `terminalBlocks` absorbs a sidechain into the
4197
+ * call whose id its events carry as `parentToolUseId`, the takeover frames by
4198
+ * it, and `taskIdentity` labels it from the input's `subagent_type`. Not a new
4199
+ * wire idea, just a row: the same shape every other codex tool card uses.
4200
+ */
4201
+ const CODEX_AGENT_TOOL = "CodexAgent";
4202
+ /** Tool name for the model's collab-agent calls (`wait`, `sendInput`, …), the
4203
+ * `tool` field carried in the input. One name for the whole open axis rather
4204
+ * than a name per verb, so a future verb renders instead of vanishing. */
4205
+ const CODEX_COLLAB_TOOL = "CodexCollab";
4206
+ /** An agent's name is its path's basename: '/root/date_one' → 'date_one'. */
4207
+ function agentName(agentPath) {
4208
+ if (typeof agentPath !== "string") return void 0;
4209
+ return agentPath.split("/").filter(Boolean).at(-1) || void 0;
4210
+ }
4211
+ /** The collab card's input: the verb always, the rich fields only when codex
4212
+ * actually filled them (measured against 0.146.0 they arrive empty — the card
4213
+ * must not render five null columns to say 'wait'). */
4214
+ function collabInput(item) {
4215
+ return {
4216
+ tool: item.tool,
4217
+ ...item.receiverThreadIds?.length ? { receiverThreadIds: item.receiverThreadIds } : {},
4218
+ ...item.prompt ? { prompt: item.prompt } : {},
4219
+ ...item.model ? { model: item.model } : {}
4220
+ };
4221
+ }
4222
+ /** A completed turn's answer, from its summary `items` page — the last
4223
+ * `agentMessage` text. For a sub-agent's thread this is the agent's report,
4224
+ * which is exactly what belongs in the anchor's `tool_result`. */
4225
+ function turnReport(turn) {
4226
+ const items = Array.isArray(turn.items) ? turn.items : [];
4227
+ for (let index = items.length - 1; index >= 0; index--) {
4228
+ const item = items[index];
4229
+ if (item?.type === "agentMessage" && typeof item.text === "string" && item.text) return item.text;
4230
+ }
4231
+ }
3457
4232
  /** Longest `result` worth putting in a tool card. The field is free-form and
3458
4233
  * undocumented; anything past this is assumed to be an encoded image rather
3459
4234
  * than a sentence, and encoded images do not go in the event log. */
@@ -3795,8 +4570,15 @@ var CodexRunner = class {
3795
4570
  /** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
3796
4571
  #cwd;
3797
4572
  #events = [];
3798
- #listeners = /* @__PURE__ */ new Set();
4573
+ #subscribers = new SubscriberSet();
3799
4574
  #seq = 0;
4575
+ /**
4576
+ * Latest context-window reading, retained from the last `context_usage` this
4577
+ * runner emitted so `GET /sessions` can answer it without an attach — see
4578
+ * `SessionInfo.contextUsage`. Folded in the emit path, so it is by
4579
+ * construction the same number the transcript last drew.
4580
+ */
4581
+ #contextUsage;
3800
4582
  #activityCount = 0;
3801
4583
  #status = "starting";
3802
4584
  #sdkSessionId;
@@ -3852,6 +4634,12 @@ var CodexRunner = class {
3852
4634
  * `mcpServerStatus/list` does not carry a status field at all, so without
3853
4635
  * this every server would read as "configured" and never as up or down. */
3854
4636
  #mcpStatus = /* @__PURE__ */ new Map();
4637
+ /** The spawned agents, keyed by their thread ids — the attribution table
4638
+ * behind `parentToolUseId` and the rollup behind `info().subagents`. Runner-
4639
+ * level, not per-turn: an agent's thread outlives the root turn that spawned
4640
+ * it, and only the child process dying (or the session closing) ends them
4641
+ * all — see the module doc in `subagents.ts`. */
4642
+ #agents = new CodexAgentTracker();
3855
4643
  constructor(config, id = randomUUID()) {
3856
4644
  const mode = config.permissionMode ?? "default";
3857
4645
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
@@ -3903,13 +4691,15 @@ var CodexRunner = class {
3903
4691
  createdAt: this.createdAt,
3904
4692
  lastSeq: this.#seq,
3905
4693
  activityCount: this.#activityCount,
4694
+ contextUsage: this.#contextUsage,
3906
4695
  pendingPermissionCount: this.#approvals.size,
3907
4696
  meta: this.#config.meta,
3908
4697
  scope: this.#config.scope,
3909
4698
  title: this.#title(),
3910
4699
  totalCostUsd: this.#totalCostUsd,
3911
4700
  numTurns: this.#numTurns || void 0,
3912
- lastActivityAt: this.#lastActivityAt
4701
+ lastActivityAt: this.#lastActivityAt,
4702
+ subagents: this.#agents.list()
3913
4703
  };
3914
4704
  }
3915
4705
  #title() {
@@ -4123,6 +4913,7 @@ var CodexRunner = class {
4123
4913
  }, "policy");
4124
4914
  this.#connection?.close();
4125
4915
  this.#connection = void 0;
4916
+ this.#agents.sweep();
4126
4917
  this.#activeTurn?.reject(/* @__PURE__ */ new Error("session closed"));
4127
4918
  if (this.#imageDir) try {
4128
4919
  rmSync(this.#imageDir, {
@@ -4136,15 +4927,14 @@ var CodexRunner = class {
4136
4927
  });
4137
4928
  this.#setStatus("closed");
4138
4929
  }
4930
+ /** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
4931
+ * "show everything" on one row, so a per-runner seq index would be a map
4932
+ * maintained on every emit to save a walk nobody makes twice a minute. */
4933
+ eventAt(seq) {
4934
+ return this.#events.find((event) => event.seq === seq);
4935
+ }
4139
4936
  subscribe(listener, afterSeq = 0, options) {
4140
- const stale = options?.coalesceReplay ? staleReplaySeqs(this.#events, afterSeq) : void 0;
4141
- for (const event of this.#events) {
4142
- if (event.seq <= afterSeq) continue;
4143
- if (stale?.has(event.seq)) continue;
4144
- listener(event);
4145
- }
4146
- this.#listeners.add(listener);
4147
- return () => this.#listeners.delete(listener);
4937
+ return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
4148
4938
  }
4149
4939
  #scheduleTurn() {
4150
4940
  this.#turnChain = this.#turnChain.then(() => this.#runTurn());
@@ -4174,6 +4964,7 @@ var CodexRunner = class {
4174
4964
  behavior: "deny",
4175
4965
  message
4176
4966
  }, "policy");
4967
+ this.#agents.sweep();
4177
4968
  this.#activeTurn?.reject(new Error(message));
4178
4969
  });
4179
4970
  try {
@@ -4469,11 +5260,79 @@ var CodexRunner = class {
4469
5260
  }
4470
5261
  #handleNotification(method, params) {
4471
5262
  if (this.#closed) return;
5263
+ if (THREAD_SCOPED_NOTIFICATIONS.has(method) && !this.#isRootThread(params)) {
5264
+ if (method === "turn/completed") this.#settleAgentTurn(params);
5265
+ else if (method === "turn/started") {
5266
+ const threadId = this.#threadIdOf(params);
5267
+ const record = threadId ? this.#agents.get(threadId) : void 0;
5268
+ if (record && record.status !== "running") this.#agents.revive(record);
5269
+ }
5270
+ return;
5271
+ }
4472
5272
  this.#notifications[method]?.(params);
4473
5273
  }
5274
+ /** Whether a notification is about the session's own thread. A notification
5275
+ * with no `threadId` counts as the root's: every thread-scoped method the
5276
+ * schema defines carries one, so an absent id means an older or narrower
5277
+ * shape, not a sub-agent. */
5278
+ #isRootThread(params) {
5279
+ const threadId = this.#threadIdOf(params);
5280
+ if (threadId === void 0) return true;
5281
+ return threadId === this.#sdkSessionId;
5282
+ }
5283
+ #threadIdOf(params) {
5284
+ const threadId = params?.threadId;
5285
+ return typeof threadId === "string" ? threadId : void 0;
5286
+ }
5287
+ /**
5288
+ * The agent behind a notification's `threadId` — the attribution every item
5289
+ * and delta handler asks before emitting, so two agents streaming
5290
+ * concurrently into this one connection come apart again by the id each
5291
+ * frame carries, never by any mutable "current agent".
5292
+ *
5293
+ * A non-root thread with no record still gets one: a thread emitting items on
5294
+ * this connection *is* an agent, whatever announced it (codex runs threads of
5295
+ * its own for review/compact, and a `subAgentActivity` could in principle be
5296
+ * missed) — the claude tracker's nested-event fallback, on a stronger signal.
5297
+ * The minted record is label-less and its anchor is authored here, because an
5298
+ * attributed event whose parent id matches no top-level `tool_use` would
5299
+ * render inline rather than as a frame; a late `started` edge fills the name
5300
+ * in. Root-thread traffic — and, defensively, the pre-thread shapes with no
5301
+ * id at all — stays unattributed (`undefined`).
5302
+ */
5303
+ #agentFor(params) {
5304
+ const threadId = this.#threadIdOf(params);
5305
+ if (threadId === void 0 || threadId === this.#sdkSessionId) return void 0;
5306
+ const known = this.#agents.get(threadId);
5307
+ if (known) return known;
5308
+ const nonce = this.#activeTurn?.nonce ?? "codex";
5309
+ const record = this.#agents.open(threadId, `${nonce}:agent:${threadId}`, void 0, Date.now());
5310
+ record.anchored = true;
5311
+ this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, { agentThreadId: threadId });
5312
+ return record;
5313
+ }
5314
+ /**
5315
+ * A child thread's `turn/completed` is that AGENT's completion — the one
5316
+ * codex sends (`subAgentActivity` has no 'completed' kind, verified live).
5317
+ * The verdict is the turn's own status, and the report is the completed
5318
+ * turn's final message, delivered as the anchor's `tool_result` so the row
5319
+ * settles exactly the way a claude `Task`'s does. Deliberately not gated on
5320
+ * `#activeTurn`: an agent finishing between root turns still finished.
5321
+ */
5322
+ #settleAgentTurn(params) {
5323
+ const threadId = this.#threadIdOf(params);
5324
+ const record = threadId ? this.#agents.get(threadId) : void 0;
5325
+ if (!record || record.status !== "running") return;
5326
+ const turn = params?.turn;
5327
+ const status = turn?.status === "completed" ? "done" : "failed";
5328
+ this.#agents.settle(record, status);
5329
+ const report = (turn ? turnReport(turn) : void 0) ?? turn?.error?.message ?? (status === "done" ? "" : turn?.status ?? "failed");
5330
+ this.#emitToolResult(record.toolUseId, report, status === "failed");
5331
+ }
4474
5332
  /** Reasoning deltas arrive on two methods that differ only in which section
4475
5333
  * counter they advance; the section key carries the method so the two streams
4476
- * never share a boundary. Section boundaries (a new summary/content entry)
5334
+ * never share a boundary (and item ids are per-thread, so two agents' streams
5335
+ * never share one either). Section boundaries (a new summary/content entry)
4477
5336
  * render as paragraph breaks — the completed item joins sections with '\n\n'. */
4478
5337
  #reasoningDelta(method) {
4479
5338
  return (params) => {
@@ -4489,7 +5348,7 @@ var CodexRunner = class {
4489
5348
  this.#emitDelta({
4490
5349
  type: "thinking_delta",
4491
5350
  thinking: separator + payload.delta
4492
- });
5351
+ }, this.#agentFor(params)?.toolUseId ?? null);
4493
5352
  };
4494
5353
  }
4495
5354
  /** One item-progress handler serves `item/started` and `item/updated`. */
@@ -4497,7 +5356,7 @@ var CodexRunner = class {
4497
5356
  const active = this.#activeTurn;
4498
5357
  if (!active) return;
4499
5358
  const item = params?.item;
4500
- if (item) this.#handleItemProgress(item, active);
5359
+ if (item) this.#handleItemProgress(item, active, this.#agentFor(params));
4501
5360
  };
4502
5361
  /** The notification dispatch table — every method the child emits that this
4503
5362
  * runner maps, in one place. Handlers read `this.#activeTurn` themselves:
@@ -4515,7 +5374,9 @@ var CodexRunner = class {
4515
5374
  "turn/completed": (params) => {
4516
5375
  const active = this.#activeTurn;
4517
5376
  const turn = params?.turn;
4518
- if (active && turn) active.resolve(turn);
5377
+ if (!active || !turn) return;
5378
+ if (active.turnId && turn.id && turn.id !== active.turnId) return;
5379
+ active.resolve(turn);
4519
5380
  },
4520
5381
  "item/started": this.#itemProgress,
4521
5382
  "item/updated": this.#itemProgress,
@@ -4523,7 +5384,7 @@ var CodexRunner = class {
4523
5384
  const active = this.#activeTurn;
4524
5385
  if (!active) return;
4525
5386
  const item = params?.item;
4526
- if (item) this.#handleItemCompleted(item, active);
5387
+ if (item) this.#handleItemCompleted(item, active, this.#agentFor(params));
4527
5388
  },
4528
5389
  "item/agentMessage/delta": (params) => {
4529
5390
  if (!this.#activeTurn) return;
@@ -4531,7 +5392,7 @@ var CodexRunner = class {
4531
5392
  if (typeof delta === "string" && delta) this.#emitDelta({
4532
5393
  type: "text_delta",
4533
5394
  text: delta
4534
- });
5395
+ }, this.#agentFor(params)?.toolUseId ?? null);
4535
5396
  },
4536
5397
  "item/reasoning/textDelta": this.#reasoningDelta("item/reasoning/textDelta"),
4537
5398
  "item/reasoning/summaryTextDelta": this.#reasoningDelta("item/reasoning/summaryTextDelta"),
@@ -4724,30 +5585,40 @@ var CodexRunner = class {
4724
5585
  if (!this.#closed && this.#approvals.size === 0 && this.#status === "awaiting_approval") this.#setStatus("running");
4725
5586
  }
4726
5587
  /** Tool calls surface as tool_use when they start; text and reasoning stream
4727
- * natively via the delta notifications. */
4728
- #handleItemProgress(item, active) {
5588
+ * natively via the delta notifications. `agent` is the sub-agent whose thread
5589
+ * the item arrived on — undefined for the session's own. */
5590
+ #handleItemProgress(item, active, agent) {
4729
5591
  const id = `${active.nonce}:${item.id}`;
5592
+ if (item.type === "subAgentActivity") {
5593
+ this.#itemCompleted.subAgentActivity(item, active, id, agent);
5594
+ return;
5595
+ }
4730
5596
  if (item.type === "commandExecution" && !active.toolUseEmitted.has(id)) {
4731
5597
  active.toolUseEmitted.add(id);
4732
- this.#emitToolUse(id, "CodexCommand", { command: item.command });
5598
+ this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
4733
5599
  return;
4734
5600
  }
4735
5601
  if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
4736
5602
  active.toolUseEmitted.add(id);
4737
- this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
5603
+ this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
5604
+ return;
5605
+ }
5606
+ if (item.type === "collabAgentToolCall" && !active.toolUseEmitted.has(id)) {
5607
+ active.toolUseEmitted.add(id);
5608
+ this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
4738
5609
  return;
4739
5610
  }
4740
5611
  if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
4741
5612
  active.toolUseEmitted.add(id);
4742
- this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
5613
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
4743
5614
  if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
4744
5615
  }
4745
5616
  }
4746
- #handleItemCompleted(item, active) {
5617
+ #handleItemCompleted(item, active, agent) {
4747
5618
  const id = `${active.nonce}:${item.id}`;
4748
5619
  const handler = this.#itemCompleted[item.type];
4749
5620
  if (handler) {
4750
- handler(item, active, id);
5621
+ handler(item, active, id, agent);
4751
5622
  return;
4752
5623
  }
4753
5624
  const unknown = item;
@@ -4768,67 +5639,121 @@ var CodexRunner = class {
4768
5639
  * union has never heard of; those take the passthrough above.)
4769
5640
  */
4770
5641
  #itemCompleted = {
4771
- userMessage: () => {},
4772
- agentMessage: (item, active, id) => {
5642
+ userMessage: (item, active, _id, agent) => {
5643
+ if (!agent) return;
5644
+ const text = historyUserText(item);
5645
+ if (!text) return;
5646
+ this.#emit({
5647
+ type: "user_message",
5648
+ message: {
5649
+ role: "user",
5650
+ content: text
5651
+ },
5652
+ parentToolUseId: agent.toolUseId,
5653
+ uuid: `${active.nonce}:${item.id}`
5654
+ });
5655
+ },
5656
+ agentMessage: (item, active, id, agent) => {
4773
5657
  const text = typeof item.text === "string" ? item.text : "";
4774
5658
  this.#emitAssistant(id, [{
4775
5659
  type: "text",
4776
5660
  text
4777
- }]);
4778
- active.finalText = text;
5661
+ }], agent?.toolUseId ?? null);
5662
+ if (!agent) active.finalText = text;
4779
5663
  },
4780
- reasoning: (item, _active, id) => {
5664
+ reasoning: (item, _active, id, agent) => {
4781
5665
  const summary = Array.isArray(item.summary) ? item.summary.filter(Boolean) : [];
4782
5666
  const content = Array.isArray(item.content) ? item.content.filter(Boolean) : [];
4783
5667
  const thinking = (summary.length > 0 ? summary : content).join("\n\n");
4784
5668
  if (thinking) this.#emitAssistant(id, [{
4785
5669
  type: "thinking",
4786
5670
  thinking
4787
- }]);
5671
+ }], agent?.toolUseId ?? null);
4788
5672
  },
4789
- commandExecution: (item, active, id) => {
5673
+ commandExecution: (item, active, id, agent) => {
4790
5674
  if (!active.toolUseEmitted.has(id)) {
4791
5675
  active.toolUseEmitted.add(id);
4792
- this.#emitToolUse(id, "CodexCommand", { command: item.command });
5676
+ this.#emitToolUse(id, "CodexCommand", { command: item.command }, agent);
4793
5677
  }
4794
5678
  const exitCode = item.exitCode ?? void 0;
4795
5679
  const failed = item.status === "failed" || item.status === "declined" || exitCode !== void 0 && exitCode !== 0;
4796
5680
  const output = (item.aggregatedOutput ?? "") + (exitCode !== void 0 && exitCode !== 0 ? `\n(exit code ${exitCode})` : "");
4797
- this.#emitToolResult(id, output, failed);
5681
+ this.#emitToolResult(id, output, failed, void 0, agent?.toolUseId ?? null);
4798
5682
  },
4799
- fileChange: (item, _active, id) => {
4800
- this.#emitToolUse(id, "CodexFileChange", { changes: item.changes });
5683
+ fileChange: (item, _active, id, agent) => {
5684
+ this.#emitToolUse(id, "CodexFileChange", { changes: item.changes }, agent);
4801
5685
  const lines = item.changes.map((change) => {
4802
5686
  return `${(typeof change.kind === "string" ? change.kind : change.kind?.type) ?? "change"}: ${change.path}`;
4803
5687
  });
4804
5688
  const only = item.changes.length === 1 ? item.changes[0] : void 0;
4805
- this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0);
5689
+ this.#emitToolResult(id, lines.join("\n") || item.status, item.status === "failed" || item.status === "declined", only?.diff ? parseUnifiedDiff(only.diff, only.path) : void 0, agent?.toolUseId ?? null);
4806
5690
  },
4807
- mcpToolCall: (item, active, id) => {
5691
+ mcpToolCall: (item, active, id, agent) => {
4808
5692
  if (!active.toolUseEmitted.has(id)) {
4809
5693
  active.toolUseEmitted.add(id);
4810
- this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
5694
+ this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments, agent);
4811
5695
  }
4812
5696
  const isError = item.error !== void 0 && item.error !== null || item.status === "failed";
4813
- this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError);
5697
+ this.#emitToolResult(id, item.error?.message ?? (item.result === void 0 || item.result === null ? "" : JSON.stringify(item.result)), isError, void 0, agent?.toolUseId ?? null);
4814
5698
  },
4815
- webSearch: (item, _active, id) => {
4816
- this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
4817
- this.#emitToolResult(id, "", false);
5699
+ webSearch: (item, _active, id, agent) => {
5700
+ this.#emitToolUse(id, "CodexWebSearch", { query: item.query }, agent);
5701
+ this.#emitToolResult(id, "", false, void 0, agent?.toolUseId ?? null);
4818
5702
  },
4819
- imageGeneration: (item, active, id) => {
5703
+ imageGeneration: (item, active, id, agent) => {
4820
5704
  active.toolUseEmitted.add(id);
4821
- this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
5705
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item), agent);
4822
5706
  if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
4823
5707
  const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
4824
- this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
5708
+ this.#emitToolResult(id, lines.join("\n"), item.status === "failed", void 0, agent?.toolUseId ?? null);
4825
5709
  },
4826
- imageView: (item, _active, id) => {
4827
- this.#emitToolUse(id, "CodexImageView", { path: item.path });
4828
- this.#emitToolResult(id, item.path, false);
5710
+ imageView: (item, _active, id, agent) => {
5711
+ this.#emitToolUse(id, "CodexImageView", { path: item.path }, agent);
5712
+ this.#emitToolResult(id, item.path, false, void 0, agent?.toolUseId ?? null);
5713
+ },
5714
+ subAgentActivity: (item, _active, id, agent) => {
5715
+ if (this.#replayingHistory) {
5716
+ if (item.kind !== "started") return;
5717
+ this.#emitToolUse(id, CODEX_AGENT_TOOL, {
5718
+ ...agentName(item.agentPath) ? { subagent_type: agentName(item.agentPath) } : {},
5719
+ agentThreadId: item.agentThreadId,
5720
+ ...item.agentPath ? { agentPath: item.agentPath } : {}
5721
+ }, agent);
5722
+ this.#emitToolResult(id, "(ran in its own thread — its work is not part of this thread's stored history)", false, void 0, agent?.toolUseId ?? null);
5723
+ return;
5724
+ }
5725
+ const record = this.#agents.get(item.agentThreadId) ?? this.#agents.open(item.agentThreadId, id, void 0, Date.now());
5726
+ const name = agentName(item.agentPath);
5727
+ const relabel = record.agentType === void 0 && name !== void 0;
5728
+ if (relabel) record.agentType = name;
5729
+ if (!record.anchored || relabel) {
5730
+ record.anchored = true;
5731
+ this.#emitToolUse(record.toolUseId, CODEX_AGENT_TOOL, {
5732
+ ...record.agentType ? { subagent_type: record.agentType } : {},
5733
+ agentThreadId: item.agentThreadId,
5734
+ ...item.agentPath ? { agentPath: item.agentPath } : {}
5735
+ }, agent);
5736
+ }
5737
+ if (item.kind === "interrupted") {
5738
+ if (record.status === "running") {
5739
+ this.#agents.settle(record, "failed");
5740
+ this.#emitToolResult(record.toolUseId, "interrupted", true);
5741
+ }
5742
+ return;
5743
+ }
5744
+ if (item.kind !== "started" && record.status !== "running") this.#agents.revive(record);
5745
+ },
5746
+ collabAgentToolCall: (item, active, id, agent) => {
5747
+ if (!active.toolUseEmitted.has(id)) {
5748
+ active.toolUseEmitted.add(id);
5749
+ this.#emitToolUse(id, CODEX_COLLAB_TOOL, collabInput(item), agent);
5750
+ }
5751
+ if (item.status === "inProgress") return;
5752
+ const failed = item.status === "failed" || item.status === "declined";
5753
+ this.#emitToolResult(id, failed ? item.status : "", failed, void 0, agent?.toolUseId ?? null);
4829
5754
  }
4830
5755
  };
4831
- #emitDelta(delta) {
5756
+ #emitDelta(delta, parent) {
4832
5757
  if (this.#config.includePartialMessages === false) return;
4833
5758
  this.#emit({
4834
5759
  type: "stream_delta",
@@ -4836,11 +5761,11 @@ var CodexRunner = class {
4836
5761
  type: "content_block_delta",
4837
5762
  delta
4838
5763
  },
4839
- parentToolUseId: null,
5764
+ parentToolUseId: parent,
4840
5765
  uuid: randomUUID()
4841
5766
  });
4842
5767
  }
4843
- #emitAssistant(uuid, content) {
5768
+ #emitAssistant(uuid, content, parent) {
4844
5769
  this.#emit({
4845
5770
  type: "assistant_message",
4846
5771
  message: {
@@ -4848,11 +5773,19 @@ var CodexRunner = class {
4848
5773
  content,
4849
5774
  model: this.#model ?? this.#resolvedModel
4850
5775
  },
4851
- parentToolUseId: null,
5776
+ parentToolUseId: parent,
4852
5777
  uuid
4853
5778
  });
4854
5779
  }
4855
- #emitToolUse(id, name, input) {
5780
+ /** `agent` (rather than a bare parent id) because a nested call is also the
5781
+ * agent's progress reading: `SubagentInfo.toolCount` ticks here, once per
5782
+ * card — the `counted` set is what keeps an upserted re-emission (the
5783
+ * finished imageGeneration input) from counting one picture twice. */
5784
+ #emitToolUse(id, name, input, agent) {
5785
+ if (agent && !agent.counted.has(id)) {
5786
+ agent.counted.add(id);
5787
+ agent.toolCount += 1;
5788
+ }
4856
5789
  this.#emit({
4857
5790
  type: "assistant_message",
4858
5791
  message: {
@@ -4865,11 +5798,11 @@ var CodexRunner = class {
4865
5798
  }],
4866
5799
  model: this.#model ?? this.#resolvedModel
4867
5800
  },
4868
- parentToolUseId: null,
5801
+ parentToolUseId: agent?.toolUseId ?? null,
4869
5802
  uuid: `${id}-use`
4870
5803
  });
4871
5804
  }
4872
- #emitToolResult(toolUseId, content, isError, patch) {
5805
+ #emitToolResult(toolUseId, content, isError, patch, parent = null) {
4873
5806
  this.#emit({
4874
5807
  type: "user_message",
4875
5808
  message: {
@@ -4881,7 +5814,7 @@ var CodexRunner = class {
4881
5814
  is_error: isError || void 0
4882
5815
  }]
4883
5816
  },
4884
- parentToolUseId: null,
5817
+ parentToolUseId: parent,
4885
5818
  synthetic: true,
4886
5819
  patch,
4887
5820
  uuid: `${toolUseId}-result`
@@ -5009,10 +5942,10 @@ var CodexRunner = class {
5009
5942
  };
5010
5943
  this.#lastActivityAt = event.ts;
5011
5944
  this.#activityCount += transcriptActivity(body);
5945
+ this.#contextUsage = contextReading(body) ?? this.#contextUsage;
5946
+ if (body.type === "conversation_reset") this.#contextUsage = void 0;
5012
5947
  this.#events.push(event);
5013
- for (const listener of this.#listeners) try {
5014
- listener(event);
5015
- } catch {}
5948
+ this.#subscribers.emit(event);
5016
5949
  }
5017
5950
  };
5018
5951
  //#endregion
@@ -5446,6 +6379,6 @@ function getEngineAdapter(engine) {
5446
6379
  return ADAPTERS[engine ?? "claude"];
5447
6380
  }
5448
6381
  //#endregion
5449
- export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, withHostTools, withMcpTools };
6382
+ export { AiSdkRunner, BrowserBridgeExecutor, CLAUDE_CATALOG, CODEX_CATALOG, CodexRunner, DeferredExecutor, InputQueue, JsonRpcError, JsonRpcStdioConnection, PendingRequestRegistry, QuickJsExecutor, SUPPORTED_ATTACHMENT_TYPES, SessionRunner, attachmentContentBlocks, attachmentKind, attachmentRef, checkClaudeAuth, claudeAdapter, codexAdapter, connectAppServer, connectMcpTools, createEngineSession, createToolContext, createWebFetch, getEngineAdapter, htmlToMarkdown, isHostAllowed, isPrivateAddress, listCodexSessions, mcpStatusInfo, modelOptionsFromSdk, normalizeMediaType, normalizeSdkMessage, providerAdapter, replaySlice, resolveBundledClaudeExecutable, resolveBundledCodexExecutable, toApiMessage, toExecutionResult, truncateResultBlocks, withHostTools, withMcpTools };
5450
6383
 
5451
6384
  //# sourceMappingURL=index.mjs.map