@workerdeck/core 0.16.0 → 0.17.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/README.md +22 -0
- package/build/index.d.mts +162 -11
- package/build/index.mjs +615 -53
- package/build/index.mjs.map +1 -1
- package/package.json +3 -3
package/build/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, 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
7
|
import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -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,7 +1098,7 @@ var SessionRunner = class {
|
|
|
608
1098
|
/** {@link SessionRunnerConfig.cwd}, checked once in the constructor. */
|
|
609
1099
|
#cwd;
|
|
610
1100
|
#events = [];
|
|
611
|
-
#
|
|
1101
|
+
#subscribers = new SubscriberSet();
|
|
612
1102
|
#seq = 0;
|
|
613
1103
|
#activityCount = 0;
|
|
614
1104
|
/**
|
|
@@ -627,6 +1117,28 @@ var SessionRunner = class {
|
|
|
627
1117
|
#apiKeySource;
|
|
628
1118
|
#permissionMode;
|
|
629
1119
|
#pending = /* @__PURE__ */ new Map();
|
|
1120
|
+
/**
|
|
1121
|
+
* The turn ended while an approval was standing, and nothing has started a
|
|
1122
|
+
* new one since.
|
|
1123
|
+
*
|
|
1124
|
+
* `awaiting_approval` rightly outranks `idle` for display, so a turn-over
|
|
1125
|
+
* signal arriving under a standing approval cannot be applied when it lands.
|
|
1126
|
+
* It used to be **discarded** for that reason, which is a different thing
|
|
1127
|
+
* from outranked: the settle path then asserted `running` on the assumption
|
|
1128
|
+
* that an answered approval means work resumes, and when the turn was already
|
|
1129
|
+
* over — an interrupt, a timeout — the session claimed to be running one that
|
|
1130
|
+
* had produced its result. Status is purely edge-driven here, with no poll and
|
|
1131
|
+
* no reconciliation anywhere, so that single dropped edge never came back and
|
|
1132
|
+
* every client rendered it faithfully for the life of the session.
|
|
1133
|
+
*
|
|
1134
|
+
* So the fact is *deferred* rather than dropped, and it is deliberately
|
|
1135
|
+
* cleared the moment work genuinely resumes — a turn-over belongs to the turn
|
|
1136
|
+
* that produced it and must not settle the next one.
|
|
1137
|
+
*/
|
|
1138
|
+
#turnOverWhileBlocked = false;
|
|
1139
|
+
/** The read-time sub-agent rollup (`SessionInfo.subagents`), fed from #emit —
|
|
1140
|
+
* the one chokepoint — so the resume backfill reconstructs it for free. */
|
|
1141
|
+
#subagents = new SubagentTracker();
|
|
630
1142
|
#totalCostUsd;
|
|
631
1143
|
#numTurns;
|
|
632
1144
|
#lastActivityAt;
|
|
@@ -683,6 +1195,7 @@ var SessionRunner = class {
|
|
|
683
1195
|
lastSeq: this.#seq,
|
|
684
1196
|
activityCount: this.#activityCount,
|
|
685
1197
|
pendingPermissionCount: this.#pending.size,
|
|
1198
|
+
subagents: this.#subagents.list(),
|
|
686
1199
|
meta: this.#config.meta,
|
|
687
1200
|
scope: this.#config.scope,
|
|
688
1201
|
title: this.#title(),
|
|
@@ -831,6 +1344,12 @@ var SessionRunner = class {
|
|
|
831
1344
|
});
|
|
832
1345
|
this.#setStatus("closed");
|
|
833
1346
|
}
|
|
1347
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
1348
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
1349
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
1350
|
+
eventAt(seq) {
|
|
1351
|
+
return this.#events.find((event) => event.seq === seq);
|
|
1352
|
+
}
|
|
834
1353
|
/**
|
|
835
1354
|
* Replay buffered events with seq > afterSeq, then deliver live events.
|
|
836
1355
|
* Returns an unsubscribe function.
|
|
@@ -845,15 +1364,7 @@ var SessionRunner = class {
|
|
|
845
1364
|
* content below the newer one and are skipped with what they cleared.
|
|
846
1365
|
*/
|
|
847
1366
|
subscribe(listener, afterSeq = 0, options) {
|
|
848
|
-
|
|
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);
|
|
1367
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options, this.#resetSeq);
|
|
857
1368
|
}
|
|
858
1369
|
async #run() {
|
|
859
1370
|
const queryFn = this.#config.queryFn ?? query;
|
|
@@ -944,6 +1455,7 @@ var SessionRunner = class {
|
|
|
944
1455
|
forkSession: c.forkSession,
|
|
945
1456
|
effort: c.reasoningEffort,
|
|
946
1457
|
includePartialMessages: c.includePartialMessages ?? true,
|
|
1458
|
+
forwardSubagentText: true,
|
|
947
1459
|
canUseTool: this.#canUseTool,
|
|
948
1460
|
env: c.env,
|
|
949
1461
|
pathToClaudeCodeExecutable: c.pathToClaudeCodeExecutable,
|
|
@@ -970,6 +1482,7 @@ var SessionRunner = class {
|
|
|
970
1482
|
claudeCodeVersion: msg.claude_code_version,
|
|
971
1483
|
mcpServers: msg.mcp_servers
|
|
972
1484
|
});
|
|
1485
|
+
this.#turnOverWhileBlocked = false;
|
|
973
1486
|
this.#setStatus("running");
|
|
974
1487
|
this.#fetchCapabilities();
|
|
975
1488
|
this.#fetchContextUsage();
|
|
@@ -978,7 +1491,11 @@ var SessionRunner = class {
|
|
|
978
1491
|
return;
|
|
979
1492
|
}
|
|
980
1493
|
if (msg.type === "system" && msg.subtype === "session_state_changed") {
|
|
981
|
-
if (this.#pending.size > 0)
|
|
1494
|
+
if (this.#pending.size > 0) {
|
|
1495
|
+
if (msg.state === "idle") this.#turnOverWhileBlocked = true;
|
|
1496
|
+
else if (msg.state === "running") this.#turnOverWhileBlocked = false;
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
982
1499
|
if (msg.state === "idle") this.#setStatus("idle");
|
|
983
1500
|
else if (msg.state === "running") this.#setStatus("running");
|
|
984
1501
|
return;
|
|
@@ -994,6 +1511,7 @@ var SessionRunner = class {
|
|
|
994
1511
|
this.#totalCostUsd = body.totalCostUsd;
|
|
995
1512
|
this.#numTurns = body.numTurns;
|
|
996
1513
|
if (this.#pending.size === 0) this.#setStatus("idle");
|
|
1514
|
+
else this.#turnOverWhileBlocked = true;
|
|
997
1515
|
this.#fetchContextUsage();
|
|
998
1516
|
this.#fetchRateLimits();
|
|
999
1517
|
this.#fetchEngineTitle();
|
|
@@ -1224,7 +1742,12 @@ var SessionRunner = class {
|
|
|
1224
1742
|
resolvedBy,
|
|
1225
1743
|
message: decision.behavior === "deny" ? decision.message ?? "Denied" : void 0
|
|
1226
1744
|
});
|
|
1227
|
-
if (this.#pending.size === 0
|
|
1745
|
+
if (this.#pending.size === 0) {
|
|
1746
|
+
const endedWhileBlocked = this.#turnOverWhileBlocked;
|
|
1747
|
+
this.#turnOverWhileBlocked = false;
|
|
1748
|
+
if (endedWhileBlocked) this.#setStatus("idle");
|
|
1749
|
+
else if (this.#status === "awaiting_approval") this.#setStatus("running");
|
|
1750
|
+
}
|
|
1228
1751
|
}
|
|
1229
1752
|
#setStatus(status, detail) {
|
|
1230
1753
|
if (this.#status === status && this.#statusDetail === detail) return;
|
|
@@ -1246,10 +1769,9 @@ var SessionRunner = class {
|
|
|
1246
1769
|
this.#lastActivityAt = event.ts;
|
|
1247
1770
|
this.#activityCount += transcriptActivity(body);
|
|
1248
1771
|
if (body.type === "conversation_reset") this.#resetSeq = event.seq;
|
|
1772
|
+
this.#subagents.observe(body, event.ts);
|
|
1249
1773
|
this.#events.push(event);
|
|
1250
|
-
|
|
1251
|
-
listener(event);
|
|
1252
|
-
} catch {}
|
|
1774
|
+
this.#subscribers.emit(event);
|
|
1253
1775
|
}
|
|
1254
1776
|
};
|
|
1255
1777
|
/** Answer each AskUserQuestion question with its first option's label — the tool's
|
|
@@ -1292,7 +1814,7 @@ var AiSdkRunner = class {
|
|
|
1292
1814
|
#config;
|
|
1293
1815
|
#model;
|
|
1294
1816
|
#events = [];
|
|
1295
|
-
#
|
|
1817
|
+
#subscribers = new SubscriberSet();
|
|
1296
1818
|
#seq = 0;
|
|
1297
1819
|
#activityCount = 0;
|
|
1298
1820
|
#status = "starting";
|
|
@@ -1405,10 +1927,7 @@ var AiSdkRunner = class {
|
|
|
1405
1927
|
start() {
|
|
1406
1928
|
if (this.#started) return this.#turnChain;
|
|
1407
1929
|
this.#started = true;
|
|
1408
|
-
if (this.#config.restore)
|
|
1409
|
-
if (this.#pendingToolCalls.size === 0) this.#scheduleTurn();
|
|
1410
|
-
return this.#turnChain;
|
|
1411
|
-
}
|
|
1930
|
+
if (this.#config.restore) return this.#turnChain;
|
|
1412
1931
|
this.#setStatus("idle");
|
|
1413
1932
|
if (this.#config.prompt) this.sendMessage(this.#config.prompt);
|
|
1414
1933
|
return this.#turnChain;
|
|
@@ -1423,6 +1942,61 @@ var AiSdkRunner = class {
|
|
|
1423
1942
|
if (this.#closed || this.#parked) return void 0;
|
|
1424
1943
|
if (this.#abort || !this.#restingOnDeferred()) return void 0;
|
|
1425
1944
|
this.#setStatus("parked");
|
|
1945
|
+
const snapshot = this.#buildSnapshot();
|
|
1946
|
+
this.#parked = true;
|
|
1947
|
+
this.#subscribers.clear();
|
|
1948
|
+
try {
|
|
1949
|
+
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1950
|
+
} catch {}
|
|
1951
|
+
return snapshot;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* The same snapshot, taken without ending anything.
|
|
1955
|
+
*
|
|
1956
|
+
* `park()` and this are two operations that happen to produce the same value,
|
|
1957
|
+
* and the difference is the whole point: `park()` *ends* the live runner
|
|
1958
|
+
* (inert, listeners dropped, `onClose` called), which is right for deferred
|
|
1959
|
+
* execution — the session has nothing to do for possibly days — and wrong for
|
|
1960
|
+
* restart-survival, where the session is active and someone is mid-
|
|
1961
|
+
* conversation. This one changes nothing at all: no status emit, no listener
|
|
1962
|
+
* clear, no disposer. The host writes the value through to durable storage
|
|
1963
|
+
* after each turn and keeps the runner live and warm, so a restart rebuilds
|
|
1964
|
+
* from the last write through the existing `restore` path and the next message
|
|
1965
|
+
* costs no wake.
|
|
1966
|
+
*
|
|
1967
|
+
* The gate is `park()`'s minus the requirement that there be something parked:
|
|
1968
|
+
*
|
|
1969
|
+
* - `#abort` set is refused for the reason it always was — a `generate()` in
|
|
1970
|
+
* flight has produced messages that are not in the history yet, so the
|
|
1971
|
+
* snapshot would be of a turn that half-happened.
|
|
1972
|
+
* - Pending calls that are **not** all deferred are refused, which is
|
|
1973
|
+
* `park()`'s rule wearing a different hat. An in-process execution's result
|
|
1974
|
+
* is coming back to *this* runner and dies with the process; a restore would
|
|
1975
|
+
* wait on it forever, and `state.dispatched` is what would stop the rebuilt
|
|
1976
|
+
* runner from simply calling it again.
|
|
1977
|
+
* - Idle with nothing pending — the case `park()` exists to refuse — is
|
|
1978
|
+
* exactly the case this exists to allow.
|
|
1979
|
+
*/
|
|
1980
|
+
snapshot() {
|
|
1981
|
+
if (this.#closed || this.#parked || this.#abort) return void 0;
|
|
1982
|
+
if (this.#pendingToolCalls.size > 0 && !this.#restingOnDeferred()) return void 0;
|
|
1983
|
+
return this.#buildSnapshot();
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* The snapshot value itself, shared so a park and a write-through cannot
|
|
1987
|
+
* disagree about what a session *is*.
|
|
1988
|
+
*
|
|
1989
|
+
* The event log is filtered through {@link snapshotRetains} — the persisted
|
|
1990
|
+
* log drops stream deltas, which are superseded by the `assistant_message`
|
|
1991
|
+
* that flushes them and would otherwise be tens of times the size of the text
|
|
1992
|
+
* they spell. Parks get it too, and should: a park sits on disk for days.
|
|
1993
|
+
*
|
|
1994
|
+
* The `parked` list and `state.parkedAt` are honest under both callers. An
|
|
1995
|
+
* idle write-through has no pending calls, so `parked` is empty and the host
|
|
1996
|
+
* arms no watchdogs; `parkedAt` is "when this was taken", which is what
|
|
1997
|
+
* `#restore` needs to discount a turn's clock either way.
|
|
1998
|
+
*/
|
|
1999
|
+
#buildSnapshot() {
|
|
1426
2000
|
const parked = [...this.#pendingToolCalls.values()].map((call) => ({
|
|
1427
2001
|
executionId: call.toolCallId,
|
|
1428
2002
|
toolName: call.toolName,
|
|
@@ -1440,22 +2014,16 @@ var AiSdkRunner = class {
|
|
|
1440
2014
|
lastActivityAt: this.#lastActivityAt,
|
|
1441
2015
|
parkedAt: Date.now()
|
|
1442
2016
|
};
|
|
1443
|
-
|
|
2017
|
+
return {
|
|
1444
2018
|
engine: "provider",
|
|
1445
2019
|
id: this.id,
|
|
1446
2020
|
createdAt: this.createdAt,
|
|
1447
2021
|
seq: this.#seq,
|
|
1448
|
-
events:
|
|
2022
|
+
events: this.#events.filter((event) => snapshotRetains(event)),
|
|
1449
2023
|
vfs: this.#config.vfs?.snapshot(),
|
|
1450
2024
|
parked,
|
|
1451
2025
|
state
|
|
1452
2026
|
};
|
|
1453
|
-
this.#parked = true;
|
|
1454
|
-
this.#listeners.clear();
|
|
1455
|
-
try {
|
|
1456
|
-
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1457
|
-
} catch {}
|
|
1458
|
-
return snapshot;
|
|
1459
2027
|
}
|
|
1460
2028
|
sendMessage(text, attachments) {
|
|
1461
2029
|
if (this.#parked) throw new Error("session is parked");
|
|
@@ -1638,15 +2206,14 @@ var AiSdkRunner = class {
|
|
|
1638
2206
|
Promise.resolve(this.#config.onClose?.()).catch(() => {});
|
|
1639
2207
|
} catch {}
|
|
1640
2208
|
}
|
|
2209
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
2210
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
2211
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
2212
|
+
eventAt(seq) {
|
|
2213
|
+
return this.#events.find((event) => event.seq === seq);
|
|
2214
|
+
}
|
|
1641
2215
|
subscribe(listener, afterSeq = 0, options) {
|
|
1642
|
-
|
|
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);
|
|
2216
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
1650
2217
|
}
|
|
1651
2218
|
#scheduleTurn() {
|
|
1652
2219
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -2036,9 +2603,7 @@ var AiSdkRunner = class {
|
|
|
2036
2603
|
this.#lastActivityAt = event.ts;
|
|
2037
2604
|
this.#activityCount += transcriptActivity(body);
|
|
2038
2605
|
this.#events.push(event);
|
|
2039
|
-
|
|
2040
|
-
listener(event);
|
|
2041
|
-
} catch {}
|
|
2606
|
+
this.#subscribers.emit(event);
|
|
2042
2607
|
}
|
|
2043
2608
|
};
|
|
2044
2609
|
function turnUsage(accum) {
|
|
@@ -3795,7 +4360,7 @@ var CodexRunner = class {
|
|
|
3795
4360
|
/** {@link CodexRunnerConfig.cwd}, checked once in the constructor. */
|
|
3796
4361
|
#cwd;
|
|
3797
4362
|
#events = [];
|
|
3798
|
-
#
|
|
4363
|
+
#subscribers = new SubscriberSet();
|
|
3799
4364
|
#seq = 0;
|
|
3800
4365
|
#activityCount = 0;
|
|
3801
4366
|
#status = "starting";
|
|
@@ -4136,15 +4701,14 @@ var CodexRunner = class {
|
|
|
4136
4701
|
});
|
|
4137
4702
|
this.#setStatus("closed");
|
|
4138
4703
|
}
|
|
4704
|
+
/** See `Runner.eventAt`. A linear scan: the one caller is a reader pressing
|
|
4705
|
+
* "show everything" on one row, so a per-runner seq index would be a map
|
|
4706
|
+
* maintained on every emit to save a walk nobody makes twice a minute. */
|
|
4707
|
+
eventAt(seq) {
|
|
4708
|
+
return this.#events.find((event) => event.seq === seq);
|
|
4709
|
+
}
|
|
4139
4710
|
subscribe(listener, afterSeq = 0, options) {
|
|
4140
|
-
|
|
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);
|
|
4711
|
+
return this.#subscribers.subscribe(this.#events, listener, afterSeq, options);
|
|
4148
4712
|
}
|
|
4149
4713
|
#scheduleTurn() {
|
|
4150
4714
|
this.#turnChain = this.#turnChain.then(() => this.#runTurn());
|
|
@@ -5010,9 +5574,7 @@ var CodexRunner = class {
|
|
|
5010
5574
|
this.#lastActivityAt = event.ts;
|
|
5011
5575
|
this.#activityCount += transcriptActivity(body);
|
|
5012
5576
|
this.#events.push(event);
|
|
5013
|
-
|
|
5014
|
-
listener(event);
|
|
5015
|
-
} catch {}
|
|
5577
|
+
this.#subscribers.emit(event);
|
|
5016
5578
|
}
|
|
5017
5579
|
};
|
|
5018
5580
|
//#endregion
|
|
@@ -5446,6 +6008,6 @@ function getEngineAdapter(engine) {
|
|
|
5446
6008
|
return ADAPTERS[engine ?? "claude"];
|
|
5447
6009
|
}
|
|
5448
6010
|
//#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 };
|
|
6011
|
+
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
6012
|
|
|
5451
6013
|
//# sourceMappingURL=index.mjs.map
|