@koda-sl/baker-bridge 0.71.0 → 0.71.2-dev.5e66c2900

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.
@@ -216,6 +216,13 @@ function resultMessage(costUsd, sessionId, modelUsage) {
216
216
  ...(modelUsage ? { modelUsage } : {}),
217
217
  };
218
218
  }
219
+ function streamEvent(event, parentToolUseId) {
220
+ return {
221
+ type: "stream_event",
222
+ event,
223
+ parent_tool_use_id: parentToolUseId ?? null,
224
+ };
225
+ }
219
226
  function assistantMessage(uuid, text, sessionId, extras) {
220
227
  return {
221
228
  type: "assistant",
@@ -573,6 +580,75 @@ describe("BridgeJobExecutor", () => {
573
580
  { target: { threadId: "thread_zero", turnId: "turn_zero" }, tokens: 96000, window: 200000 },
574
581
  ]);
575
582
  });
583
+ // The shape production actually sends: OpenRouter zeroes `message_start` and
584
+ // the assembled assistant message with it, and reports the real occupancy only
585
+ // on `message_delta`. Read from the message alone, the meter never had a single
586
+ // reading to show.
587
+ it("reads the meter off the stream when the assistant message reports no usage", async () => {
588
+ const relay = recordingRelay();
589
+ const executor = createExecutor({
590
+ relay,
591
+ queryResults: [
592
+ scriptedMessages(streamEvent({
593
+ type: "message_start",
594
+ message: { usage: { input_tokens: 0, cache_read_input_tokens: null, cache_creation_input_tokens: null } },
595
+ }), streamEvent({
596
+ type: "message_delta",
597
+ delta: { stop_reason: "end_turn" },
598
+ usage: { input_tokens: 5000, output_tokens: 300, cache_read_input_tokens: 90000 },
599
+ }), assistantMessage("assistant_zeroed", "Done.", "session_next", {
600
+ usage: { input_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
601
+ }), resultMessage(0.02, "session_next", {
602
+ "claude-main": {
603
+ inputTokens: 0,
604
+ outputTokens: 0,
605
+ cacheReadInputTokens: 0,
606
+ cacheCreationInputTokens: 0,
607
+ webSearchRequests: 0,
608
+ costUSD: 0.02,
609
+ contextWindow: 200000,
610
+ maxOutputTokens: 32000,
611
+ },
612
+ })),
613
+ ],
614
+ });
615
+ await executor.execute({ threadId: "thread_stream_usage", turnId: "turn_stream_usage", prompt: "hi", history: history([]) }, activeControls());
616
+ expect(relay.contextUsageUpdates).toEqual([
617
+ { target: { threadId: "thread_stream_usage", turnId: "turn_stream_usage" }, tokens: 95000, window: 200000 },
618
+ ]);
619
+ });
620
+ it("ignores subagent stream usage — only the main conversation drives the meter", async () => {
621
+ const relay = recordingRelay();
622
+ const executor = createExecutor({
623
+ relay,
624
+ queryResults: [
625
+ scriptedMessages(streamEvent({
626
+ type: "message_delta",
627
+ delta: { stop_reason: "end_turn" },
628
+ usage: { input_tokens: 5000, cache_read_input_tokens: 90000 },
629
+ }), streamEvent({
630
+ type: "message_delta",
631
+ delta: { stop_reason: "end_turn" },
632
+ usage: { input_tokens: 400000, cache_read_input_tokens: 0 },
633
+ }, "toolu_subagent"), resultMessage(0.02, "session_next", {
634
+ "claude-main": {
635
+ inputTokens: 0,
636
+ outputTokens: 0,
637
+ cacheReadInputTokens: 0,
638
+ cacheCreationInputTokens: 0,
639
+ webSearchRequests: 0,
640
+ costUSD: 0.02,
641
+ contextWindow: 200000,
642
+ maxOutputTokens: 32000,
643
+ },
644
+ })),
645
+ ],
646
+ });
647
+ await executor.execute({ threadId: "thread_sub_stream", turnId: "turn_sub_stream", prompt: "hi", history: history([]) }, activeControls());
648
+ expect(relay.contextUsageUpdates).toEqual([
649
+ { target: { threadId: "thread_sub_stream", turnId: "turn_sub_stream" }, tokens: 95000, window: 200000 },
650
+ ]);
651
+ });
576
652
  it("ignores subagent request usage — only the main conversation drives the meter", async () => {
577
653
  const relay = recordingRelay();
578
654
  const executor = createExecutor({
@@ -747,11 +823,13 @@ describe("BridgeJobExecutor", () => {
747
823
  await executor.execute({ threadId: "thread_ordinary", turnId: "turn_ordinary", prompt: "Make the hero blue", history: history([]) }, activeControls());
748
824
  expect(queryInputs[0]?.options.settings ?? {}).not.toHaveProperty("autoCompactEnabled");
749
825
  });
750
- it("compacts and retries once when publish genuinely no longer fits the window", async () => {
826
+ it("restarts publish on a fresh session when the conversation no longer fits", async () => {
751
827
  const queryInputs = [];
828
+ const clearCalls = [];
752
829
  const executor = createExecutor({
753
830
  loadedBoundary: cleanBoundary("session_overflow"),
754
831
  queryInputs,
832
+ clearCalls,
755
833
  queryResults: [
756
834
  // The CLI announces its session id before the first model request, so the
757
835
  // refusal arrives with an id but no work done — the retry still owes the
@@ -761,15 +839,43 @@ describe("BridgeJobExecutor", () => {
761
839
  subtype: "init",
762
840
  session_id: "session_overflow_live",
763
841
  }),
764
- scriptedMessages(resultMessage(0.4, "session_after_compaction")),
842
+ scriptedMessages(resultMessage(0.4, "session_restarted")),
765
843
  ],
766
844
  });
767
845
  const outcome = await executor.execute({ threadId: "thread_overflow", turnId: "turn_overflow", prompt: "/publish", history: history([]) }, activeControls());
768
846
  expect(outcome.kind).toBe("completed");
769
847
  expect(queryInputs[0]?.options.settings).toMatchObject({ autoCompactEnabled: false });
770
- expect(queryInputs[1]?.options.settings ?? {}).not.toHaveProperty("autoCompactEnabled");
848
+ // The oversized session is abandoned, not summarized: the retry resumes
849
+ // nothing and delivers the `/publish` the turn still owes.
850
+ expect(queryInputs[1]?.options.resume).toBeUndefined();
771
851
  expect(queryInputs[1]?.prompt).toBe("/publish");
772
- expect(queryInputs[1]?.options.resume).toBe("session_overflow");
852
+ expect(clearCalls).toEqual([{ threadId: "thread_overflow", reason: "sdk_error" }]);
853
+ });
854
+ it("does not silently restart an ordinary message that overflows", async () => {
855
+ const queryInputs = [];
856
+ const executor = createExecutor({
857
+ loadedBoundary: cleanBoundary("session_ordinary_overflow"),
858
+ queryInputs,
859
+ queryResults: [
860
+ contextOverflowQuery({
861
+ type: "system",
862
+ subtype: "init",
863
+ session_id: "session_ordinary_overflow_live",
864
+ }),
865
+ ],
866
+ });
867
+ const outcome = await executor.execute({
868
+ threadId: "thread_ordinary_overflow",
869
+ turnId: "turn_ordinary_overflow",
870
+ prompt: "Make the hero blue",
871
+ history: history([{ role: "user", text: "The brand colour is teal", createdAt: 1 }]),
872
+ }, activeControls());
873
+ // A restart would answer from rehydrated history instead of the live session,
874
+ // and an ordinary message — unlike `/publish` — is answered *from* that
875
+ // context. Silently trading it for a shorter one degrades the reply with no
876
+ // explanation, so this turn says so instead.
877
+ expect(queryInputs).toHaveLength(1);
878
+ expect(outcome.kind).toBe("errored");
773
879
  });
774
880
  it("resumes the in-flight publish session when the overflow lands mid-turn", async () => {
775
881
  const queryInputs = [];
@@ -808,6 +914,52 @@ describe("BridgeJobExecutor", () => {
808
914
  // progress — the retry still owes the turn its `/publish`.
809
915
  expect(queryInputs[1]?.prompt).toBe("/publish");
810
916
  });
917
+ // How the failure actually reached production: the CLI does not throw, it ends
918
+ // the turn on an error *result* carrying the provider's refusal.
919
+ it("restarts publish when the overflow arrives as an error result", async () => {
920
+ const relay = recordingRelay();
921
+ const queryInputs = [];
922
+ const executor = createExecutor({
923
+ loadedBoundary: cleanBoundary("session_result_overflow"),
924
+ relay,
925
+ queryInputs,
926
+ queryResults: [
927
+ scriptedMessages({ type: "system", subtype: "init", session_id: "session_result_overflow_live" }, assistantMessage("assistant_result_refusal", "Prompt is too long", "session_result_overflow_live"), {
928
+ type: "result",
929
+ subtype: "error_during_execution",
930
+ is_error: true,
931
+ errors: ["Prompt is too long"],
932
+ total_cost_usd: 0.2,
933
+ session_id: "session_result_overflow_live",
934
+ }),
935
+ scriptedMessages(assistantMessage("assistant_result_published", "Your changes are live.", "session_result_restarted"), resultMessage(0.4, "session_result_restarted")),
936
+ ],
937
+ });
938
+ const outcome = await executor.execute({ threadId: "thread_result_overflow", turnId: "turn_result_overflow", prompt: "/publish", history: history([]) }, activeControls());
939
+ expect(outcome.kind).toBe("completed");
940
+ expect(queryInputs[1]?.options.resume).toBeUndefined();
941
+ expect(queryInputs[1]?.prompt).toBe("/publish");
942
+ expect(JSON.stringify(relay.assistantEvents)).not.toContain("Prompt is too long");
943
+ });
944
+ it("reports a turn the restart could not save in product language", async () => {
945
+ const relay = recordingRelay();
946
+ const executor = createExecutor({
947
+ loadedBoundary: cleanBoundary("session_doomed"),
948
+ relay,
949
+ queryResults: [
950
+ contextOverflowQuery({ type: "system", subtype: "init", session_id: "session_doomed_live" }, assistantMessage("assistant_doomed", "Prompt is too long", "session_doomed_live")),
951
+ // Even the restarted session is refused: the turn is out of moves.
952
+ contextOverflowQuery(assistantMessage("assistant_doomed_again", "Prompt is too long", "session_doomed_restarted")),
953
+ ],
954
+ });
955
+ const outcome = await executor.execute({ threadId: "thread_doomed", turnId: "turn_doomed", prompt: "/publish", history: history([]) }, activeControls());
956
+ expect(outcome.kind).toBe("errored");
957
+ const errors = outcome.kind === "errored" ? outcome.errors.join(" ") : "";
958
+ expect(errors).not.toContain("Prompt is too long");
959
+ expect(errors).toContain("Send your message again");
960
+ // Said once, by the product — the provider's own words never reach the chat.
961
+ expect(JSON.stringify(relay.assistantEvents)).not.toContain("Prompt is too long");
962
+ });
811
963
  it("does not resurrect a dropped refusal as end-of-turn text", async () => {
812
964
  const relay = recordingRelay();
813
965
  const executor = createExecutor({