From 575579d68ac4bae09f8426a345d4a3eac1e90c67 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sat, 31 Oct 2026 16:19:39 -0700 Subject: [PATCH 001/322] fix(web): model picker stays put with long names and agent options (#28409) Co-authored-by: Claude Opus 5.5 --- .../src/provider/AcpRegistryProbe.test.ts | 1 + apps/server/src/provider/GrokProvider.test.ts | 4 +- .../chat/ComposerModelEffort.browser.test.tsx | 125 +++++++ .../components/chat/ComposerModelEffort.tsx | 350 ++++++++++++++---- .../src/components/chat/EffortGlyph.test.ts | 4 +- apps/web/src/components/chat/EffortGlyph.tsx | 4 +- apps/web/src/components/chat/ModelListRow.tsx | 9 +- .../components/chat/ModelPickerContent.tsx | 20 +- .../components/chat/ProviderModelPicker.tsx | 47 ++- apps/web/src/components/chat/TraitsPicker.tsx | 6 +- .../restingComposerControlsMeasurement.ts | 12 +- .../settings/ProviderModelsSection.tsx | 6 +- apps/web/src/components/ui/menu.tsx | 3 + packages/client-runtime/src/modelTraits.ts | 29 +- packages/contracts/src/model.ts | 2 + .../src/server/sessionConfig.test.ts | 1 + .../provider-acp/src/server/sessionConfig.ts | 1 + packages/provider-grok/src/server/status.ts | 7 + 18 files changed, 506 insertions(+), 125 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerModelEffort.browser.test.tsx diff --git a/apps/server/src/provider/AcpRegistryProbe.test.ts b/apps/server/src/provider/AcpRegistryProbe.test.ts index bc7c14d3b3c8a..e6835159f9e7f 100644 --- a/apps/server/src/provider/AcpRegistryProbe.test.ts +++ b/apps/server/src/provider/AcpRegistryProbe.test.ts @@ -229,6 +229,7 @@ describe("ACP Registry probe", () => { { id: "medium", label: "Medium" }, { id: "high", label: "High" }, ], + role: "reasoning", }, ]); }); diff --git a/apps/server/src/provider/GrokProvider.test.ts b/apps/server/src/provider/GrokProvider.test.ts index 4ecc0ad28971b..8139df621f98c 100644 --- a/apps/server/src/provider/GrokProvider.test.ts +++ b/apps/server/src/provider/GrokProvider.test.ts @@ -129,9 +129,9 @@ describe("buildGrokModelCapabilities", () => { type: "select", currentValue: "medium", options: [ - { id: "xhigh", label: "Extra High Effort", isDefault: true }, - { id: "high", label: "High Effort" }, { id: "medium", label: "Medium Effort" }, + { id: "high", label: "High Effort" }, + { id: "xhigh", label: "Extra High Effort", isDefault: true }, ], }, ]); diff --git a/apps/web/src/components/chat/ComposerModelEffort.browser.test.tsx b/apps/web/src/components/chat/ComposerModelEffort.browser.test.tsx new file mode 100644 index 0000000000000..3d704b5209305 --- /dev/null +++ b/apps/web/src/components/chat/ComposerModelEffort.browser.test.tsx @@ -0,0 +1,125 @@ +import { + ProviderDriverKind, + ProviderInstanceId, + type ProviderOptionSelection, + type ServerProviderModel, +} from "@t3tools/contracts"; +import { createModelCapabilities } from "@t3tools/shared/model"; +import { useState } from "react"; +import { expect, it } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; + +import { render } from "../../../test/render"; +import { ComposerEffortColumn, describeComposerEffort } from "./ComposerModelEffort"; +import type { TraitsSelectionInput } from "./TraitsPicker"; + +// Shaped like Devin over ACP: its session mode comes first, and its reasoning id is its own. +const models: ReadonlyArray = [ + { + slug: "fusion", + name: "Fusion (Claude Opus 5.5 Medium + SWE-2 High)", + isCustom: false, + capabilities: createModelCapabilities({ + optionDescriptors: [ + { + id: "mode", + label: "Session Mode", + type: "select", + currentValue: "bypass", + options: [ + { id: "code", label: "Code" }, + { id: "plan", label: "Plan" }, + { id: "bypass", label: "Bypass Permissions" }, + ], + }, + { + id: "thought_level", + label: "Thinking", + type: "select", + role: "reasoning", + currentValue: "medium", + options: [ + { id: "low", label: "Low" }, + { id: "medium", label: "Medium" }, + { id: "high", label: "High" }, + ], + }, + { + id: "speed", + label: "Speed", + type: "select", + currentValue: "standard", + options: [ + { id: "standard", label: "Standard" }, + { id: "fast", label: "Fast" }, + ], + }, + { id: "auto_approve", label: "Auto-approve tools", type: "boolean", currentValue: false }, + ], + }), + }, +]; + +function traitsInput( + modelOptions: ReadonlyArray, + onModelOptionsChange: (next: ReadonlyArray | undefined) => void, +): TraitsSelectionInput { + return { + provider: ProviderDriverKind.make("acpRegistry"), + instanceId: ProviderInstanceId.make("devin"), + models, + model: "fusion", + modelOptions, + planModeEnabled: false, + onModelOptionsChange, + }; +} + +function Column() { + const [options, setOptions] = useState>([]); + const traits = traitsInput(options, (next) => setOptions(next ?? [])); + return ( + <> +
+ +
+

{describeComposerEffort(traits, "bars", "sm").suffix}

+ + ); +} + +it("draws an agent's reasoning as bars and keeps its mode out of the levels", async () => { + await using screen = await render(, { router: false }); // no routes involved + const status = screen.getByRole("status"); + expect(status.element().querySelector("[data-composer-effort-glyph]")).not.toBeNull(); + await expect.element(status).toHaveTextContent(""); + + const levels = screen.getByRole("radiogroup", { name: "Thinking" }); + await expect.element(levels.getByRole("radio")).toHaveLength(3); + await expect.element(screen.getByRole("radio", { name: "Plan" })).not.toBeInTheDocument(); + + // Up and Down walk from the levels through the option rows without opening their menus. + await userEvent.tab(); + await expect.element(screen.getByRole("radio", { name: "Medium" })).toHaveFocus(); + await userEvent.keyboard("{ArrowDown}{ArrowDown}{ArrowDown}"); + const speed = screen.getByRole("button", { name: "Speed: Standard" }); + await expect.element(speed).toHaveFocus(); + await expect.element(page.getByRole("menuitemradio")).not.toBeInTheDocument(); + await userEvent.keyboard("{ArrowRight}"); + await expect.element(page.getByRole("menuitemradio", { name: "Standard" })).toHaveFocus(); + await userEvent.keyboard("{ArrowLeft}"); + await expect.element(page.getByRole("menuitemradio")).not.toBeInTheDocument(); + await expect.element(speed).toHaveFocus(); + + await userEvent.click(screen.getByText("Auto-approve tools")); + await expect + .element(screen.getByRole("switch", { name: "Auto-approve tools" })) + .toHaveAttribute("aria-checked", "true"); + + await userEvent.click(screen.getByRole("button", { name: "Session Mode: Bypass Permissions" })); + await userEvent.click(page.getByRole("menuitemradio", { name: "Plan" })); + await expect + .element(screen.getByRole("button", { name: "Session Mode: Plan" })) + .toBeInTheDocument(); + await expect.element(page.getByRole("menuitemradio")).not.toBeInTheDocument(); +}); diff --git a/apps/web/src/components/chat/ComposerModelEffort.tsx b/apps/web/src/components/chat/ComposerModelEffort.tsx index 5ce77c60b1078..3a863936101b2 100644 --- a/apps/web/src/components/chat/ComposerModelEffort.tsx +++ b/apps/web/src/components/chat/ComposerModelEffort.tsx @@ -1,16 +1,25 @@ import { buildTraitsTriggerDisplay, getDescriptorStringValue, - REASONING_SELECT_DESCRIPTOR_IDS, + isReasoningControl, selectableProviderOptionChoices, } from "@t3tools/client-runtime/model-traits"; import type { ProviderOptionDescriptor, RuntimeMode } from "@t3tools/contracts"; import type { EffortDisplay } from "@t3tools/contracts/settings"; -import { CheckIcon, ChevronUpIcon } from "lucide-react"; -import { type KeyboardEvent, type ReactNode, use, useRef } from "react"; +import { getProviderOptionCurrentLabel } from "@t3tools/shared/model"; +import { CheckIcon, ChevronRightIcon, ChevronUpIcon } from "lucide-react"; +import { type KeyboardEvent, type ReactNode, use, useRef, useState } from "react"; import { cn } from "~/lib/utils"; -import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; +import { + Menu, + MenuPopup, + MenuRadioGroup, + MenuRadioItem, + MenuRadioItemIndicator, + MenuTrigger, +} from "../ui/menu"; +import { Switch } from "../ui/switch"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import type { ComposerControlSize } from "./ComposerControl"; import { ComposerPermissionsSection, type RuntimeModeOption } from "./ComposerFooterControls"; @@ -51,8 +60,8 @@ type ColumnControl = | { kind: "boolean"; descriptor: BooleanDescriptor }; /** - * The column lists the model's reasoning control first, under the header that names it, then - * its other options. + * The column lists the model's reasoning levels under the header that names it. Other options, + * such as an agent's session mode, sit below as one row each. */ function columnControls( provider: TraitsSelectionInput["provider"], @@ -64,8 +73,7 @@ function columnControls( ...columnSelects.map((descriptor) => ({ kind: "select" as const, descriptor })), ...columnToggles.map((descriptor) => ({ kind: "boolean" as const, descriptor })), ]; - const effort = - controls.find((control) => REASONING_SELECT_DESCRIPTOR_IDS.has(control.descriptor.id)) ?? null; + const effort = controls.find((control) => isReasoningControl(control.descriptor)) ?? null; return { effort, others: controls.filter((control) => control !== effort) }; } @@ -94,20 +102,40 @@ export function describeComposerEffort( modelSelection, reportedModelSelection: traits.reportedModelSelection, }); + // The same reasoning control the column lists, wherever the agent put it among its options. + const reasoning = + columnControls(traits.provider, visibility.selectDescriptors, visibility.booleanDescriptors) + .effort?.descriptor ?? null; + const ultrathink = visibility.ultrathinkPromptControlled && reasoning === primary; const glyph = - primary && REASONING_SELECT_DESCRIPTOR_IDS.has(primary.id) + reasoning?.type === "select" ? resolveEffortGlyph( - primary, - visibility.ultrathinkPromptControlled + reasoning, + ultrathink ? "ultrathink" - : getDescriptorStringValue(primary, modelSelection, traits.reportedModelSelection), + : getDescriptorStringValue(reasoning, modelSelection, traits.reportedModelSelection), ) : null; - // Without a reasoning select the whole label is the word; it already names speed and the rest. // Workflow levels such as Ultracode always show their name; the setting names every level. const word = glyph ? (glyph.special ?? (effortDisplay === "label" ? glyph.label : null)) - : display.label; + : !reasoning + ? null + : reasoning.type === "boolean" + ? `${reasoning.label} ${reasoning.currentValue === true ? "On" : "Off"}` + : ultrathink + ? "Ultrathink" + : getProviderOptionCurrentLabel(reasoning, modelSelection, traits.reportedModelSelection); + const fast = display.speedLabel === "Fast" || display.speedLabel === "Ultrafast"; + // Agent options such as modes live in the picker; the trigger adds only the context window. + const contextWindow = + visibility.contextWindowDescriptor && + getProviderOptionCurrentLabel( + visibility.contextWindowDescriptor, + modelSelection, + traits.reportedModelSelection, + ); + if (!word && !glyph && !fast && !contextWindow) return { suffix: null, detail: display.label }; return { detail: display.label, suffix: ( @@ -118,22 +146,22 @@ export function describeComposerEffort( size === "sm" && "-ms-0.5", )} > - {word !== null ? ( - {word} + {word ? ( + + {word} + ) : glyph ? ( ) : null} - {glyph && (display.speedLabel === "Fast" || display.speedLabel === "Ultrafast") ? ( - - ) : null} - {glyph && display.otherLabels.length > 0 ? ( + {fast ? : null} + {contextWindow ? ( - {display.otherLabels.join(" · ")} + {contextWindow} ) : null} @@ -150,10 +178,10 @@ function ColumnRow(props: { /** Shown on hover; the column is too narrow to print it under the label. */ description?: string | undefined; selected: boolean; + /** The column's one tab stop: the selected level, else the first when none shows as selected. */ + tabStop: boolean; isDefault?: boolean | undefined; disabled?: boolean; - /** A level of the reasoning control, where focus lands after a model pick. */ - level: boolean; onPick: () => void; }) { const rowProps = { @@ -162,9 +190,10 @@ function ColumnRow(props: { "aria-checked": props.selected, disabled: props.disabled, "data-effort-row": true, - "data-effort-level": props.level || undefined, - // The reasoning control holds the column's one tab stop; Up and Down move between rows. - tabIndex: props.selected && props.level ? 0 : -1, + // Focus lands on a level after a model pick. + "data-effort-level": true, + // Up and Down move between rows. + tabIndex: props.tabStop ? 0 : -1, className: COLUMN_ROW, onClick: props.onPick, } as const; @@ -194,6 +223,8 @@ function ColumnRow(props: { function moveBetweenRows(event: KeyboardEvent) { if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return; + // An option's side menu is a React child here, but it moves through its own items. + if (!event.currentTarget.contains(event.target as Node)) return; const rows = Array.from( event.currentTarget.querySelectorAll("[data-effort-row]:not(:disabled)"), ); @@ -230,66 +261,222 @@ function EffortLevels(props: { traits: TraitsSelectionInput }) { ); } if (!effort && others.length === 0) return ; - const renderControl = (control: ColumnControl) => { - const isEffort = control === effort; - const { descriptor } = control; - if (control.kind === "boolean") { - return ( + // Ultrathink in the prompt locks the levels; the first option then holds the tab stop. + const locked = + effort?.descriptor.id === traits.primarySelectDescriptor?.id && traits.ultrathinkInBodyText; + return ( + <> + {effort ? : null} + {others.length > 0 ? (
- {isEffort ? null :
{descriptor.label}
} - {([true, false] as const).map((value) => ( - traits.changeBoolean(descriptor.id, value)} - /> - ))} + {others.map((control, index) => + control.kind === "boolean" ? ( + + ) : ( + + ), + )}
- ); - } - const locked = - descriptor.id === traits.primarySelectDescriptor?.id && traits.ultrathinkInBodyText; - const value = traits.selectValue(control.descriptor); + ) : null} + + ); +} + +type TraitsSelection = ReturnType; + +function EffortRows(props: { control: ColumnControl; traits: TraitsSelection; locked: boolean }) { + const { control, traits } = props; + const { descriptor } = control; + if (control.kind === "boolean") { return ( -
- {isEffort ? null :
{descriptor.label}
} - {locked ? ( -
- {ULTRATHINK_IN_BODY_NOTE} -
- ) : null} - {selectableProviderOptionChoices(control.descriptor).map((option) => ( +
+ {([true, false] as const).map((value) => ( traits.changeSelect(control.descriptor, option.id)} + key={String(value)} + label={value ? "On" : "Off"} + selected={(control.descriptor.currentValue === true) === value} + tabStop={(control.descriptor.currentValue === true) === value} + onPick={() => traits.changeBoolean(descriptor.id, value)} /> ))}
); - }; + } + const { locked } = props; + const value = traits.selectValue(control.descriptor); + const choices = selectableProviderOptionChoices(control.descriptor); + // An "Ultrathink:" prefix selects a level the column does not offer. + const shown = choices.some((option) => option.id === value); return ( - <> - {effort ? renderControl(effort) : null} - {others.map(renderControl)} - +
+ {locked ? ( +
+ {ULTRATHINK_IN_BODY_NOTE} +
+ ) : null} + {choices.map((option, index) => ( + traits.changeSelect(control.descriptor, option.id)} + /> + ))} +
+ ); +} + +const OPTION_ROW = cn(COLUMN_ROW, "h-auto py-1"); + +/** An option's name over its current value. */ +function OptionRowText(props: { label: string; value: string }) { + return ( + + {props.label} + {props.value} + + ); +} + +/** + * One select besides reasoning, such as an agent's session mode. It names the current choice and + * opens the rest beside the column, so any number of options leaves the levels in place. + */ +function OptionMenuRow(props: { + descriptor: SelectDescriptor; + traits: TraitsSelection; + /** Holds the column's tab stop when the model has no levels. */ + tabStop: boolean; +}) { + const { descriptor, traits } = props; + const [open, setOpen] = useState(false); + const popupRef = useRef(null); + const choices = selectableProviderOptionChoices(descriptor); + const value = traits.selectValue(descriptor); + const current = choices.find((choice) => choice.id === value)?.label ?? ""; + return ( + { + if (!isOpen) return; + const checked = popupRef.current?.querySelector( + '[role="menuitemradio"][aria-checked="true"]', + ); + checked?.focus({ preventScroll: true }); + checked?.scrollIntoView({ block: "nearest" }); + }} + > + {/* No hover opening: an opened menu takes focus, which would steal typing from search. */} + { + if (event.key !== "ArrowRight") return; + event.preventDefault(); + event.stopPropagation(); + setOpen(true); + }} + /> + } + > + + + { + if (event.key !== "ArrowLeft") return; + event.preventDefault(); + event.stopPropagation(); + setOpen(false); + }} + > + { + if (next !== value) traits.changeSelect(descriptor, next); + }} + > + {choices.map((choice) => ( + + + + {choice.label} + {choice.description ? ( + + {choice.description} + + ) : null} + + {choice.isDefault ? ( + Default + ) : null} + + + + ))} + + + + ); +} + +/** A two-state option toggles in place; a menu would add a click for nothing. */ +function OptionSwitchRow(props: { + descriptor: BooleanDescriptor; + traits: TraitsSelection; + tabStop: boolean; +}) { + const { descriptor, traits } = props; + const checked = descriptor.currentValue === true; + return ( + ); } @@ -303,20 +490,22 @@ function NoEffortRow() { export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | null }) { const picker = use(DockedPickerContext); const visibility = props.traits ? getTraitsSectionVisibility(props.traits) : null; - const effort = + const controls = props.traits && visibility ? columnControls( props.traits.provider, visibility.selectDescriptors, visibility.booleanDescriptors, - ).effort + ) : null; return ( <> {/* Matches the search row's box, so one rule runs across the top of the popup. */}
- {effort?.descriptor.label ?? "Effort"} + + {controls?.effort?.descriptor.label ?? (controls?.others.length ? "Options" : "Effort")} + {picker.modelLabel} @@ -325,7 +514,8 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul {/* pt-2.5 puts the first level on the same line as the first model. */}
{props.traits ? : }
diff --git a/apps/web/src/components/chat/EffortGlyph.test.ts b/apps/web/src/components/chat/EffortGlyph.test.ts index 712e4b796ba61..0b73e89e4f833 100644 --- a/apps/web/src/components/chat/EffortGlyph.test.ts +++ b/apps/web/src/components/chat/EffortGlyph.test.ts @@ -45,9 +45,11 @@ describe("resolveEffortGlyph", () => { }); }); - it("falls back to the word when the value is unknown or the scale has one level", () => { + it("falls back to the word when the value is unknown or the scale is too short or long", () => { expect(resolveEffortGlyph(claude, "unknown")).toBeNull(); expect(resolveEffortGlyph(descriptor(["high"]), "high")).toBeNull(); + const levels = Array.from({ length: 8 }, (_, index) => `level${index}`); + expect(resolveEffortGlyph(descriptor(levels), "level0")).toBeNull(); }); }); diff --git a/apps/web/src/components/chat/EffortGlyph.tsx b/apps/web/src/components/chat/EffortGlyph.tsx index 39a46d2564901..7614e41c496a6 100644 --- a/apps/web/src/components/chat/EffortGlyph.tsx +++ b/apps/web/src/components/chat/EffortGlyph.tsx @@ -14,6 +14,8 @@ type SelectDescriptor = Extract; // Max is named too, so the full scale stops short of it. const NAMED_LEVELS = new Set(["ultracode", "max"]); const OFF_LEVELS = new Set(["off", "none"]); +// More bars than this no longer fit the glyph's height in whole pixels. +const MAX_BARS = 7; /** * Maps a reasoning select to bars: one per ordinary level, filled up to the current one, with an @@ -22,7 +24,7 @@ const OFF_LEVELS = new Set(["off", "none"]); export function resolveEffortGlyph(descriptor: SelectDescriptor, current: string | null) { const special = new Set([...(descriptor.promptInjectedValues ?? []), ...NAMED_LEVELS]); const levels = descriptor.options.filter((option) => !special.has(option.id)); - if (levels.length < 2 || current === null) return null; + if (levels.length < 2 || levels.length > MAX_BARS || current === null) return null; const startsAtOff = OFF_LEVELS.has(levels[0]!.id); const count = startsAtOff ? levels.length - 1 : levels.length; if (special.has(current)) { diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx index 9928530b97f2a..e70c32b064a3e 100644 --- a/apps/web/src/components/chat/ModelListRow.tsx +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -67,7 +67,14 @@ export const ModelListRow = memo(function ModelListRow(props: { badgeClassName="right-[-0.125rem] bottom-[-0.125rem] h-3 min-w-3 px-0.5 text-5xs" />
-
+
{ + const name = event.currentTarget; + name.title = name.scrollWidth > name.clientWidth ? getDisplayModelName(model) : ""; + }} + > {getDisplayModelName(model)} {/* The logo is decorative, so name the provider for screen readers. */} , {props.providerDisplayName} diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 742a7cf6b09fd..f040df764cda9 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -366,6 +366,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { docked?: boolean; /** Right at the end of the search text moves into the popup's effort column. */ onFocusEffort?: () => void; + /** Closes the list column, inside the popup's fixed height. */ + listFooter?: ReactNode; }) { const { searchInputRef, @@ -838,16 +840,17 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ref={pickerContentRef} className={cn( "relative flex w-screen max-w-90 flex-row overflow-hidden", - props.docked - ? "max-sm:h-56 max-sm:min-h-0! max-sm:w-full max-sm:max-w-none" - : "max-h-86.5", + props.docked ? "max-sm:h-56 max-sm:w-full max-sm:max-w-none" : "max-h-86.5", )} + // Docked, the popup sets the height; a minimum here would overflow short windows. style={ - isSearching - ? { height: searchHeight } - : { - minHeight: `min(calc(var(--spacing) * ${tallestTabUnits + LIST_CHROME_UNITS} + 1px), calc(var(--spacing) * 86.5))`, - } + props.docked + ? undefined + : isSearching + ? { height: searchHeight } + : { + minHeight: `min(calc(var(--spacing) * ${tallestTabUnits + LIST_CHROME_UNITS} + 1px), calc(var(--spacing) * 86.5))`, + } } data-model-picker-content="true" > @@ -1072,6 +1075,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { onRequestClose={onRequestClose} onOpenProviderSetup={onOpenProviderSetup} /> + {props.listFooter}
diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 1680e8d426b0b..848b1ee4532db 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -37,7 +37,9 @@ function lockBackgroundScroll(popup: HTMLElement | null) { const shouldAllowOverlayScroll = (target: EventTarget | null) => { return ( target instanceof Element && - target.closest("[data-model-picker-content], [data-model-picker-effort]") + target.closest( + "[data-model-picker-content], [data-model-picker-effort], [data-model-picker-option-menu]", + ) ); }; const preventBackgroundWheel = (event: WheelEvent) => { @@ -154,16 +156,22 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { /** * Focuses the selected reasoning level, else the first level. With `rows: "any"` it falls back - * to the column's other options. False when nothing took focus. + * to the column's first enabled option. False when nothing took focus. */ const focusEffort = (rows: "levels" | "any") => { const column = popupRef.current?.querySelector("[data-model-picker-effort]"); - const first = (selector: string) => - column?.querySelector(`${selector}[aria-checked="true"]:not(:disabled)`) ?? - column?.querySelector(`${selector}:not(:disabled)`); + const level = + column?.querySelector( + '[data-effort-level][aria-checked="true"]:not(:disabled)', + ) ?? column?.querySelector("[data-effort-level]:not(:disabled)"); + // No aria-checked here: an On switch is checked but is not where focus belongs. const row = - first("[data-effort-level]") ?? (rows === "any" ? first("[data-effort-row]") : null); + level ?? + (rows === "any" + ? column?.querySelector("[data-effort-row]:not(:disabled)") + : null); row?.focus({ preventScroll: true }); + row?.scrollIntoView({ block: "nearest" }); return row != null; }; const openTarget = (target: "effort" | "permissions") => { @@ -230,7 +238,11 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { const triggerText = props.triggerLabel ?? multipleLabel ?? triggerTitle; const triggerTextProps = { - className: "min-w-0 flex-1 overflow-hidden truncate", + className: cn( + "min-w-0 flex-1 overflow-hidden truncate", + // Long names such as "Fusion (Claude Opus 5.5 Medium + SWE-2 High)" would push the composer. + props.isComposerOwned && "max-w-56", + ), "data-chat-provider-model-picker-label": "true", } as const; @@ -262,7 +274,16 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { : (props.getModelDisabledReason?.(instanceId, model) ?? null) } onInstanceModelChange={handleInstanceModelChange} - {...(docked ? { docked, onFocusEffort: () => void focusEffort("any") } : {})} + {...(docked + ? { + docked, + onFocusEffort: () => void focusEffort("any"), + // Inside the fixed height, so switching to or from a ChatGPT plan never resizes it. + ...(props.selectedModels === undefined + ? { listFooter: } + : {}), + } + : {})} /> ); @@ -401,12 +422,14 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { > {props.effortColumn ? ( -
-
+
+ {/* One fixed height, so a model with more options never resizes the popup. Short + windows shrink it and keep the footer. */} +
{picker}
{ if (event.key !== "ArrowLeft" || event.altKey || event.ctrlKey || event.metaKey) return; @@ -424,7 +447,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { ) : ( picker )} - {props.selectedModels === undefined ? ( + {props.selectedModels === undefined && !docked ? ( ) : null} diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 4639c0a3ec043..6c723c8bcb4e4 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -4,7 +4,7 @@ import { getTraitSelectChange, replaceDescriptorCurrentValue, selectableProviderOptionChoices, - REASONING_SELECT_DESCRIPTOR_IDS, + isReasoningSelect, } from "@t3tools/client-runtime/model-traits"; import { type ModelSelection, @@ -254,9 +254,7 @@ export function getReasoningTraitStep(input: { input.planModeEnabled, ); if (selected.modelIsUnavailable || selected.ultrathinkInBodyText) return null; - const descriptor = selected.selectDescriptors.find((entry) => - REASONING_SELECT_DESCRIPTOR_IDS.has(entry.id), - ); + const descriptor = selected.selectDescriptors.find(isReasoningSelect); const choices = descriptor ? selectableProviderOptionChoices(descriptor) : []; if (!descriptor || choices.length < 2) return null; const isPrimary = descriptor.id === selected.primarySelectDescriptor?.id; diff --git a/apps/web/src/components/chat/restingComposerControlsMeasurement.ts b/apps/web/src/components/chat/restingComposerControlsMeasurement.ts index 50f6a34148dfb..474df488a6c6b 100644 --- a/apps/web/src/components/chat/restingComposerControlsMeasurement.ts +++ b/apps/web/src/components/chat/restingComposerControlsMeasurement.ts @@ -25,8 +25,18 @@ function providerModelPickerNaturalWidth(picker: HTMLElement): number { const style = getComputedStyle(picker); const label = picker.querySelector('[data-chat-provider-model-picker-label="true"]'); const labelIsCollapsed = label?.clientWidth === 0 && getComputedStyle(label).flexGrow === "0"; + // A label with its own max width, such as a long model name, never grows past it. + const labelMaxWidth = label ? Number.parseFloat(getComputedStyle(label).maxWidth) : Number.NaN; const hiddenLabelWidth = - label && !labelIsCollapsed ? Math.max(0, label.scrollWidth - label.clientWidth) : 0; + label && !labelIsCollapsed + ? Math.max( + 0, + Math.min( + label.scrollWidth, + Number.isFinite(labelMaxWidth) ? labelMaxWidth : Number.POSITIVE_INFINITY, + ) - label.clientWidth, + ) + : 0; const maxWidth = Number.parseFloat(style.maxWidth); const naturalWidth = Math.min( renderedWidth + hiddenLabelWidth, diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index 1bb6c9dde0d4a..3e39d6ccfc12b 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -5,7 +5,7 @@ import { type ProviderInstanceId, type ServerProviderModel, } from "@t3tools/contracts"; -import { REASONING_SELECT_DESCRIPTOR_IDS } from "@t3tools/client-runtime/model-traits"; +import { isReasoningControl } from "@t3tools/client-runtime/model-traits"; import { type CustomModelDefinition, normalizeCustomModelSlug } from "@t3tools/shared/model"; import { useStableCallback } from "../../hooks/useStableCallback"; @@ -31,9 +31,7 @@ const FILTER_THRESHOLD = 8; const PROVIDER_ALIAS_SLUGS = new Set(["default", "auto"]); function hasReasoningControl(model: ServerProviderModel): boolean { - return (model.capabilities?.optionDescriptors ?? []).some((descriptor) => - REASONING_SELECT_DESCRIPTOR_IDS.has(descriptor.id), - ); + return (model.capabilities?.optionDescriptors ?? []).some(isReasoningControl); } // Reasoning is the norm, so only its absence is labeled, and only when other models have it. diff --git a/apps/web/src/components/ui/menu.tsx b/apps/web/src/components/ui/menu.tsx index 47ae92c241e15..1c199afe446f5 100644 --- a/apps/web/src/components/ui/menu.tsx +++ b/apps/web/src/components/ui/menu.tsx @@ -25,6 +25,7 @@ function MenuPopup({ align = "center", alignOffset, side = "bottom", + collisionAvoidance, anchor, keepMounted = false, width = "default", @@ -34,6 +35,7 @@ function MenuPopup({ sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]; alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; side?: MenuPrimitive.Positioner.Props["side"]; + collisionAvoidance?: MenuPrimitive.Positioner.Props["collisionAvoidance"]; anchor?: MenuPrimitive.Positioner.Props["anchor"]; keepMounted?: boolean; width?: "default" | "content"; @@ -44,6 +46,7 @@ function MenuPopup({ align={align} alignOffset={alignOffset} anchor={anchor} + collisionAvoidance={collisionAvoidance} className="z-[130]" data-slot="menu-positioner" render={renderClosingPositioner} diff --git a/packages/client-runtime/src/modelTraits.ts b/packages/client-runtime/src/modelTraits.ts index 23ada4ea11c46..b05f9226defb3 100644 --- a/packages/client-runtime/src/modelTraits.ts +++ b/packages/client-runtime/src/modelTraits.ts @@ -11,11 +11,25 @@ import { } from "@t3tools/shared/model"; import { REASONING_EFFORT_OPTION_IDS } from "./state/threadExecution.ts"; -export const REASONING_SELECT_DESCRIPTOR_IDS: ReadonlySet = new Set([ +const REASONING_DESCRIPTOR_IDS: ReadonlySet = new Set([ ...REASONING_EFFORT_OPTION_IDS, "thinking", ]); +/** The model's reasoning control: a known effort id, or a select its agent marked as reasoning. */ +export function isReasoningControl(descriptor: ProviderOptionDescriptor): boolean { + return ( + (descriptor.type === "select" && descriptor.role === "reasoning") || + REASONING_DESCRIPTOR_IDS.has(descriptor.id) + ); +} + +export function isReasoningSelect( + descriptor: ProviderOptionDescriptor, +): descriptor is Extract { + return descriptor.type === "select" && isReasoningControl(descriptor); +} + export function replaceDescriptorCurrentValue( descriptors: ReadonlyArray, descriptorId: string, @@ -106,8 +120,6 @@ export function buildTraitsTriggerDisplay(input: { label: string; /** "Fast" or "Ultrafast" when a speed option is on. */ speedLabel: string | null; - /** Labels other than reasoning and speed, such as context window or thinking. */ - otherLabels: ReadonlyArray; } { let fastModeFallbackLabel: string | null = null; let speedLabel: string | null = null; @@ -154,20 +166,15 @@ export function buildTraitsTriggerDisplay(input: { input.reportedModelSelection, ); if (typeof label === "string" && label.length > 0) { - if ( - reasoningLabelIndex === -1 && - descriptor.type === "select" && - REASONING_SELECT_DESCRIPTOR_IDS.has(descriptor.id) - ) { + if (reasoningLabelIndex === -1 && isReasoningSelect(descriptor)) { reasoningLabelIndex = labels.length; } labels.push(label); } } - const otherLabels = labels.filter((_, index) => index !== reasoningLabelIndex); if (labels.length === 0 && fastModeFallbackLabel !== null) { - return { label: fastModeFallbackLabel, speedLabel, otherLabels }; + return { label: fastModeFallbackLabel, speedLabel }; } if (speedLabel) { if (reasoningLabelIndex >= 0) { @@ -176,5 +183,5 @@ export function buildTraitsTriggerDisplay(input: { labels.push(speedLabel); } } - return { label: labels.join(" · "), speedLabel, otherLabels }; + return { label: labels.join(" · "), speedLabel }; } diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 87990e7f542f7..1a04cc61c887a 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -27,6 +27,8 @@ export const SelectProviderOptionDescriptor = Schema.Struct({ options: Schema.Array(ProviderOptionChoice), currentValue: Schema.optional(TrimmedNonEmptyString), promptInjectedValues: Schema.optional(Schema.Array(TrimmedNonEmptyString)), + /** Marks the reasoning control when its id is an agent's own, such as an ACP `thought_level`. */ + role: Schema.optional(Schema.Literal("reasoning")), }); export type SelectProviderOptionDescriptor = typeof SelectProviderOptionDescriptor.Type; diff --git a/packages/provider-acp/src/server/sessionConfig.test.ts b/packages/provider-acp/src/server/sessionConfig.test.ts index 308ba1db02ccf..ceb6ceb92783a 100644 --- a/packages/provider-acp/src/server/sessionConfig.test.ts +++ b/packages/provider-acp/src/server/sessionConfig.test.ts @@ -50,6 +50,7 @@ describe("acpProviderOptionDescriptors", () => { { id: "medium", label: "Medium", description: "Balanced" }, ], currentValue: "medium", + role: "reasoning", }, ]); }); diff --git a/packages/provider-acp/src/server/sessionConfig.ts b/packages/provider-acp/src/server/sessionConfig.ts index 46269bb5e0d13..b79fdaebe245d 100644 --- a/packages/provider-acp/src/server/sessionConfig.ts +++ b/packages/provider-acp/src/server/sessionConfig.ts @@ -102,6 +102,7 @@ export function acpProviderOptionDescriptors(input: { ...(currentValue && choices.some((choice) => choice.id === currentValue) ? { currentValue } : {}), + ...(option.category === "thought_level" ? { role: "reasoning" as const } : {}), }); if (descriptors.length === MAX_OPTION_DESCRIPTORS) return descriptors; } diff --git a/packages/provider-grok/src/server/status.ts b/packages/provider-grok/src/server/status.ts index c634c5b80c74e..67908d12385e7 100644 --- a/packages/provider-grok/src/server/status.ts +++ b/packages/provider-grok/src/server/status.ts @@ -126,6 +126,8 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +const GROK_EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]; + function grokReasoningOptionsFromModel(model: EffectAcpSchema.ModelInfo): { readonly options: ReadonlyArray<{ value: string; @@ -183,6 +185,11 @@ function grokReasoningOptionsFromModel(model: EffectAcpSchema.ModelInfo): { const selectedDefault = advertisedDefaults.find((option) => option.value === currentValue)?.value ?? advertisedDefaults[0]?.value; + // Grok advertises its strongest level first; pickers and effort bars read lowest to highest. + const rank = (option: { value: string }) => GROK_EFFORT_RANK.indexOf(option.value); + if (options.every((option) => rank(option) >= 0)) { + options.sort((left, right) => rank(left) - rank(right)); + } return { options: options.map(({ value, label, description }) => ({ value, From 8b74a69019e3d0f5be62dc118c526669002fc195 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sat, 31 Oct 2026 17:54:29 -0700 Subject: [PATCH 002/322] fix(web): background command details open above their button (#28446) Co-authored-by: Claude Opus 5.5 --- .../chat/ComposerBannerStack.browser.test.tsx | 26 +++++++++++++++++++ .../components/chat/ComposerBannerStack.tsx | 4 +-- .../components/chat/ThreadActivityBanners.tsx | 9 ++++++- .../src/state/threadExecution.test.ts | 14 ++++++++++ .../src/state/threadExecution.ts | 11 +++++--- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/chat/ComposerBannerStack.browser.test.tsx b/apps/web/src/components/chat/ComposerBannerStack.browser.test.tsx index bf83b014b7f6f..4c9702abe5f31 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.browser.test.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.browser.test.tsx @@ -1,6 +1,7 @@ import { InfoIcon } from "lucide-react"; import type { ReactNode } from "react"; import { expect, it, vi } from "vite-plus/test"; +import { page, userEvent } from "vite-plus/test/browser"; import "~/index.css"; @@ -123,3 +124,28 @@ it("shows the details button while text or nested text is cut, until it fits aga await screen.rerender({noticeRow("4rem", "Ready")}); await expect.element(details).not.toBeInTheDocument(); }); + +it("opens the details of a long notice right above its button", async () => { + const viewport = { width: window.innerWidth, height: window.innerHeight }; + await page.viewport(1280, 800); + await using _viewport = { + [Symbol.asyncDispose]: () => page.viewport(viewport.width, viewport.height), + }; + const script = Array.from({ length: 400 }, (_, line) => `echo "line ${line}"`).join("\n"); + const container = document.createElement("div"); + container.style.cssText = "position: fixed; bottom: 4rem; right: 4rem;"; + await using screen = await render(noticeRow("40rem", script), { container }); + const details = screen.getByRole("button", { name: "Show details" }); + await userEvent.hover(details); + const popup = page.getByLabelText("Notice details"); + await expect.element(popup).toBeVisible(); + + // A positioner sized to the unscrolled text flips beside the button and covers it. + await vi.waitFor(() => { + const gap = + details.element().getBoundingClientRect().top - + popup.element().getBoundingClientRect().bottom; + expect(gap).toBeGreaterThanOrEqual(0); + expect(gap).toBeLessThan(8); + }); +}); diff --git a/apps/web/src/components/chat/ComposerBannerStack.tsx b/apps/web/src/components/chat/ComposerBannerStack.tsx index 61778f817dc37..ee602dcdac1eb 100644 --- a/apps/web/src/components/chat/ComposerBannerStack.tsx +++ b/apps/web/src/components/chat/ComposerBannerStack.tsx @@ -155,9 +155,7 @@ export function NoticeText({ side="top" className="max-w-[min(30rem,calc(100vw-2rem))] whitespace-normal wrap-anywhere [--inline-button-text-align:start] [--inline-button-white-space:normal] [&_[data-slot=inline-button]]:max-w-full" > - - {children} - + {children} ) : null} diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx index 4c0af2d3472c0..5342bd09b3ca9 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx @@ -191,7 +191,14 @@ export function backgroundWorkNowItem(input: { icon: rows.every(isCommand) ? : , title: only ? ( <> - {verb} {only.task.label} + {verb}{" "} + + {only.task.label} + ) : ( presentation.title diff --git a/packages/client-runtime/src/state/threadExecution.test.ts b/packages/client-runtime/src/state/threadExecution.test.ts index 81afabbe8aa49..ecb050d721ab8 100644 --- a/packages/client-runtime/src/state/threadExecution.test.ts +++ b/packages/client-runtime/src/state/threadExecution.test.ts @@ -25,6 +25,7 @@ import { deriveRunlessWorkStartedAt, deriveThreadActivityRun, deriveThreadRuntime, + presentPendingBackgroundWork, presentProviderGoal, presentRunStop, threadRuntimeHasInterruptibleRun, @@ -488,3 +489,16 @@ describe("presentProviderGoal", () => { ).toBe("1m / 1m tokens"); }); }); + +describe("presentPendingBackgroundWork", () => { + it("names a command by its script, not the shell that wraps it", () => { + const presentation = presentPendingBackgroundWork([ + { + taskId: "call_1", + kind: "command", + description: `/bin/zsh -lc "mv a.ts b.ts\ncat > b.ts <<'EOF'\nexport {};\nEOF"`, + }, + ]); + expect(presentation?.items[0]?.label).toBe("mv a.ts b.ts\ncat > b.ts <<'EOF'\nexport {};\nEOF"); + }); +}); diff --git a/packages/client-runtime/src/state/threadExecution.ts b/packages/client-runtime/src/state/threadExecution.ts index 951e5d06404b3..711f813a3b8ef 100644 --- a/packages/client-runtime/src/state/threadExecution.ts +++ b/packages/client-runtime/src/state/threadExecution.ts @@ -33,6 +33,7 @@ import { type ThreadStructure, type ThreadStructureRuntime, } from "./models.ts"; +import { commandDisplayText } from "../work-log/commandLabel.ts"; import { formatSubagentDisplayTitle } from "./subagentDisplay.ts"; const ACTIVITY_RUN_STATUSES = new Set(["preparing", "starting", "running", "waiting"]); @@ -370,9 +371,13 @@ export function presentPendingBackgroundWork( .map((task): PendingBackgroundWorkItem => { const description = task.description?.trim(); const label = - task.kind === "subagent" && description !== undefined - ? formatSubagentDisplayTitle(description).trim() - : description; + description === undefined + ? undefined + : task.kind === "subagent" + ? formatSubagentDisplayTitle(description).trim() + : task.kind === "command" || task.kind === "monitor" + ? commandDisplayText(description) + : description; return { taskId: task.taskId, kind: task.kind, From 8473d1560d6f765082de371b10fdeb6d838435ef Mon Sep 17 00:00:00 2001 From: Mike Olson Date: Sat, 31 Oct 2026 22:03:01 -0400 Subject: [PATCH 003/322] refactor(desktop): Drop the unused Linux --class switch (#14482) --- apps/desktop/src/app/DesktopEarlyElectronStartup.ts | 2 -- apps/desktop/src/app/DesktopEnvironment.ts | 2 -- apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts | 1 - apps/desktop/src/app/DesktopPreReadyPlatform.ts | 1 - 4 files changed, 6 deletions(-) diff --git a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts index 00a355bf49617..3c571bcf02b1f 100644 --- a/apps/desktop/src/app/DesktopEarlyElectronStartup.ts +++ b/apps/desktop/src/app/DesktopEarlyElectronStartup.ts @@ -26,7 +26,6 @@ type EarlyLinuxElectronOptionsInput = EarlyDesktopSettingsInput; export interface EarlyLinuxElectronOptions { readonly isDevelopment: boolean; - readonly linuxWmClass: string; readonly linuxDesktopEntryName: string; readonly passwordStore: LinuxPasswordStoreSwitch | null; } @@ -146,7 +145,6 @@ export function resolveEarlyLinuxElectronOptions( const isDevelopment = isDevelopmentEnvironment(input.env); return { isDevelopment, - linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", linuxDesktopEntryName: resolveLinuxDesktopEntryName(isDevelopment), passwordStore: resolveLinuxPasswordStoreSwitch({ preference, diff --git a/apps/desktop/src/app/DesktopEnvironment.ts b/apps/desktop/src/app/DesktopEnvironment.ts index 1066198b8f610..d5fed4b228b6f 100644 --- a/apps/desktop/src/app/DesktopEnvironment.ts +++ b/apps/desktop/src/app/DesktopEnvironment.ts @@ -75,7 +75,6 @@ export class DesktopEnvironment extends Context.Service< readonly displayName: string; readonly appUserModelId: string; readonly linuxDesktopEntryName: string; - readonly linuxWmClass: string; readonly linuxApplicationsDir: string; readonly desktopLauncherPath: Option.Option; readonly appImagePath: Option.Option; @@ -232,7 +231,6 @@ const make = Effect.fn("desktop.environment.make")(function* ( isDevelopment ? "com.t3tools.t3code.dev" : "com.t3tools.t3code", ), linuxDesktopEntryName: resolveLinuxDesktopEntryName(isDevelopment), - linuxWmClass: isDevelopment ? "t3code-dev" : "t3code", linuxApplicationsDir, desktopLauncherPath: config.desktopLauncherPath, appImagePath: config.appImagePath, diff --git a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts index 9404b0ac3087c..bfc78a834f7c4 100644 --- a/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts +++ b/apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts @@ -35,7 +35,6 @@ const makeEnvironment = (path: Path.Path, overrides: Record = { isDevelopment: false, displayName: "T3 Code (Alpha)", linuxDesktopEntryName: "com.t3tools.T3Code.desktop", - linuxWmClass: "t3code", linuxApplicationsDir: "/home/alice/.local/share/applications", desktopLauncherPath: Option.none(), appImagePath: Option.some("/home/alice/Applications/T3-Code.AppImage"), diff --git a/apps/desktop/src/app/DesktopPreReadyPlatform.ts b/apps/desktop/src/app/DesktopPreReadyPlatform.ts index 8bef7366e378a..d77544f54b69a 100644 --- a/apps/desktop/src/app/DesktopPreReadyPlatform.ts +++ b/apps/desktop/src/app/DesktopPreReadyPlatform.ts @@ -121,7 +121,6 @@ export const make = Effect.gen(function* () { } catch {} // Chromium caches portal identity; set it before any asynchronous initialization. Electron.app.setDesktopName(linux.linuxDesktopEntryName); - Electron.app.commandLine.appendSwitch("class", linux.linuxWmClass); if (linux.passwordStore !== null && linuxPasswordStoreCommandLine === null) { Electron.app.commandLine.appendSwitch("password-store", linux.passwordStore); } From 7115a0b7f200310181dee2835302e7a64017b75d Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Sun, 1 Nov 2026 02:03:16 +0000 Subject: [PATCH 004/322] perf(server): connects no longer wait on editor and SSH target discovery (#14519) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: SAPHID --- .../src/environment/RemoteOpenTargets.ts | 9 +- .../environment/staleWhileRevalidate.test.ts | 177 ++++++++++++++++++ .../src/environment/staleWhileRevalidate.ts | 81 ++++++++ .../src/process/externalLauncher.test.ts | 74 ++++++++ apps/server/src/process/externalLauncher.ts | 82 ++------ apps/server/src/ws.test.ts | 33 ++++ apps/server/src/ws.ts | 44 ++++- 7 files changed, 422 insertions(+), 78 deletions(-) create mode 100644 apps/server/src/environment/staleWhileRevalidate.test.ts create mode 100644 apps/server/src/environment/staleWhileRevalidate.ts diff --git a/apps/server/src/environment/RemoteOpenTargets.ts b/apps/server/src/environment/RemoteOpenTargets.ts index 8b4997f695d15..fff8a8168437b 100644 --- a/apps/server/src/environment/RemoteOpenTargets.ts +++ b/apps/server/src/environment/RemoteOpenTargets.ts @@ -7,7 +7,10 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import { makeStaleWhileRevalidate } from "./staleWhileRevalidate.ts"; + const SSH_PORT = 22; +const TARGETS_CACHE_TTL = "60 seconds"; export class RemoteOpenTargets extends Context.Service< RemoteOpenTargets, @@ -21,7 +24,7 @@ export const make = Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const net = yield* NetService.NetService; - const resolveTargets = Effect.gen(function* () { + const discoverTargets = Effect.gen(function* () { // Check both loopback families: sshd can be bound IPv6-only. const sshdListening = yield* Effect.zipWith( net.hasListenerOnHost(SSH_PORT, "127.0.0.1"), @@ -78,6 +81,10 @@ export const make = Effect.gen(function* () { return targets; }); + // Advertised on every client connect; probing sshd and tailscaled is slow + // on a loaded host, so serve the last result while it refreshes. + const resolveTargets = yield* makeStaleWhileRevalidate(discoverTargets, TARGETS_CACHE_TTL); + return RemoteOpenTargets.of({ resolveTargets: Effect.fn("RemoteOpenTargets.resolveTargets")(function () { return resolveTargets; diff --git a/apps/server/src/environment/staleWhileRevalidate.test.ts b/apps/server/src/environment/staleWhileRevalidate.test.ts new file mode 100644 index 0000000000000..6fa0faa9c4cbe --- /dev/null +++ b/apps/server/src/environment/staleWhileRevalidate.test.ts @@ -0,0 +1,177 @@ +import { assert, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Fiber from "effect/Fiber"; +import * as Ref from "effect/Ref"; +import * as Scope from "effect/Scope"; +import * as TestClock from "effect/testing/TestClock"; + +import { makeStaleWhileRevalidate } from "./staleWhileRevalidate.ts"; + +it.effect("computes once, then answers from memory until the ttl lapses", () => + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const read = yield* makeStaleWhileRevalidate( + Ref.updateAndGet(calls, (count) => count + 1), + "60 seconds", + ); + + assert.equal(yield* read, 1); + yield* TestClock.adjust("59 seconds"); + assert.equal(yield* read, 1); + assert.equal(yield* Ref.get(calls), 1); + }).pipe(Effect.scoped), +); + +it.effect("serves the stale value while one background refresh replaces it", () => + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const release = yield* Deferred.make(); + const started = yield* Deferred.make(); + const refreshed = yield* Deferred.make>(); + const read = yield* makeStaleWhileRevalidate( + Effect.gen(function* () { + const call = yield* Ref.updateAndGet(calls, (count) => count + 1); + if (call > 1) { + yield* Deferred.succeed(started, undefined); + yield* Deferred.await(release); + yield* Deferred.succeed(refreshed, yield* Effect.fiber); + } + return call; + }), + "60 seconds", + ); + + assert.equal(yield* read, 1); + yield* TestClock.adjust("61 seconds"); + // The refresh is blocked, yet callers are answered immediately and only + // one refresh starts however many callers arrive. + assert.equal(yield* read, 1); + assert.equal(yield* read, 1); + yield* Deferred.await(started); + assert.equal(yield* Ref.get(calls), 2); + + yield* Deferred.succeed(release, undefined); + yield* Fiber.await(yield* Deferred.await(refreshed)); + assert.equal(yield* read, 2); + }), +); + +it.effect("shares one first scan that an interrupted caller does not cancel", () => + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const release = yield* Deferred.make(); + const started = yield* Deferred.make(); + const read = yield* makeStaleWhileRevalidate( + Ref.updateAndGet(calls, (count) => count + 1).pipe( + Effect.tap(() => Deferred.succeed(started, undefined)), + Effect.tap(() => Deferred.await(release)), + ), + "60 seconds", + ); + + // A connect that times out or disconnects mid-scan leaves the scan running. + const interrupted = yield* read.pipe(Effect.forkChild); + yield* Deferred.await(started); + yield* Fiber.interrupt(interrupted); + + const next = yield* read.pipe(Effect.forkChild); + yield* Deferred.succeed(release, undefined); + assert.equal(yield* Fiber.join(next), 1); + assert.equal(yield* Ref.get(calls), 1); + }).pipe(Effect.scoped), +); + +it.effect("starts over after a failed first scan", () => + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const read = yield* makeStaleWhileRevalidate( + Ref.updateAndGet(calls, (count) => count + 1).pipe( + Effect.filterOrElse( + (call) => call !== 1, + () => Effect.die("probe crashed"), + ), + ), + "60 seconds", + ); + + assert.isTrue(Exit.isFailure(yield* Effect.exit(read))); + assert.equal(yield* read, 2); + }).pipe(Effect.scoped), +); + +it.effect("abandons a hung refresh so a later call can start another", () => + Effect.gen(function* () { + const calls = yield* Ref.make(0); + const started = yield* Deferred.make>(); + const interrupted = yield* Deferred.make(); + const refreshed = yield* Deferred.make>(); + const read = yield* makeStaleWhileRevalidate( + Ref.updateAndGet(calls, (count) => count + 1).pipe( + Effect.flatMap((call) => + call === 2 + ? Effect.fiber.pipe( + Effect.flatMap((fiber) => Deferred.succeed(started, fiber)), + Effect.andThen(Effect.never), + Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)), + ) + : Effect.succeed(call).pipe( + Effect.tap(() => + call === 3 + ? Effect.fiber.pipe( + Effect.flatMap((fiber) => Deferred.succeed(refreshed, fiber)), + ) + : Effect.void, + ), + ), + ), + ), + "60 seconds", + ); + + assert.equal(yield* read, 1); + yield* TestClock.adjust("61 seconds"); + assert.equal(yield* read, 1); + yield* Deferred.await(started); + assert.equal(yield* Ref.get(calls), 2); + + // The hung refresh times out, releasing the claim for the next caller. + yield* TestClock.adjust("30 seconds"); + yield* Deferred.await(interrupted); + yield* Fiber.await(yield* Deferred.await(started)); + assert.equal(yield* read, 1); + yield* Fiber.await(yield* Deferred.await(refreshed)); + assert.equal(yield* Ref.get(calls), 3); + assert.equal(yield* read, 3); + }).pipe(Effect.scoped), +); + +it.effect("interrupts a background refresh when the owning scope closes", () => + Effect.gen(function* () { + const interrupted = yield* Deferred.make(); + const started = yield* Deferred.make(); + const scope = yield* Scope.make(); + const calls = yield* Ref.make(0); + const read = yield* makeStaleWhileRevalidate( + Ref.updateAndGet(calls, (count) => count + 1).pipe( + Effect.filterOrElse( + (call) => call === 1, + () => + Deferred.succeed(started, undefined).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)), + ), + ), + ), + "60 seconds", + ).pipe(Scope.provide(scope)); + + yield* read; + yield* TestClock.adjust("61 seconds"); + yield* read; + yield* Deferred.await(started); + yield* Scope.close(scope, Exit.void); + yield* Deferred.await(interrupted); + }), +); diff --git a/apps/server/src/environment/staleWhileRevalidate.ts b/apps/server/src/environment/staleWhileRevalidate.ts new file mode 100644 index 0000000000000..e87757bee7031 --- /dev/null +++ b/apps/server/src/environment/staleWhileRevalidate.ts @@ -0,0 +1,81 @@ +import * as Clock from "effect/Clock"; +import * as Deferred from "effect/Deferred"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; +import * as Ref from "effect/Ref"; + +const REFRESH_TIMEOUT = Duration.seconds(30); + +type Entry = + | { readonly _tag: "Empty" } + | { readonly _tag: "Scanning"; readonly scan: Deferred.Deferred } + | { + readonly _tag: "Ready"; + readonly value: A; + readonly expiresAtNanos: bigint; + readonly refreshing: boolean; + }; + +// Shared scans survive caller cancellation; monotonic TTL bounds cache age. +// Warm reads return stale data while a scope-owned, bounded refresh runs. +export const makeStaleWhileRevalidate = Effect.fn("StaleWhileRevalidate.make")(function* ( + discover: Effect.Effect, + ttl: Duration.Input, +) { + const ttlNanos = Duration.toNanosUnsafe(Duration.fromInputUnsafe(ttl)); + const scope = yield* Effect.scope; + const state = yield* Ref.make>({ _tag: "Empty" }); + + const settle = Effect.fn("StaleWhileRevalidate.settle")(function* (exit: Exit.Exit) { + const now = yield* Clock.monotonicTimeNanos; + yield* Ref.update(state, (entry): Entry => + Exit.isSuccess(exit) + ? { + _tag: "Ready", + value: exit.value, + expiresAtNanos: now + ttlNanos, + refreshing: false, + } + : entry._tag === "Ready" + ? { ...entry, refreshing: false } + : { _tag: "Empty" }, + ); + }); + const fork = (scan: Effect.Effect) => + scan.pipe(Effect.ignoreCause({ log: true }), Effect.interruptible, Effect.forkIn(scope)); + const firstScan = (scan: Deferred.Deferred) => + fork( + discover.pipe( + Effect.onExit((exit) => settle(exit).pipe(Effect.andThen(Deferred.done(scan, exit)))), + ), + ); + const refresh = fork(discover.pipe(Effect.onExit(settle), Effect.timeoutOption(REFRESH_TIMEOUT))); + + return Effect.gen(function* () { + const now = yield* Clock.monotonicTimeNanos; + // Claiming a scan and starting it are one uninterruptible step: a caller + // interrupted between them would leave a claimed scan that never runs. + const result = yield* Ref.modify( + state, + (entry): readonly [readonly [Effect.Effect, Effect.Effect], Entry] => { + switch (entry._tag) { + case "Empty": { + const scan = Deferred.makeUnsafe(); + return [[firstScan(scan), Deferred.await(scan)], { _tag: "Scanning", scan }]; + } + case "Scanning": + return [[Effect.void, Deferred.await(entry.scan)], entry]; + case "Ready": + return entry.refreshing || entry.expiresAtNanos > now + ? [[Effect.void, Effect.succeed(entry.value)], entry] + : [[refresh, Effect.succeed(entry.value)], { ...entry, refreshing: true }]; + } + }, + ).pipe( + Effect.flatMap(([start, result]) => Effect.as(start, result)), + Effect.uninterruptible, + ); + return yield* result; + }); +}); diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index 3262c555bcf8c..1b724c17010d8 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -5,6 +5,7 @@ import * as ConfigProvider from "effect/ConfigProvider"; import * as Deferred from "effect/Deferred"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as TestClock from "effect/testing/TestClock"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; @@ -714,6 +715,79 @@ it.effect.skipIf(windowsHost)("ignores unusable app bundles and keeps PATH launc }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); +it.effect( + "memoizes editor discovery and refreshes in the background after the cache window", + () => { + let statCalls = 0; + const rescanned = Deferred.makeUnsafe(); + let initialScanDone = false; + const fileInfo = { type: "File" } as FileSystem.File.Info; + const layerLauncher = ExternalLauncher.layer.pipe( + Layer.provide( + Layer.mergeAll( + FileSystem.layerNoop({ + stat: () => + Effect.sync(() => { + statCalls += 1; + return fileInfo; + }).pipe( + Effect.tap(() => + initialScanDone ? Deferred.succeed(rescanned, undefined) : Effect.void, + ), + ), + }), + Path.layer, + Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => Effect.sync(() => makeMockDetachedHandle())), + ), + ), + ), + ); + + return Effect.gen(function* () { + const launcher = yield* ExternalLauncher.ExternalLauncher; + + const first = yield* launcher.resolveAvailableEditors(); + assert.equal(first.includes("vscode"), true); + const statCallsAfterFirstScan = statCalls; + initialScanDone = true; + assert.isAbove(statCallsAfterFirstScan, 0); + + // Past the shared command-resolution cache TTL (30s) but within the + // discovery cache window: the memoized set is reused without any scan. + yield* TestClock.adjust("31 seconds"); + const second = yield* launcher.resolveAvailableEditors(); + assert.deepEqual([...second], [...first]); + assert.equal(statCalls, statCallsAfterFirstScan); + + // Past the discovery cache window the next call still answers from the + // memoized set and rescans in the background. + yield* TestClock.adjust("30 seconds"); + const third = yield* launcher.resolveAvailableEditors(); + assert.deepEqual([...third], [...first]); + yield* Deferred.await(rescanned); + assert.isAbove(statCalls, statCallsAfterFirstScan); + }).pipe( + Effect.provide( + Layer.mergeAll( + layerLauncher, + Layer.succeed(HostProcess.Platform, "win32"), + ConfigProvider.layer( + ConfigProvider.fromEnv({ + env: { + PATH: "C:\\t3-editor-discovery-cache-test", + PATHEXT: ".COM;.EXE;.BAT;.CMD", + }, + }), + ), + TestClock.layer(), + ), + ), + ); + }, +); + it.effect("keeps scanning after the caller is interrupted and shares that scan", () => { const fileInfo = { type: "File" } as FileSystem.File.Info; const release = Deferred.makeUnsafe(); diff --git a/apps/server/src/process/externalLauncher.ts b/apps/server/src/process/externalLauncher.ts index 6a2eb145162e2..7f4417d1fb67a 100644 --- a/apps/server/src/process/externalLauncher.ts +++ b/apps/server/src/process/externalLauncher.ts @@ -19,24 +19,20 @@ import { resolveSpawnCommand, withPathDirectoryListings, } from "@t3tools/shared/shell"; -import * as Clock from "effect/Clock"; import * as Config from "effect/Config"; import * as Context from "effect/Context"; -import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Base64 from "effect/encoding/Base64"; -import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import { makeStaleWhileRevalidate } from "../environment/staleWhileRevalidate.ts"; export { ExternalLauncherError, ExternalLauncherBrowserSpawnError, @@ -447,13 +443,7 @@ const resolveFileManagerRevealKind = Effect.fn("externalLauncher.resolveFileMana }, ); -// Shared scans must survive caller timeouts/disconnects; monotonic expiry prevents clock rollback extending stale entries. -const EDITOR_DISCOVERY_CACHE_TTL_NANOS = 60_000_000_000n; - -interface EditorDiscoveryCacheEntry { - readonly scan: Deferred.Deferred>; - readonly expiresAtNanos: bigint | undefined; -} +const EDITOR_DISCOVERY_CACHE_TTL = "60 seconds"; export class ExternalLauncher extends Context.Service< ExternalLauncher, @@ -817,69 +807,27 @@ export const make = Effect.gen(function* () { Effect.provideService(Path.Path, path), ); - const scope = yield* Scope.Scope; - const editorDiscoveryCache = yield* Ref.make>( - Option.none(), - ); - const runEditorDiscovery = (scan: Deferred.Deferred>) => + const cachedAvailableEditors = yield* makeStaleWhileRevalidate( provideCommandResolutionServices(resolveAvailableEditors()).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), - Effect.onExit((exit) => - Effect.gen(function* () { - const expiresAtNanos = - (yield* Clock.monotonicTimeNanos) + EDITOR_DISCOVERY_CACHE_TTL_NANOS; - yield* Ref.update(editorDiscoveryCache, (current) => - Option.isNone(current) || current.value.scan !== scan - ? current - : Exit.isSuccess(exit) - ? Option.some({ scan, expiresAtNanos }) - : Option.none(), - ); - yield* Deferred.done(scan, exit); - }), - ), - Effect.interruptible, - Effect.forkIn(scope), - ); - // Claim and start atomically; interruption between them would leave an entry waiting on no scan. - const acquireEditorDiscovery = Effect.gen(function* () { - const nowNanos = yield* Clock.monotonicTimeNanos; - const [scan, isNewScan] = yield* Ref.modify( - editorDiscoveryCache, - ( - current, - ): [ - [EditorDiscoveryCacheEntry["scan"], boolean], - Option.Option, - ] => { - if ( - Option.isSome(current) && - (current.value.expiresAtNanos === undefined || current.value.expiresAtNanos > nowNanos) - ) { - return [[current.value.scan, false], current]; - } - const scan = Deferred.makeUnsafe>(); - return [[scan, true], Option.some({ scan, expiresAtNanos: undefined })]; - }, - ); - if (isNewScan) { - yield* runEditorDiscovery(scan); - } - return scan; - }).pipe(Effect.uninterruptible); - const cachedAvailableEditors = Effect.flatMap(acquireEditorDiscovery, Deferred.await); + ), + EDITOR_DISCOVERY_CACHE_TTL, + ); + const cachedFileManagerRevealKind = yield* makeStaleWhileRevalidate( + provideCommandResolutionServices(resolveFileManagerRevealKind()).pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + ), + EDITOR_DISCOVERY_CACHE_TTL, + ); // Forwarders retain the underlying launcher and discovery spans. return ExternalLauncher.of({ resolveAvailableEditors: Effect.fnUntraced(function* () { return yield* cachedAvailableEditors; }), - resolveFileManagerRevealKind: Effect.fnUntraced( - function* () { - return yield* provideCommandResolutionServices(resolveFileManagerRevealKind()); - }, - Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), - ), + resolveFileManagerRevealKind: Effect.fnUntraced(function* () { + return yield* cachedFileManagerRevealKind; + }), launchBrowser: Effect.fnUntraced( function* (target) { return yield* launchBrowser(target); diff --git a/apps/server/src/ws.test.ts b/apps/server/src/ws.test.ts index 641845f322ea6..ba647514ecef4 100644 --- a/apps/server/src/ws.test.ts +++ b/apps/server/src/ws.test.ts @@ -123,6 +123,7 @@ import * as ProjectEnrichment from "./project/ProjectEnrichmentService.ts"; import * as ProjectService from "./project/ProjectService.ts"; import { layerWsRpc, + resolveOpenDiscoveryForConfig, subscribeOrchestrationV2Shell, subscribeOrchestrationV2Thread, WS_RPC_SERVER_OPTIONS, @@ -188,6 +189,38 @@ it.effect("resends a file manager reveal kind that missed the snapshot", () => }), ); +it.effect("runs editor, remote open target and direct endpoint discovery side by side", () => + Effect.gen(function* () { + // Each discovery finishes only once the other two have started, so a + // config that ran them one after another would never resolve. + const editorsStarted = yield* Deferred.make(); + const targetsStarted = yield* Deferred.make(); + const endpointsStarted = yield* Deferred.make(); + const startThenAwait = (started: Deferred.Deferred, value: A) => + Deferred.succeed(started, undefined).pipe( + Effect.andThen( + Effect.forEach([editorsStarted, targetsStarted, endpointsStarted], Deferred.await), + ), + Effect.as(value), + ); + + const discovery = yield* resolveOpenDiscoveryForConfig({ + editors: startThenAwait(editorsStarted, ["file-manager" as const]), + fileManagerRevealKind: Effect.succeed("finder" as const), + remoteOpenTargets: startThenAwait(targetsStarted, []), + directEndpoints: startThenAwait(endpointsStarted, []), + }); + + assert.deepEqual(discovery, { + availableEditors: ["file-manager"], + shellRevealInFileManager: true, + shellRevealInFileManagerKind: "finder", + remoteOpenTargets: [], + directEndpoints: [], + }); + }), +); + const ReplayTestLayer = OrchestrationEventStore.layer.pipe( Layer.provideMerge(SqlitePersistence.layerMemory), ); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 4781fcf78dde6..8373bbfae0c55 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -324,7 +324,8 @@ export const withLateEditorConfig = ( editors.join() !== config.availableEditors.join() || (editors.includes("file-manager") && config.shellRevealInFileManagerKind === undefined), ), - // Leave the unshared reveal probe unbounded so timeouts cannot cancel every attempt on slow hosts. + // Unbounded, unlike the snapshot: this exists to deliver a probe that + // outlasts the snapshot's timeout. Stream.mapEffect((editors) => resolveEditorConfig(editors, launcher.resolveFileManagerRevealKind()), ), @@ -366,6 +367,31 @@ export const withLateEditorConfig = ( ); }; +// Discoveries run side by side so the slowest one, not their sum, bounds the +// server config snapshot. Each degrades on timeout rather than failing it. +export const resolveOpenDiscoveryForConfig = (discovery: { + readonly editors: Effect.Effect, E1, R1>; + readonly fileManagerRevealKind: Effect.Effect; + readonly remoteOpenTargets: Effect.Effect, E3, R3>; + readonly directEndpoints: Effect.Effect, E4, R4>; +}) => + Effect.all( + { + editorConfig: resolveAvailableEditorsForConfig(discovery.editors).pipe( + Effect.flatMap((availableEditors) => + resolveEditorConfig( + availableEditors, + resolveFileManagerRevealKindForConfig(discovery.fileManagerRevealKind), + ), + ), + ), + // Same discovery-with-timeout treatment as editors: a slow probe must + // not stall server.getConfig, so it degrades to an empty list. + remoteOpenTargets: resolveAvailableEditorsForConfig(discovery.remoteOpenTargets), + directEndpoints: resolveAvailableEditorsForConfig(discovery.directEndpoints), + }, + { concurrency: "unbounded" }, + ).pipe(Effect.map(({ editorConfig, ...endpoints }) => ({ ...editorConfig, ...endpoints }))); function withoutWorkspaceSnapshots(providers: ReadonlyArray): ServerProvider[] { return providers.map(({ workspaceSnapshots: _workspaceSnapshots, ...provider }) => provider); } @@ -1744,10 +1770,12 @@ export const layerWsRpc = ( const auth = yield* serverAuth.getDescriptor(); const scratchWorkspaceRoot = yield* managedFolders.scratchRoot; const lastEventLoopStall = yield* eventLoopMonitor.latestNoticeableStall; - const editorConfig = yield* resolveEditorConfig( - yield* resolveAvailableEditorsForConfig(externalLauncher.resolveAvailableEditors()), - resolveFileManagerRevealKindForConfig(externalLauncher.resolveFileManagerRevealKind()), - ); + const discovery = yield* resolveOpenDiscoveryForConfig({ + editors: externalLauncher.resolveAvailableEditors(), + fileManagerRevealKind: externalLauncher.resolveFileManagerRevealKind(), + remoteOpenTargets: remoteOpenTargets.resolveTargets(), + directEndpoints: directEndpoints.resolve(), + }); return { environment, @@ -1757,11 +1785,7 @@ export const layerWsRpc = ( keybindings: keybindingsConfig.keybindings, issues: keybindingsConfig.issues, providers, - ...editorConfig, - remoteOpenTargets: yield* resolveAvailableEditorsForConfig( - remoteOpenTargets.resolveTargets(), - ), - directEndpoints: yield* resolveAvailableEditorsForConfig(directEndpoints.resolve()), + ...discovery, observability: { logsDirectoryPath: config.logsDir, localTracingEnabled: true, From 28724828b3cd5267815cf7dd6c6e14b67b693b17 Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Sun, 1 Nov 2026 02:03:41 +0000 Subject: [PATCH 005/322] fix(vcs): look up pull request clone URLs on the pull request's host (#15000) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: SAPHID Co-authored-by: Claude Opus 5.5 Co-authored-by: maria-rcks --- apps/server/src/git/GitManager.test.ts | 164 +++++++++++++++++- apps/server/src/git/GitManager.ts | 7 + .../src/server/SourceControlProvider.ts | 2 + .../ForgejoSourceControlProvider.test.ts | 67 +++++++ .../GitHubSourceControlProvider.test.ts | 32 ++++ .../src/server/GitHubSourceControlProvider.ts | 11 +- 6 files changed, 278 insertions(+), 5 deletions(-) diff --git a/apps/server/src/git/GitManager.test.ts b/apps/server/src/git/GitManager.test.ts index 3c13991426f92..a71f19e6a973b 100644 --- a/apps/server/src/git/GitManager.test.ts +++ b/apps/server/src/git/GitManager.test.ts @@ -490,7 +490,12 @@ function createFakeSourceControlProvider(scenario: FakeProviderScenario = {}): { repository: input.repository, }).pipe( Effect.flatMap(() => { - const cloneUrls = scenario.repositoryCloneUrls?.[input.repository]; + const repository = input.host ? `${input.host}/${input.repository}` : input.repository; + const cloneUrls = + scenario.repositoryCloneUrls?.[repository] ?? + (input.host === "github.com" + ? scenario.repositoryCloneUrls?.[input.repository] + : undefined); return cloneUrls ? Effect.succeed({ nameWithOwner: input.repository, ...cloneUrls }) : Effect.fail( @@ -3906,6 +3911,163 @@ it.layer(layerGitManagerTest)("GitManager", (it) => { }), ); + it.effect.each([ + { mode: "local", repositoryType: "same repo" }, + { mode: "local", repositoryType: "fork" }, + { mode: "worktree", repositoryType: "same repo" }, + { mode: "worktree", repositoryType: "fork" }, + ] as const)( + "checks out same-named repositories on the pull request host ($mode, $repositoryType)", + ({ mode, repositoryType }) => + Effect.gen(function* () { + const isCrossRepository = repositoryType === "fork"; + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + // The checkout is on github.com, which also has a same-named repository, + // and the pull request is on another host. Clone lookups that fall back + // to the checkout's host find the github.com repository. + const enterpriseRemote = yield* createBareRemote(); + const defaultHostRemote = yield* createBareRemote(); + const host = "github.enterprise.test"; + const repository = isCrossRepository ? "fork/repo" : "owner/base"; + const cloneUrl = `https://${host}/${repository}.git`; + yield* runGit(repoDir, ["remote", "add", "origin", defaultHostRemote]); + yield* runGit(repoDir, ["push", "origin", "main", "main:feature/host"]); + yield* runGit(repoDir, ["push", enterpriseRemote, "main"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/host"]); + NodeFS.writeFileSync(NodePath.join(repoDir, "host.txt"), "enterprise head\n"); + yield* runGit(repoDir, ["add", "host.txt"]); + yield* runGit(repoDir, ["commit", "-m", "Enterprise head"]); + const expectedHead = (yield* runGit(repoDir, ["rev-parse", "HEAD"])).stdout.trim(); + yield* runGit(repoDir, ["push", enterpriseRemote, "feature/host"]); + yield* runGit(repoDir, ["checkout", "main"]); + if (mode === "worktree") { + yield* runGit(repoDir, ["branch", "-D", "feature/host"]); + } + yield* configureVisibleRemoteUrlWithLocalRewrite( + repoDir, + "origin", + "https://github.com/owner/base.git", + defaultHostRemote, + ); + yield* runGit(repoDir, ["config", `url.${enterpriseRemote}.insteadOf`, cloneUrl]); + const defaultCloneUrl = `https://github.com/${repository}.git`; + yield* runGit(repoDir, [ + "config", + "--add", + `url.${defaultHostRemote}.insteadOf`, + defaultCloneUrl, + ]); + + const { manager } = yield* makeManager({ + providerScenario: { + pullRequest: { + number: 42, + title: "Enterprise head", + url: `https://${host}/owner/base/pull/42`, + baseRefName: "main", + headRefName: "feature/host", + state: "open", + isCrossRepository, + headRepositoryNameWithOwner: repository, + headRepositoryOwnerLogin: isCrossRepository ? "fork" : "owner", + }, + repositoryCloneUrls: { + [repository]: { url: defaultCloneUrl, sshUrl: defaultCloneUrl }, + [`${host}/${repository}`]: { url: cloneUrl, sshUrl: cloneUrl }, + }, + }, + }); + const result = yield* preparePullRequestThread(manager, { + cwd: repoDir, + reference: "42", + mode, + }); + const checkoutDir = result.worktreePath ?? repoDir; + const upstreamRemote = (yield* runGit(checkoutDir, [ + "config", + `branch.${result.branch}.remote`, + ])).stdout.trim(); + const upstreamUrl = (yield* runGit(checkoutDir, [ + "config", + `remote.${upstreamRemote}.url`, + ])).stdout.trim(); + const head = (yield* runGit(checkoutDir, ["rev-parse", "HEAD"])).stdout.trim(); + expect({ upstreamUrl, head }).toEqual({ upstreamUrl: cloneUrl, head: expectedHead }); + expect(NodeFS.readFileSync(NodePath.join(checkoutDir, "host.txt"), "utf8")).toBe( + "enterprise head\n", + ); + }), + ); + + it.effect.each([ + { mode: "local", url: "malformed pull request URL" }, + { mode: "worktree", url: "malformed pull request URL" }, + { mode: "local", url: "file:///owner/base/pull/42" }, + { mode: "worktree", url: "file:///owner/base/pull/42" }, + ] as const)( + "checks out pull requests with hostless URLs using the default host ($mode, $url)", + ({ mode, url }) => + Effect.gen(function* () { + const repoDir = yield* makeTempDir("t3code-git-manager-"); + yield* initRepo(repoDir); + const originDir = yield* createBareRemote(); + const forkDir = yield* createBareRemote(); + const cloneUrl = "https://github.com/fork/repo.git"; + yield* runGit(repoDir, ["remote", "add", "origin", originDir]); + yield* runGit(repoDir, ["push", "origin", "main"]); + yield* runGit(repoDir, ["checkout", "-b", "feature/malformed-url"]); + NodeFS.writeFileSync(NodePath.join(repoDir, "fork.txt"), "fork head\n"); + yield* runGit(repoDir, ["add", "fork.txt"]); + yield* runGit(repoDir, ["commit", "-m", "Fork head"]); + const expectedHead = (yield* runGit(repoDir, ["rev-parse", "HEAD"])).stdout.trim(); + yield* runGit(repoDir, ["push", forkDir, "feature/malformed-url"]); + yield* runGit(repoDir, ["checkout", "main"]); + if (mode === "worktree") { + yield* runGit(repoDir, ["branch", "-D", "feature/malformed-url"]); + } + yield* runGit(repoDir, ["config", `url.${forkDir}.insteadOf`, cloneUrl]); + + const { manager } = yield* makeManager({ + providerScenario: { + pullRequest: { + number: 42, + title: "Fork head", + url, + baseRefName: "main", + headRefName: "feature/malformed-url", + state: "open", + isCrossRepository: true, + headRepositoryNameWithOwner: "fork/repo", + headRepositoryOwnerLogin: "fork", + }, + repositoryCloneUrls: { + "fork/repo": { url: cloneUrl, sshUrl: cloneUrl }, + }, + }, + }); + const result = yield* preparePullRequestThread(manager, { + cwd: repoDir, + reference: "42", + mode, + }); + const checkoutDir = result.worktreePath ?? repoDir; + const upstreamRemote = (yield* runGit(checkoutDir, [ + "config", + `branch.${result.branch}.remote`, + ])).stdout.trim(); + const upstreamUrl = (yield* runGit(checkoutDir, [ + "config", + `remote.${upstreamRemote}.url`, + ])).stdout.trim(); + const head = (yield* runGit(checkoutDir, ["rev-parse", "HEAD"])).stdout.trim(); + expect({ upstreamUrl, head }).toEqual({ upstreamUrl: cloneUrl, head: expectedHead }); + expect(NodeFS.readFileSync(NodePath.join(checkoutDir, "fork.txt"), "utf8")).toBe( + "fork head\n", + ); + }), + ); + const setUpLocalCheckoutPullRequest = Effect.fnUntraced(function* ( reportedHead: (pullHead: string) => string | undefined = (pullHead) => pullHead, syncCheckoutWithPullHead = true, diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 71e5edb3b697b..95f5187b1a303 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -893,6 +893,11 @@ export const make = Effect.gen(function* () { }), ); + const pullRequestHostOptions = (url: string) => { + const host = URL.parse(url)?.host; + return host ? { host } : {}; + }; + const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")( function* ( cwd: string, @@ -923,6 +928,7 @@ export const make = Effect.gen(function* () { const cloneUrls = yield* (yield* sourceControlProvider(cwd)).getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, + ...pullRequestHostOptions(pullRequest.url), }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; @@ -1036,6 +1042,7 @@ export const make = Effect.gen(function* () { const cloneUrls = yield* (yield* sourceControlProvider(cwd)).getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, + ...pullRequestHostOptions(pullRequest.url), }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; diff --git a/packages/source-control-core/src/server/SourceControlProvider.ts b/packages/source-control-core/src/server/SourceControlProvider.ts index 76eb90cfd8c0e..c128b683ad731 100644 --- a/packages/source-control-core/src/server/SourceControlProvider.ts +++ b/packages/source-control-core/src/server/SourceControlProvider.ts @@ -195,6 +195,8 @@ export class SourceControlProvider extends Context.Service< readonly cwd: string; readonly context?: SourceControlProviderContext; readonly repository: string; + /** The host the repository is on. Without it, the checkout's host. */ + readonly host?: string; }) => Effect.Effect; readonly createRepository: (input: { readonly cwd: string; diff --git a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts index 142bc4c0fc669..586ce19f28d7e 100644 --- a/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts +++ b/packages/source-control-forgejo/src/server/ForgejoSourceControlProvider.test.ts @@ -33,6 +33,73 @@ const requestJson = (request: HttpClientRequest.HttpClientRequest): unknown => { }; const encodeJsonEffect = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +it.effect("reads clone URLs on the given host rather than the checkout's", () => { + const urls: string[] = []; + const client = HttpClient.make((request) => { + urls.push(request.url); + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + encodeJson({ + full_name: "maria/project", + clone_url: "https://forgejo.test/maria/project.git", + ssh_url: "git@forgejo.test:maria/project.git", + }), + ), + ), + ); + }); + return Effect.gen(function* () { + const cli = yield* ForgejoCli.make; + const provider = yield* ForgejoSourceControlProvider.make.pipe( + Effect.provideService(ForgejoCli.ForgejoCli, cli), + ); + const cloneUrls = yield* provider.getRepositoryCloneUrls({ + cwd: "/repo", + repository: "maria/project", + host: "forgejo.test", + }); + assert.strictEqual(cloneUrls.url, "https://forgejo.test/maria/project.git"); + assert.deepStrictEqual(urls, ["https://forgejo.test/api/v1/repos/maria/project"]); + }).pipe( + Effect.provideService(HttpClient.HttpClient, client), + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(true), + readFileString: () => + Effect.succeed( + encodeJson({ + hosts: { + "forgejo.test": { type: "Application", token: "test-token" }, + "code.example": { type: "Application", token: "other-token" }, + }, + }), + ), + }), + ), + Effect.provide( + TestSourceControlHost.layer({ + process: { + run: (input) => { + if (input.command === "git") { + assert.deepStrictEqual(input.args, ["remote", "-v"]); + // The checkout is on another Forgejo server with a same-named repository. + return Effect.succeed( + processOutput("origin\thttps://code.example/maria/project.git (fetch)"), + ); + } + assert.strictEqual(input.command, "fj"); + assert.deepStrictEqual(input.args, ["--host", "https://forgejo.test", "whoami"]); + return Effect.succeed(processOutput("")); + }, + }, + }), + ), + ); +}); + const requiredChecksPullRequest = { number: 7, title: "Required checks", diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts index e14b495a4ef3e..a5a08fcfc2cc4 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.test.ts @@ -289,6 +289,38 @@ describe("GitHubSourceControlProvider repository resolution", () => { }).pipe(Effect.provide(layer)); }); + it.effect("reads clone URLs on the given host before the checkout's", () => { + const paths: string[] = []; + const { layer } = harness({ + remotes: remotesOutput(["origin", "https://github.com/acme/web.git"]), + api: { + rest: (input) => + Effect.sync(() => { + paths.push(`${input.host} ${input.path}`); + return restResponse({ + full_name: "acme/web", + html_url: `https://${input.host}/acme/web`, + ssh_url: `git@${input.host}:acme/web.git`, + }); + }), + }, + }); + return Effect.gen(function* () { + const gh = yield* GitHubSourceControlProvider.make; + const cloneUrls = yield* gh.getRepositoryCloneUrls({ + cwd: "/repo", + repository: "acme/web", + host: "GitHub.Enterprise.test", + }); + yield* gh.getRepositoryCloneUrls({ cwd: "/repo", repository: "acme/web" }); + assert.strictEqual(cloneUrls.url, "https://github.enterprise.test/acme/web"); + assert.deepStrictEqual(paths, [ + "github.enterprise.test repos/acme/web", + "github.com repos/acme/web", + ]); + }).pipe(Effect.provide(layer)); + }); + it.effect.each([ ["github", "git@github:acme/web.git"], ["github.com-scale", "git@github.com-scale:acme/web.git"], diff --git a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts index 4a42e9422a5de..bc0438cf41b60 100644 --- a/packages/source-control-github/src/server/GitHubSourceControlProvider.ts +++ b/packages/source-control-github/src/server/GitHubSourceControlProvider.ts @@ -996,10 +996,13 @@ export const make = Effect.gen(function* () { ), getRepositoryCloneUrls: Effect.fn("GitHubSourceControlProvider.getRepositoryCloneUrls")( function* (input) { - const fallbackHost = (yield* resolveRepository(input).pipe( - Effect.map((locator) => locator.host), - Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"), - )).toLowerCase(); + const fallbackHost = ( + input.host ?? + (yield* resolveRepository(input).pipe( + Effect.map((locator) => locator.host), + Effect.orElseSucceed(() => environment.GH_HOST ?? "github.com"), + )) + ).toLowerCase(); const bareName = input.repository.trim().replace(/\.git$/i, ""); const locator = parseGitHubRepositorySelector(input.repository, fallbackHost) ?? From 90f064685e517c95bf85a35b8573ce49514c78c4 Mon Sep 17 00:00:00 2001 From: oliver <97427849+flamboh@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:03:55 -0700 Subject: [PATCH 006/322] fix(mobile): images stop re-downloading when their URL is re-signed (#21527) Co-authored-by: Claude Opus 5.5 --- .../pull-requests/PullRequestMarkdown.tsx | 1 + .../pull-requests/PullRequestMedia.tsx | 5 +- .../features/threads/ThreadMarkdownImage.tsx | 11 +++- apps/mobile/src/lib/assetImageSource.test.ts | 62 ++++++++++++++++--- apps/mobile/src/lib/assetImageSource.ts | 53 +++++++++++++--- .../src/shared/definition.ts | 2 + .../src/shared/definition.test.ts | 19 ++++++ .../src/shared/gitHubMedia.ts | 26 +++++--- 8 files changed, 154 insertions(+), 25 deletions(-) diff --git a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx index b840ce47d9e07..e09963c2bf32a 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx @@ -77,6 +77,7 @@ function MarkdownImage(props: { diff --git a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx index 0665f045b8465..54d3a82227731 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx @@ -12,6 +12,7 @@ import { Pressable, View } from "react-native"; import { Image } from "expo-image"; import { AppText } from "../../components/ui/AppText"; import { useAssetUrlState, useRefreshAssetUrl } from "../../state/assets"; +import { assetImageCacheKey } from "../../lib/assetImageSource"; import { MediaVideoPlayer } from "../../components/MediaVideoPlayer"; import { VideoAttachmentTile } from "../../components/VideoAttachmentTile"; import { VideoPreviewModal, type VideoPreviewSource } from "../../components/VideoPreviewModal"; @@ -40,6 +41,7 @@ export function usePullRequestMediaUrl(url: string) { const refresh = useRefreshAssetUrl(context?.environmentId ?? null, resource); return { uri: asset._tag === "Success" ? asset.url : resource ? null : url, + cacheKey: context && resource ? assetImageCacheKey(context.environmentId, resource) : undefined, unavailable: resource !== null && asset._tag === "Failure", source: pullRequestMediaSource(url, context), resolvePlaybackUri: resource ? async () => (await refresh()) ?? null : undefined, @@ -65,6 +67,7 @@ export function PullRequestMedia(props: { @@ -134,7 +137,7 @@ export function PullRequestImageAttachment(props: { > setDecoded({ sourceKey: props.sourceKey, size })} onError={() => setFailure({ uri: props.uri, failed: true })} /> @@ -209,6 +212,7 @@ export function ThreadMarkdownImageView(props: { function ThreadMarkdownImageRequest(props: { readonly uri: string; + readonly cacheKey: string | undefined; readonly onLoad: (sourceSize: ImageSize) => void; readonly onError: () => void; }) { @@ -217,7 +221,7 @@ function ThreadMarkdownImageRequest(props: { return ( <> { expect(left.cacheKey).not.toBe(right.cacheKey); }); - it("keeps workspace files keyed by URL because they can change on disk", () => { - const source = assetImageSource( + it("keys host files by the revision the server read, and by URL without one", () => { + const resource = { + _tag: "media-file", + threadId: ThreadId.make("thread-1"), + path: "docs/shot.png", + } as const; + const first = assetImageSource( environmentId, - { _tag: "draft-workspace-file", cwd: "/repo", path: "logo.png" }, - "https://host/api/assets/token-a/logo.png", + resource, + "https://host/api/assets/token-a/shot.png", + "1:2:5:1000:1000", ); - expect(source).toStrictEqual({ uri: "https://host/api/assets/token-a/logo.png" }); + expect(first.cacheKey).toBeDefined(); + expect( + assetImageSource( + environmentId, + resource, + "https://host/api/assets/token-b/shot.png", + "1:2:5:1000:1000", + ), + ).toStrictEqual({ uri: "https://host/api/assets/token-b/shot.png", cacheKey: first.cacheKey }); + expect(assetImageCacheKey(environmentId, resource, "1:2:5:2000:2000")).not.toBe(first.cacheKey); + expect( + assetImageCacheKey(environmentId, { ...resource, path: "docs/other.png" }, "1:2:5:1000:1000"), + ).not.toBe(first.cacheKey); + expect( + assetImageCacheKey(EnvironmentId.make("environment-2"), resource, "1:2:5:1000:1000"), + ).not.toBe(first.cacheKey); + expect( + assetImageSource( + environmentId, + { _tag: "draft-workspace-file", cwd: "/repo", path: "logo.png" }, + "https://host/api/assets/token-a/logo.png", + ), + ).toStrictEqual({ uri: "https://host/api/assets/token-a/logo.png" }); + }); + + it("keys source control media by URL only when its bytes cannot change", () => { + const sha = "0123456789abcdef0123456789abcdef01234567"; + const key = (url: string, environment = environmentId) => + assetImageCacheKey(environment, { _tag: SOURCE_CONTROL_MEDIA_ASSET, cwd: "/repo", url }); + const pinned = key(`https://raw.githubusercontent.com/owner/repo/${sha}/docs/shot.png`); + + expect(pinned).toBeDefined(); + expect(key(`https://github.com/owner/repo/blob/${sha}/docs/shot.png`)).toBe(pinned); + expect(key(`https://raw.githubusercontent.com/owner/repo/${sha}/docs/other.png`)).not.toBe( + pinned, + ); + expect( + key( + `https://raw.githubusercontent.com/owner/repo/${sha}/docs/shot.png`, + EnvironmentId.make("environment-2"), + ), + ).not.toBe(pinned); + expect(key("https://raw.githubusercontent.com/owner/repo/main/docs/shot.png")).toBeUndefined(); }); }); diff --git a/apps/mobile/src/lib/assetImageSource.ts b/apps/mobile/src/lib/assetImageSource.ts index a8fb52362319e..6ddd484a66a9f 100644 --- a/apps/mobile/src/lib/assetImageSource.ts +++ b/apps/mobile/src/lib/assetImageSource.ts @@ -1,16 +1,53 @@ -import type { AssetResource, EnvironmentId } from "@t3tools/contracts"; +import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; +import { + SOURCE_CONTROL_MEDIA_ASSET, + type AssetResource, + type EnvironmentId, +} from "@t3tools/contracts"; import type { ImageSource } from "expo-image"; -/** Cache immutable attachments by id; signed URLs change on each mint and reconnect. */ +/** + * Signed URLs change on each mint and reconnect, so a URL-keyed cache misses every time. + * Content that cannot change caches under its identity, and host files under the revision the + * server read. Anything else stays keyed by URL. + */ +export function assetImageCacheKey( + environmentId: EnvironmentId, + resource: AssetResource, + fileRevision?: string, +): string | undefined { + switch (resource._tag) { + case "attachment": + return `t3-attachment:${JSON.stringify([environmentId, resource.attachmentId])}`; + case SOURCE_CONTROL_MEDIA_ASSET: { + const media = sourceControlClients.mediaUrl(resource.url); + return media?.immutable + ? `t3-media:${JSON.stringify([environmentId, media.fetchUrl])}` + : undefined; + } + case "media-file": + case "workspace-file": + case "draft-workspace-file": + return fileRevision === undefined + ? undefined + : `t3-file:${JSON.stringify([ + environmentId, + resource._tag === "draft-workspace-file" ? resource.cwd : resource.threadId, + resource.path, + fileRevision, + ])}`; + // A tool can replace the image at its index, and nothing cheap identifies its bytes. + default: + return undefined; + } +} + export function assetImageSource( environmentId: EnvironmentId, resource: AssetResource, uri: string, + fileRevision?: string, ): ImageSource { - return resource._tag === "attachment" - ? { - uri, - cacheKey: `t3-attachment:${JSON.stringify([environmentId, resource.attachmentId])}`, - } - : { uri }; + const cacheKey = assetImageCacheKey(environmentId, resource, fileRevision); + return cacheKey === undefined ? { uri } : { uri, cacheKey }; } diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index c766a92a5a5f5..30e88377590f8 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -101,6 +101,8 @@ export interface SourceControlMediaUrl { readonly fileName: string; /** Set when the URL alone says what the media is, as an extensionless upload can. */ readonly kind?: "image" | "video"; + /** Set when the bytes behind `fetchUrl` can never change, so a client may cache them by it. */ + readonly immutable?: true; } export interface ReferenceAutolink { diff --git a/packages/source-control-github/src/shared/definition.test.ts b/packages/source-control-github/src/shared/definition.test.ts index 3aadd4697b6fe..924ab31716853 100644 --- a/packages/source-control-github/src/shared/definition.test.ts +++ b/packages/source-control-github/src/shared/definition.test.ts @@ -132,6 +132,7 @@ describe("GitHub media URLs", () => { fetchUrl: "https://github.com/user-attachments/assets/1a1842fb-6383-492f-873c-57aa0033fa6c", fileName: "1a1842fb-6383-492f-873c-57aa0033fa6c", kind: "video", + immutable: true, }); expect(media("https://github.com/owner/repo/blob/main/docs/shot.png")).toEqual({ fetchUrl: "https://raw.githubusercontent.com/owner/repo/main/docs/shot.png", @@ -147,6 +148,24 @@ describe("GitHub media URLs", () => { expect(media("https://raw.githubusercontent.com/o/r/main/100%.png")?.fileName).toBe("100%.png"); }); + it("marks only uploads and files at a full commit SHA as immutable", () => { + const sha = "0123456789abcdef0123456789abcdef01234567"; + for (const url of [ + "https://github.com/user-attachments/assets/1a1842fb-6383-492f-873c-57aa0033fa6c", + "https://github.com/owner/repo/assets/45952064/1a1842fb", + `https://github.com/owner/repo/blob/${sha}/docs/shot.png`, + `https://raw.githubusercontent.com/owner/repo/${sha}/docs/shot.png`, + ]) { + expect(media(url)?.immutable, url).toBe(true); + } + for (const url of [ + "https://raw.githubusercontent.com/owner/repo/0123456789ab/docs/shot.png", + `https://media.githubusercontent.com/media/owner/repo/${sha}/a.mp4`, + ]) { + expect(media(url)?.immutable, url).toBeUndefined(); + } + }); + it("claims nothing a request without a credential loads just as well", () => { for (const url of [ "https://example.com/shot.png", diff --git a/packages/source-control-github/src/shared/gitHubMedia.ts b/packages/source-control-github/src/shared/gitHubMedia.ts index 0bcc126fd7c93..eec80c071fd1b 100644 --- a/packages/source-control-github/src/shared/gitHubMedia.ts +++ b/packages/source-control-github/src/shared/gitHubMedia.ts @@ -18,6 +18,8 @@ const ATTACHMENT_PATH_PATTERN = /^\/user-attachments\/assets\/[\w-]+$/u; const LEGACY_ATTACHMENT_PATH_PATTERN = /^\/[^/]+\/[^/]+\/assets\/\d+\/[\w-]+$/u; /** `blob` and `raw` both address file bytes; `raw` is the one the token is honoured on. */ const REPOSITORY_FILE_PATTERN = /^\/([^/]+)\/([^/]+)\/(?:raw|blob)\/(.*[^/])$/u; +/** GitHub rejects 40-hex branch and tag names, and serves no SHA-256 repositories. */ +const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/u; function fileName(fetchUrl: string): string { const segment = new URL(fetchUrl).pathname.split("/").pop() ?? ""; @@ -31,26 +33,34 @@ function fileName(fetchUrl: string): string { return name.length > 0 ? name : "github-media"; } -function media(fetchUrl: string, kind?: "video"): SourceControlMediaUrl { - return { fetchUrl, fileName: fileName(fetchUrl), ...(kind ? { kind } : {}) }; +function media( + fetchUrl: string, + extra: Pick = {}, +): SourceControlMediaUrl { + return { fetchUrl, fileName: fileName(fetchUrl), ...extra }; +} + +/** A branch or tag can move; a raw file is fixed only when read at a full commit SHA. */ +function rawFile(fetchUrl: string): SourceControlMediaUrl { + const ref = new URL(fetchUrl).pathname.split("/")[3] ?? ""; + return media(fetchUrl, COMMIT_SHA_PATTERN.test(ref) ? { immutable: true } : {}); } export function gitHubMediaUrl(url: URL): SourceControlMediaUrl | null { if (url.protocol !== "https:") return null; const host = url.hostname.toLowerCase(); - if (host === RAW_HOST || host === LFS_HOST) { - return media(`https://${host}${url.pathname}${url.search}`); - } + if (host === RAW_HOST) return rawFile(`https://${host}${url.pathname}${url.search}`); + if (host === LFS_HOST) return media(`https://${host}${url.pathname}${url.search}`); if (!WEB_HOSTS.has(host)) return null; // GitHub links an uploaded video bare, with no extension, and an image as markdown. if (ATTACHMENT_PATH_PATTERN.test(url.pathname)) { - return media(`https://github.com${url.pathname}`, "video"); + return media(`https://github.com${url.pathname}`, { kind: "video", immutable: true }); } if (LEGACY_ATTACHMENT_PATH_PATTERN.test(url.pathname)) { - return media(`https://github.com${url.pathname}`); + return media(`https://github.com${url.pathname}`, { immutable: true }); } const repositoryFile = REPOSITORY_FILE_PATTERN.exec(url.pathname); return repositoryFile - ? media(`https://${RAW_HOST}/${repositoryFile[1]}/${repositoryFile[2]}/${repositoryFile[3]}`) + ? rawFile(`https://${RAW_HOST}/${repositoryFile[1]}/${repositoryFile[2]}/${repositoryFile[3]}`) : null; } From 0b75a852d8fe529774daf6ccc37916336aa3e0b2 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 1 Nov 2026 03:04:09 +0100 Subject: [PATCH 007/322] fix(desktop): keep noisy shell probes from blocking PATH capture (#21913) Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com> Co-authored-by: maria-rcks --- .../src/shell/DesktopShellEnvironment.test.ts | 38 +++++++++++++++++++ .../src/shell/DesktopShellEnvironment.ts | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 4804823069024..33325d9de9907 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -1,6 +1,7 @@ // @effect-diagnostics nodeBuiltinImport:off -- The process fixture runs real shell commands with isolated config files. import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; @@ -72,6 +73,7 @@ function runShellEnvironment(input: { command: ChildProcess.Command, ) => string | { output: string; exitCode: number }; readonly failure?: PlatformError.PlatformError; + readonly processSpawner?: ChildProcessSpawner.ChildProcessSpawner["Service"]; }) { const layerEnvironment = Layer.succeed( DesktopEnvironment.DesktopEnvironment, @@ -82,6 +84,7 @@ function runShellEnvironment(input: { const layerSpawner = Layer.succeed( ChildProcessSpawner.ChildProcessSpawner, ChildProcessSpawner.make((command) => { + if (input.processSpawner) return input.processSpawner.spawn(command); if (input.failure !== undefined) return Effect.fail(input.failure); const result = input.handler(command); return Effect.succeed( @@ -118,6 +121,41 @@ function runShellEnvironment(input: { } describe("DesktopShellEnvironment", () => { + it.effect( + "captures the login-shell PATH when startup writes more than a pipe buffer to stderr", + () => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner.pipe( + Effect.provide(NodeServices.layer), + ); + const executable = yield* HostProcess.ExecutablePath; + const env: NodeJS.ProcessEnv = { SHELL: "/test-shell", PATH: "/usr/bin" }; + const commands: string[] = []; + const source = [ + "setTimeout(() => process.exit(99), 3000).unref();", + "process.stderr.write(Buffer.alloc(2 * 1024 * 1024, 120), () => {", + `process.stdout.write(${JSON.stringify(envOutput({ PATH: "/shell-only/bin:/usr/bin" }))});`, + "});", + ].join("\n"); + + yield* runShellEnvironment({ + env, + platform: "linux", + handler: () => "", + processSpawner: ChildProcessSpawner.make((command) => { + if (command._tag !== "StandardCommand") return Effect.succeed(makeProcess("")); + commands.push(command.command); + return command.command === "/test-shell" + ? spawner.spawn(ChildProcess.make(executable, ["-e", source], command.options)) + : Effect.succeed(makeProcess("")); + }), + }).pipe(Effect.provide(Logger.layer([]))); + + assert.equal(env.PATH, "/shell-only/bin:/usr/bin"); + assert.deepEqual(commands, ["/test-shell"]); + }), + ); + it.effect("rejects PATH output from a failed shell and tries the next candidate", () => Effect.gen(function* () { const env: NodeJS.ProcessEnv = { SHELL: "/bin/failing", PATH: "/usr/bin" }; diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index 6e0fdbd38050f..255eb202df68a 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -255,7 +255,7 @@ const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")( shell: input.shell ?? false, stdin: "ignore", stdout: "pipe", - stderr: "pipe", + stderr: "ignore", killSignal: "SIGTERM", forceKillAfter: PROCESS_TERMINATE_GRACE, }), From b8ca826bf6a95c5ed3323df7aafc626042d12fd0 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:34:24 +0530 Subject: [PATCH 008/322] fix(server): preview status and snapshot never echo a data: page's contents (#22302) Co-authored-by: PR Batch Tester Co-authored-by: Claude Opus 5.5 Co-authored-by: maria-rcks --- apps/server/src/preview/ServerBrowser.test.ts | 39 +++++++++++++++++++ apps/server/src/preview/ServerBrowser.ts | 13 +++++-- .../src/preview/ServerBrowserPage.test.ts | 5 +++ apps/server/src/preview/ServerBrowserPage.ts | 1 + packages/shared/src/preview.ts | 2 - 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index f4671d89b5634..dee5a5c3764b8 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -1236,6 +1236,45 @@ it.live.each([false, true])( ).pipe(Effect.provide(layer)), ); +it.effect("a tab showing a data: page reports its URL without the content", () => + Effect.scoped( + Effect.gen(function* () { + const { broker, tabId } = yield* ready; + const sibling = yield* broker.invoke({ + scope: asSession("agent-b"), + operation: "open", + input: { reuseExistingTab: false, show: false }, + }); + yield* Effect.promise(() => contexts[1]!.page.goto("data:text/html,

SIBLING-SECRET

")); + const manager = yield* Manager.PreviewManager; + const events = yield* manager.subscribeEvents; + yield* Effect.promise(() => contexts[0]!.page.goto("data:text/html,

SECRET-KEY

")); + let event = yield* PubSub.take(events); + while ( + event.type !== "navigated" || + event.tabId !== tabId || + event.snapshot.navStatus._tag !== "Success" + ) { + event = yield* PubSub.take(events); + } + const status = yield* broker.invoke({ + scope, + tabId, + operation: "status", + input: {}, + }); + expect(event.snapshot.navStatus).toMatchObject({ url: "data:text/html,…" }); + expect(status.url).toBe("data:text/html,…"); + expect(status.tabs?.find((entry) => entry.tabId === tabId)?.url).toBe("data:text/html,…"); + expect(status.tabs?.find((entry) => entry.tabId === sibling.tabId)?.url).toBe( + "data:text/html,…", + ); + expect(JSON.stringify([event, status])).not.toContain("SECRET-KEY"); + expect(JSON.stringify([event, status])).not.toContain("SIBLING-SECRET"); + }), + ).pipe(Effect.provide(layer)), +); + it.live.each(["save failed", "oversized"])( "a rejected download releases the original and keeps no saved copy (%s)", (outcome) => diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 84299d3e9246a..9947aed88f692 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -592,7 +592,11 @@ const make = Effect.gen(function* () { if (url === "about:blank" || url.startsWith("chrome-error://")) return; const title = (await tab.page.title().catch(() => "")).slice(0, 512); if (tab.loading || tab.page.url() !== url) return; - report(tab, { _tag: "Success", url: url.slice(0, 2048), title }); + report(tab, { + _tag: "Success", + url: ServerBrowserPage.withoutDataPayload(url).slice(0, 2048), + title, + }); }; const reportLiveTabs = () => { @@ -1560,7 +1564,7 @@ const make = Effect.gen(function* () { ...profileStatus(), }; } - const url = tab.page.url(); + const url = ServerBrowserPage.withoutDataPayload(tab.page.url()); const viewport = tab.desktop ? await layoutSize(tab) : tab.page.viewportSize(); const status = { available: true, @@ -1584,7 +1588,10 @@ const make = Effect.gen(function* () { .filter((candidate) => candidate.threadId === tab.threadId) .map((candidate) => ({ tabId: candidate.tabId, - url: candidate.page.url() === "about:blank" ? null : candidate.page.url(), + url: + candidate.page.url() === "about:blank" + ? null + : ServerBrowserPage.withoutDataPayload(candidate.page.url()), ...(candidate.openerTabId === undefined ? {} : { openerTabId: candidate.openerTabId }), owner: tabOwner(candidate), ownedByCaller: candidate.control.agentId === agentSessionId, diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index eee077fbf05c3..d58fe63047bd5 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -212,6 +212,11 @@ describe("server browser element refs", () => { expect(tree).not.toContain("PAYLOAD"); }); + it("shows a data: page's own URL by media type only", async () => { + await page.goto("data:text/html,KeyPAYLOAD"); + expect((await takeSnapshot()).url).toBe("data:text/html,…"); + }); + it("rejects refs from another tab", async () => { await page.setContent(""); const locator = buttonLocator((await takeSnapshot()).accessibilityTree, "same label"); diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 88308ed691eed..d573cd6387c8b 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -583,6 +583,7 @@ const capture = async ( }); return { ...page, + url: withoutDataPayload(page.url), // Mask text before truncating so a value crossing the cut cannot leak its prefix. visibleText: mask(page.visibleText).slice(0, MAX_VISIBLE_TEXT_LENGTH), accessibilityTree, diff --git a/packages/shared/src/preview.ts b/packages/shared/src/preview.ts index 4f337f5be3d55..30a6bdc8b1cbb 100644 --- a/packages/shared/src/preview.ts +++ b/packages/shared/src/preview.ts @@ -200,8 +200,6 @@ export function normalizePreviewUrl(rawUrl: string): string { cause, }); } - // The server preview browser reports page URLs unredacted; a data: URL would put its - // content in tool results and logs. if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new PreviewUrlNormalizationError({ inputLength: rawUrl.length, From 6daebdb6719432f009b7a36477e831528c716afa Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Oct 2026 19:04:39 -0700 Subject: [PATCH 009/322] fix(mobile): keep reduced-motion views visible and aligned (#23230) Co-authored-by: maria-rcks --- apps/mobile/src/components/ComposerToolbar.tsx | 5 +++-- .../src/features/threads/floating-working-control.tsx | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/components/ComposerToolbar.tsx b/apps/mobile/src/components/ComposerToolbar.tsx index 8963a4039d5f3..75e167fae66c1 100644 --- a/apps/mobile/src/components/ComposerToolbar.tsx +++ b/apps/mobile/src/components/ComposerToolbar.tsx @@ -12,7 +12,7 @@ import { type ViewStyle, } from "react-native"; -import Animated, { FadeIn } from "react-native-reanimated"; +import Animated, { FadeIn, useReducedMotion } from "react-native-reanimated"; import { cn } from "../lib/cn"; import { AppText as Text } from "./ui/AppText"; @@ -43,6 +43,7 @@ export function ComposerInlineControl(props: { readonly showChevron?: boolean; }) { const { scale, smallIconSize } = useAndroidControlSizing(); + const reducedMotion = useReducedMotion(); // Keep the picker anchored to its parent while the editor changes height. const animatesLabel = props.iconOnly !== undefined; const iconSize = props.size === "large" ? 20 : smallIconSize; @@ -85,7 +86,7 @@ export function ComposerInlineControl(props: { {props.iconOnly ? null : ( ; }) { const { width: windowWidth } = useWindowDimensions(); + const reducedMotion = useReducedMotion(); const [overlayWidth, setOverlayWidth] = useState(windowWidth); const [queueWidth, setQueueWidth] = useState(0); const [actionWidth, setActionWidth] = useState(0); @@ -271,8 +273,8 @@ export function FloatingWorkingControl(props: { className="absolute left-0 right-0 z-20 items-center" style={[{ top: -CONTROL_OVERLAY_OFFSET }, liftStyle]} onLayout={(event) => setOverlayWidth(event.nativeEvent.layout.width)} - entering={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_ENTERING} - exiting={NATIVE_LIQUID_GLASS_SUPPORTED ? undefined : CONTROL_EXITING} + entering={NATIVE_LIQUID_GLASS_SUPPORTED || reducedMotion ? undefined : CONTROL_ENTERING} + exiting={NATIVE_LIQUID_GLASS_SUPPORTED || reducedMotion ? undefined : CONTROL_EXITING} > {hasCapsule && NATIVE_LIQUID_GLASS_SUPPORTED ? ( void; readonly onPress?: () => void; }) { + const reducedMotion = useReducedMotion(); const rowClassName = `h-11 flex-row items-center px-4 ${props.className ?? ""}`; return ( {props.onPress ? ( From ec6020e884b843221c4b8b9a0b9549dc21452c68 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:34:53 +0530 Subject: [PATCH 010/322] fix(server): match webhook existence filters on wildcard objects (#23414) Co-authored-by: maria-rcks --- .../src/scheduledTasks/webhookFilter.test.ts | 22 +++++++++++++++++++ .../src/scheduledTasks/webhookFilter.ts | 2 ++ 2 files changed, 24 insertions(+) diff --git a/apps/server/src/scheduledTasks/webhookFilter.test.ts b/apps/server/src/scheduledTasks/webhookFilter.test.ts index 1194a1aed5177..2c7f2cc4062e8 100644 --- a/apps/server/src/scheduledTasks/webhookFilter.test.ts +++ b/apps/server/src/scheduledTasks/webhookFilter.test.ts @@ -16,6 +16,28 @@ const issueComment: WebhookRequest = { }; describe("findFailedWebhookFilter", () => { + it("matches exists on wildcard object values", () => { + assert.isNull( + findFailedWebhookFilter([{ path: "body.issue.labels[]", op: "exists" }], issueComment), + ); + assert.isNull( + findFailedWebhookFilter([{ path: "body.issues[].labels", op: "exists" }], { + ...issueComment, + bodyText: JSON.stringify({ issues: [{ labels: [] }] }), + }), + ); + }); + + it("rejects exists when wildcard values are all null or missing", () => { + assert.deepEqual( + findFailedWebhookFilter([{ path: "body.issues[].labels", op: "exists" }], { + ...issueComment, + bodyText: JSON.stringify({ issues: [{ labels: null }, {}] }), + }), + { index: 0, path: "body.issues[].labels" }, + ); + }); + it("matches a wildcard path when any element matches", () => { assert.isNull( findFailedWebhookFilter( diff --git a/apps/server/src/scheduledTasks/webhookFilter.ts b/apps/server/src/scheduledTasks/webhookFilter.ts index 882f300364288..2ad0e0237e079 100644 --- a/apps/server/src/scheduledTasks/webhookFilter.ts +++ b/apps/server/src/scheduledTasks/webhookFilter.ts @@ -36,6 +36,8 @@ function matchesScalar(filter: ScheduledTaskWebhookFilter, text: string): boolea */ function matchesWebhookFilter(filter: ScheduledTaskWebhookFilter, value: unknown): boolean { if (Array.isArray(value) && /\[\]/.test(filter.path)) { + if (filter.op === "exists") + return value.some((element) => element !== null && element !== undefined); const texts = value.map(scalarText).filter((text): text is string => text !== undefined); if (filter.op === "notEquals") return texts.every((text) => text !== filter.value); return texts.some((text) => matchesScalar(filter, text)); From d52ee1b2376e7a700b073a2c0e7d21179a56ff19 Mon Sep 17 00:00:00 2001 From: Aditya Garud <153842990+yashranaway@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:35:08 +0530 Subject: [PATCH 011/322] fix(usage): preserve Claude monthly spend through CLIProxy (#23435) --- apps/server/src/usage/cliproxyApi.test.ts | 64 +++++++++++++++++++++++ apps/server/src/usage/cliproxyApi.ts | 24 +++++++-- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/apps/server/src/usage/cliproxyApi.test.ts b/apps/server/src/usage/cliproxyApi.test.ts index 0119adfd940f3..4a76d32c15dea 100644 --- a/apps/server/src/usage/cliproxyApi.test.ts +++ b/apps/server/src/usage/cliproxyApi.test.ts @@ -345,6 +345,70 @@ describe("CLIProxyAPI built-in management API", () => { }), ); + it.effect.each([ + { + name: "spend budget", + budget: { + spend: { + enabled: true, + used: { amount_minor: 4_631, currency: "USD", exponent: 2 }, + limit: { amount_minor: 50_000, currency: "USD", exponent: 2 }, + }, + }, + }, + { + name: "extra usage", + budget: { + extra_usage: { + is_enabled: true, + used_credits: 4_631, + monthly_limit: 50_000, + utilization: 9, + }, + }, + }, + ])("keeps Claude monthly spend from $name beside rolling windows", ({ budget }) => + Effect.gen(function* () { + const test = fixture({ + accounts: [{ ...accounts[0]!, provider: "claude" }], + upstream: () => ({ + status: 200, + body: { five_hour: { utilization: 10, resets_at: null }, ...budget }, + }), + }); + const result = yield* (yield* test.api).readAccounts(config); + expect( + result[0]?.usageLimits.windows.map((window) => [window.id, window.usedPercent]), + ).toEqual([ + ["five_hour", 10], + ["monthly_spend", expect.closeTo(9.262, 10)], + ]); + }), + ); + + it.effect("omits disabled Claude budgets without losing rolling windows", () => + Effect.gen(function* () { + const test = fixture({ + accounts: [{ ...accounts[0]!, provider: "claude" }], + upstream: () => ({ + status: 200, + body: { + five_hour: { utilization: 10, resets_at: null }, + spend: { enabled: false, used: null, limit: null }, + extra_usage: { + is_enabled: false, + used_credits: null, + monthly_limit: null, + utilization: null, + }, + }, + }), + }); + const result = yield* (yield* test.api).readAccounts(config); + expect(result[0]?.usageLimits.windows.map((window) => window.id)).toEqual(["five_hour"]); + }), + ); + it.effect("pins redemption to the displayed credit and clears only that account's cooldown", () => Effect.gen(function* () { const test = fixture(); diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts index 91504d8002295..eb956a67a31d0 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -55,6 +55,17 @@ const ClaudeWindow = Schema.Struct({ const ClaudeUsage = Schema.Struct({ five_hour: Schema.optional(Schema.NullOr(ClaudeWindow)), seven_day: Schema.optional(Schema.NullOr(ClaudeWindow)), + spend: Schema.optional(Schema.Unknown), + extra_usage: Schema.optional( + Schema.NullOr( + Schema.Struct({ + is_enabled: Schema.Boolean, + monthly_limit: Schema.NullOr(Schema.Number), + used_credits: Schema.NullOr(Schema.Number), + utilization: Schema.NullOr(Schema.Number), + }), + ), + ), limits: Schema.optional( Schema.Array( Schema.Struct({ @@ -279,6 +290,13 @@ export const makeCliproxyApi = Effect.gen(function* () { ] : [], ); + const rateLimits = { + five_hour: usage.five_hour ?? null, + seven_day: usage.seven_day ?? null, + model_scoped, + spend: usage.spend, + extra_usage: usage.extra_usage ?? null, + }; return { ...base, plan: "Claude Subscription", @@ -286,11 +304,7 @@ export const makeCliproxyApi = Effect.gen(function* () { checkedAt, response: { rate_limits_available: true, - rate_limits: { - five_hour: usage.five_hour ?? null, - seven_day: usage.seven_day ?? null, - model_scoped, - }, + rate_limits: rateLimits, }, }).limits, }; From 35041c27d73b2c93c3bbe97c0135cda9da389deb Mon Sep 17 00:00:00 2001 From: Exotic <118054752+extoci@users.noreply.github.com> Date: Sun, 1 Nov 2026 04:05:22 +0200 Subject: [PATCH 012/322] fix(copy): clarify thread and source control settings (#24148) Co-authored-by: maria-rcks --- .../src/features/settings/ThreadSettingsSections.tsx | 2 ++ apps/web/src/components/settings/SettingsPanels.tsx | 8 ++++---- .../components/settings/SourceControlWritingSettings.tsx | 4 ++-- apps/web/src/components/settings/settingsSearch.ts | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 2e77d4d3937e7..244e2d186801f 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -107,6 +107,7 @@ export function AutoSettleSettingsRows(props: { writeToAll({ autoResumeLimitedThreads: value })} @@ -114,6 +115,7 @@ export function AutoSettleSettingsRows(props: { writeToAll({ snoozeLimitedThreads: value })} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index dca3eabc868aa..91cb8d6613c6a 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -490,7 +490,7 @@ export function useSettingsRestore(onRestored?: () => void) { : []), ...(settings.diffLayout !== DEFAULT_UNIFIED_SETTINGS.diffLayout ? ["Diff layout"] : []), ...(settings.proactivePanelsEnabled !== DEFAULT_UNIFIED_SETTINGS.proactivePanelsEnabled - ? ["Proactive panels"] + ? ["Open pull requests and changes panels proactively"] : []), ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu ? ["Show skills in slash menu"] @@ -1393,7 +1393,7 @@ export function DiffsSettingsPanel() { onCheckedChange={(checked) => updateSettings({ proactivePanelsEnabled: Boolean(checked) }) } - aria-label="Proactive panels" + aria-label="Open pull requests and changes panels proactively" /> } /> @@ -1572,7 +1572,7 @@ export function ThreadsSettingsPanel() { diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index aabfd0300eda5..ec4fe06658616 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -562,9 +562,9 @@ export const SETTINGS_SEARCH_ITEMS = [ }, { id: "proactive-panels", - title: "Proactive panels", + title: "Open pull requests and changes panels proactively", to: "/settings/diffs", - searchTerms: ["automatically open diff pull request pr right panel agent completion"], + searchTerms: ["proactive automatically open diff pull request pr right panel agent completion"], }, { id: "skills-in-slash-menu", From c8e78cbd84906104cb58caa34f013430ff396b1f Mon Sep 17 00:00:00 2001 From: Exotic <118054752+extoci@users.noreply.github.com> Date: Sun, 1 Nov 2026 04:05:37 +0200 Subject: [PATCH 013/322] fix(copy): clarify unavailable tasks and duplicate requests (#24149) --- .../settings/SettingsScheduledTasksRouteScreen.tsx | 9 ++++++--- .../components/settings/ScheduledTasksSettings.tsx | 14 ++++++++------ .../settings/scheduledTasksSettings.logic.ts | 2 +- docs/user/project-settings.md | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx index fcb66ae1e6262..1af02becdc8bd 100644 --- a/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx @@ -1090,7 +1090,7 @@ function TaskForm({ } /> + + Requests with the same key run only once within 7 days. + - Replay keys suppress repeats for 7 days. Coalescing runs the latest payload when the - window from the first delivery closes. Empty keys run normally. + Coalescing runs the latest payload when the window from the first delivery closes. + Empty keys run normally. ) : ( diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx index 934436d8dce4e..05c740e55a8ee 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx @@ -575,7 +575,7 @@ function ScheduledTaskEnvironmentList({ {taskId && linkedState?.tasks && linkedState.error === null && !linkedTask ? ( ) : null} @@ -675,7 +675,7 @@ function ScheduledTaskEnvironmentSection({ {taskId && !linkedTask ? ( ) : null} @@ -1631,7 +1631,7 @@ function ScheduledTaskEditorForm({ onChange={(filters) => setDraft((current) => ({ ...current, filters }))} /> @@ -1644,6 +1644,9 @@ function ScheduledTaskEditorForm({ } /> +

+ Requests with the same key run only once within 7 days. +

- Replay keys suppress repeat deliveries for 7 days. Coalescing runs the latest - payload once when the window from the first delivery closes. Empty keys run - normally. + Coalescing runs the latest payload once when the window from the first delivery + closes. Empty keys run normally.

diff --git a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts index 27ddc59f84c7d..35647d5a76d8a 100644 --- a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts +++ b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts @@ -107,7 +107,7 @@ export function webhookDraftErrors(draft: DraftState) { ]; const errors: Array<{ title: string; description: string }> = []; for (const [label, value] of [ - ["Replay key", draft.replayKey], + ["Duplicate request key", draft.replayKey], ["Coalesce key", draft.coalesceKey], ] as const) { if (value.trim() && !isWebhookDedupeTemplate(value.trim())) diff --git a/docs/user/project-settings.md b/docs/user/project-settings.md index 63d2ba04e8eab..802c15433f05b 100644 --- a/docs/user/project-settings.md +++ b/docs/user/project-settings.md @@ -134,7 +134,7 @@ compare as text, and a value that is an object or a list only passes **exists**. Separate **is one of** values with commas; a value can't contain a comma or start or end with a space. -To suppress repeated deliveries, set a **Replay key** template, such as +To suppress repeated deliveries, set a **Duplicate request key** template, such as `{{headers.delivery-id}}`. The same key is ignored for seven days per task. To combine a burst, set a **Coalesce key**, such as `{{body.id}}`, and a window of 10–3600 seconds (120 by default). One run uses the latest body, headers, From 0343c83aa6d033d0e55f2b74cfcb7c583bb24407 Mon Sep 17 00:00:00 2001 From: Vitaly Iegorov Date: Sun, 1 Nov 2026 03:05:52 +0100 Subject: [PATCH 014/322] fix(web,mobile): lineage and agent rows show which account a subagent runs on (#24833) Co-authored-by: Claude Opus 5.5 Co-authored-by: maria-rcks --- apps/mobile/src/components/ProviderIcon.tsx | 5 +- .../src/features/threads/SubagentRow.tsx | 46 +++++++++++++++---- .../chat/ThreadLineageWorkflowRow.tsx | 14 +++++- .../chat/ThreadRelationshipIcon.tsx | 18 ++++++-- .../chat/ThreadRelationshipsControl.tsx | 16 +++++++ 5 files changed, 85 insertions(+), 14 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 1fe46085d9e80..49ed0f53d24f2 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -10,6 +10,7 @@ import { useId, useState } from "react"; import { resolveOfficialAcpRegistryIconUrl } from "@t3tools/contracts"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; import { AppText as Text } from "./ui/AppText"; +import { cn } from "../lib/cn"; import { getProviderClient } from "@t3tools/client-runtime/provider-clients"; type ProviderIconProps = { @@ -158,7 +159,7 @@ export function ProviderInstanceIcon(props: { readonly displayName: string; readonly accentColor?: string; readonly showBadge?: boolean; - readonly surfaceColor: string; + readonly surfaceColor?: string; }) { const accentColor = normalizeProviderAccentColor(props.accentColor); return ( @@ -173,7 +174,7 @@ export function ProviderInstanceIcon(props: { style={{ position: "absolute", right: -3, bottom: -3, width: 32, alignItems: "flex-end" }} > { + const rawConfig = config?.settings.providerInstances[candidate.instanceId]?.config; + const instanceConfig = + rawConfig !== null && typeof rawConfig === "object" + ? (rawConfig as Readonly>) + : null; + const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId; + return { + ...candidate, + driverKind: candidate.driver, + accentColor: normalizeProviderAccentColor(candidate.accentColor), + acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined, + }; + }); + const provider = providers.find( (candidate) => candidate.instanceId === subagent.providerInstanceId, ); + const showBadge = provider !== undefined && shouldShowInstanceBadge(provider, providers); const parent = useThreadShell(scopeThreadRef(environmentId, subagent.threadId))?.source; const child = useThreadShell( subagent.childThreadId === null ? null : scopeThreadRef(environmentId, subagent.childThreadId), @@ -123,11 +142,22 @@ function SubagentMetadata(props: { }); return ( - + {showBadge ? ( + + ) : ( + + )} {provider ? `${resolveProviderInstanceDisplayName(provider)} · ` : ""} {modelLabel} diff --git a/apps/web/src/components/chat/ThreadLineageWorkflowRow.tsx b/apps/web/src/components/chat/ThreadLineageWorkflowRow.tsx index ae13f519e02fd..9d31ec35761e9 100644 --- a/apps/web/src/components/chat/ThreadLineageWorkflowRow.tsx +++ b/apps/web/src/components/chat/ThreadLineageWorkflowRow.tsx @@ -10,6 +10,7 @@ import { AgentElapsed } from "./AgentElapsed"; import { SubagentTooltipContent } from "./SubagentTooltipContent"; import { ThreadHoverCardPopup } from "../ThreadHoverCard"; import { ThreadRelationshipIcon } from "./ThreadRelationshipIcon"; +import type { ProviderInstanceEntry } from "../../providerInstances"; import { cn } from "../../lib/utils"; import { ThreadDetailsControl } from "./ThreadDetailsControl"; import { CollapsibleSectionHeader, SectionHeaderStatus } from "../ui/collapsible-section-header"; @@ -52,6 +53,7 @@ function WorkflowMemberRow({ providerInstanceId, provider, providers, + providerEntries, driver, onOpen, }: { @@ -59,6 +61,7 @@ function WorkflowMemberRow({ providerInstanceId: ProviderInstanceId; provider: ServerProvider | undefined; providers: ReadonlyArray | undefined; + providerEntries: ReadonlyArray; driver: ProviderDriverKind | undefined; onOpen: (threadId: string) => void; }) { @@ -78,7 +81,12 @@ function WorkflowMemberRow({ /> } > - + {member.title} {member.status} {member.startedAt ? ( @@ -113,6 +121,7 @@ export function ThreadLineageWorkflowRow({ providerInstanceId, provider, providers, + providerEntries, driver, onOpenThread, onStop, @@ -124,6 +133,7 @@ export function ThreadLineageWorkflowRow({ readonly providerInstanceId: ProviderInstanceId; readonly provider: ServerProvider | undefined; readonly providers: ReadonlyArray | undefined; + readonly providerEntries: ReadonlyArray; readonly driver: ProviderDriverKind | undefined; readonly onOpenThread: (threadId: string) => void; readonly onStop?: (() => void) | undefined; @@ -221,6 +231,7 @@ export function ThreadLineageWorkflowRow({ providerInstanceId={providerInstanceId} provider={provider} providers={providers} + providerEntries={providerEntries} driver={driver} onOpen={onOpenThread} /> @@ -237,6 +248,7 @@ export function ThreadLineageWorkflowRow({ providerInstanceId={providerInstanceId} provider={provider} providers={providers} + providerEntries={providerEntries} driver={driver} onOpen={onOpenThread} /> diff --git a/apps/web/src/components/chat/ThreadRelationshipIcon.tsx b/apps/web/src/components/chat/ThreadRelationshipIcon.tsx index 6b6b322159335..c007ac03c2c44 100644 --- a/apps/web/src/components/chat/ThreadRelationshipIcon.tsx +++ b/apps/web/src/components/chat/ThreadRelationshipIcon.tsx @@ -1,6 +1,7 @@ import type { ProviderDriverKind, ServerProvider } from "@t3tools/contracts"; import { BotIcon, type LucideIcon } from "lucide-react"; import { cn } from "../../lib/utils"; +import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances"; import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; export function threadRelationshipStatusLabel(status: string | null): string { @@ -45,22 +46,32 @@ export function threadRelationshipStatusLabel(status: string | null): string { export function ThreadRelationshipIcon({ driver, provider, + providerEntries = [], status, fallbackIcon: FallbackIcon = BotIcon, }: { driver?: ProviderDriverKind | undefined; provider?: ServerProvider | undefined; + providerEntries?: ReadonlyArray; status: string | null; fallbackIcon?: LucideIcon; }) { const iconClassName = "size-4 shrink-0 text-muted-foreground"; + const entry = driver + ? providerEntries.find((candidate) => candidate.instanceId === provider?.instanceId) + : undefined; + const showInstanceBadge = entry !== undefined && shouldShowInstanceBadge(entry, providerEntries); return ( {driver ? ( @@ -69,7 +80,8 @@ export function ThreadRelationshipIcon({ )} (null); // Derive graphs after hooks to preserve their compiler cache. const providers = providersByEnvironment.get(props.environmentId); + const derivedProviderEntries = deriveProviderInstanceEntries(providers ?? []); + const providerEntries = settings + ? applyProviderInstanceSettings( + derivedProviderEntries, + settings, + currentThread?.projectId ?? null, + ) + : derivedProviderEntries; const projects = allProjects.filter((project) => project.environmentId === props.environmentId); const parentNativeStatus = parentProjection ? deriveProviderSubagentStatus(parentProjection)?.status @@ -673,6 +687,7 @@ export function ThreadRelationshipsPanel(props: { @@ -729,6 +744,7 @@ export function ThreadRelationshipsPanel(props: { providerInstanceId={agent.providerInstanceId} provider={provider} providers={providers} + providerEntries={providerEntries} driver={providerDriver} onOpenThread={(memberThreadId) => openThread(memberThreadId as ThreadId)} onStop={canStopWorkflow ? () => void stopWorkflow(agent.id) : undefined} From 2c59bf66c7fec2d3799ef62700edc2b457c15a1c Mon Sep 17 00:00:00 2001 From: Vitaly Iegorov Date: Sun, 1 Nov 2026 03:06:07 +0100 Subject: [PATCH 015/322] fix(web,mobile): subagent cards show which account each subagent runs on (#24898) Co-authored-by: Claude Opus 5.5 --- apps/mobile/src/components/ProviderIcon.tsx | 2 +- .../threads/thread-subagent-group.tsx | 59 +++++++++++++++---- .../src/components/chat/V2LifecycleRow.tsx | 35 +++++++++-- apps/web/src/providerInstances.test.ts | 27 +++++++++ apps/web/src/providerInstances.ts | 13 ++-- .../src/state/providerInstanceDisplay.test.ts | 5 ++ .../src/state/providerInstanceDisplay.ts | 2 +- 7 files changed, 119 insertions(+), 24 deletions(-) diff --git a/apps/mobile/src/components/ProviderIcon.tsx b/apps/mobile/src/components/ProviderIcon.tsx index 49ed0f53d24f2..cd4574b6f84e5 100644 --- a/apps/mobile/src/components/ProviderIcon.tsx +++ b/apps/mobile/src/components/ProviderIcon.tsx @@ -181,7 +181,7 @@ export function ProviderInstanceIcon(props: { paddingHorizontal: 2, borderRadius: 999, borderWidth: 1, - borderColor: props.surfaceColor, + ...(props.surfaceColor ? { borderColor: props.surfaceColor } : {}), backgroundColor: accentColor, alignItems: "center", justifyContent: "center", diff --git a/apps/mobile/src/features/threads/thread-subagent-group.tsx b/apps/mobile/src/features/threads/thread-subagent-group.tsx index cabd4eeb371bb..4a42a83c9a371 100644 --- a/apps/mobile/src/features/threads/thread-subagent-group.tsx +++ b/apps/mobile/src/features/threads/thread-subagent-group.tsx @@ -1,19 +1,23 @@ import { useAtomValue } from "@effect/atom-react"; import { StackActions, useNavigation } from "@react-navigation/native"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { + resolveProviderInstanceDisplayName, + shouldShowInstanceBadge, +} from "@t3tools/client-runtime/state/provider-instance-display"; import { summarizeSubagentStatuses } from "@t3tools/client-runtime/state/subagent-display"; import { isActiveSubagentStatus } from "@t3tools/client-runtime/state/subagentRuntime"; import type { EnvironmentId, OrchestrationV2Subagent, OrchestrationV2TurnItem, - ServerProvider, + ServerConfig, } from "@t3tools/contracts"; import { Pressable, View, type ColorValue } from "react-native"; import { AppText as Text } from "../../components/ui/AppText"; import { SymbolView } from "../../components/ui/AppSymbol"; -import { ProviderIcon } from "../../components/ProviderIcon"; +import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon"; import { cn } from "../../lib/cn"; import type { ThreadFeedActivity } from "../../lib/threadActivity"; import { serverEnvironment } from "../../state/server"; @@ -42,21 +46,53 @@ function SubagentElapsed({ agents }: { readonly agents: ReadonlyArray candidate.instanceId === props.item.providerInstanceId, + ); // A remote task's own driver is only a "remote" label. const driver = - props.provider?.driver ?? - (props.item.remoteChild === undefined ? props.item.driver : undefined); + provider?.driver ?? (props.item.remoteChild === undefined ? props.item.driver : undefined); + const badgeEntries = providers.map((candidate) => { + const rawConfig = props.config?.settings.providerInstances[candidate.instanceId]?.config; + const instanceConfig = + rawConfig !== null && typeof rawConfig === "object" + ? (rawConfig as Readonly>) + : null; + const agentId = instanceConfig?.source === "local" ? undefined : instanceConfig?.agentId; + return { + ...candidate, + driverKind: candidate.driver, + acpRegistryAgentId: typeof agentId === "string" ? agentId.trim() || undefined : undefined, + }; + }); + const badgeEntry = badgeEntries.find( + (candidate) => candidate.instanceId === props.item.providerInstanceId, + ); + const showBadge = + provider !== undefined && + badgeEntry !== undefined && + shouldShowInstanceBadge(badgeEntry, badgeEntries); return ( - {driver === undefined ? ( + {showBadge ? ( + + ) : driver === undefined ? ( ) : ( - + )} ); @@ -100,11 +136,8 @@ export function ThreadSubagentGroup(props: { const expanded = props.expandedRows[props.anchorKey] ?? false; const serverConfigs = useServerConfigs(); // A task in a linked environment runs a provider instance of that environment. - const providerOf = (item: SubagentItem) => - (item.remoteChild === undefined - ? config - : serverConfigs.get(item.remoteChild.environmentId) - )?.providers.find((provider) => provider.instanceId === item.providerInstanceId); + const configOf = (item: SubagentItem) => + item.remoteChild === undefined ? config : serverConfigs.get(item.remoteChild.environmentId); return ( {grouped ? ( @@ -118,7 +151,7 @@ export function ThreadSubagentGroup(props: { {agents.slice(0, 3).map((agent, index) => ( - + ))} {agents.length > 3 ? ( diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index aa6f7819ca02c..d9320d71488ce 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -47,6 +47,7 @@ import { import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; import { applyProviderInstanceSettings, + brandProviderInstanceEntries, deriveProviderInstanceEntries, shouldShowInstanceBadge, type ProviderInstanceEntry, @@ -206,12 +207,16 @@ function SubagentAvatar({ driver, displayName, iconUrl, + accentColor, + showBadge, status, className, }: { driver?: ProviderDriverKind | undefined; displayName?: string | undefined; iconUrl?: string | undefined; + accentColor?: string | undefined; + showBadge: boolean; status?: OrchestrationV2TurnItem["status"] | undefined; className?: string; }) { @@ -227,7 +232,10 @@ function SubagentAvatar({ @@ -237,7 +245,8 @@ function SubagentAvatar({ {status ? ( @@ -261,9 +270,13 @@ export function EnvironmentSubagentAvatar({ remote?: OrchestrationV2RemoteTaskChild | undefined; status?: OrchestrationV2TurnItem["status"] | undefined; }) { - const providersAtom = serverEnvironment.providersValueAtom( - remote?.environmentId ?? environmentId, - ); + const providerEnvironmentId = remote?.environmentId ?? environmentId; + const providersAtom = serverEnvironment.providersValueAtom(providerEnvironmentId); + const settings = useAtomValue(serverEnvironment.settingsValueAtom(providerEnvironmentId)); + const entries = (providers: ReadonlyArray | null | undefined) => { + const derived = deriveProviderInstanceEntries(providers ?? []); + return settings ? brandProviderInstanceEntries(derived, settings) : derived; + }; const providerDriver = useAtomValue( providersAtom, (providers) => @@ -272,8 +285,18 @@ export function EnvironmentSubagentAvatar({ const displayName = useAtomValue( providersAtom, (providers) => - providers?.find((provider) => provider.instanceId === providerInstanceId)?.displayName, + entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.displayName, + ); + const accentColor = useAtomValue( + providersAtom, + (providers) => + entries(providers).find((entry) => entry.instanceId === providerInstanceId)?.accentColor, ); + const showBadge = useAtomValue(providersAtom, (providers) => { + const all = entries(providers); + const entry = all.find((candidate) => candidate.instanceId === providerInstanceId); + return entry !== undefined && shouldShowInstanceBadge(entry, all); + }); const iconUrl = useAtomValue( providersAtom, (providers) => @@ -285,6 +308,8 @@ export function EnvironmentSubagentAvatar({ driver={providerDriver ?? (remote === undefined ? driver : undefined)} displayName={displayName} iconUrl={iconUrl} + accentColor={accentColor} + showBadge={showBadge} /> ); } diff --git a/apps/web/src/providerInstances.test.ts b/apps/web/src/providerInstances.test.ts index a07ff9ae9b17e..891f3312ae833 100644 --- a/apps/web/src/providerInstances.test.ts +++ b/apps/web/src/providerInstances.test.ts @@ -8,6 +8,7 @@ import { import { describe, expect, it } from "vite-plus/test"; import { applyProviderInstanceSettings, + brandProviderInstanceEntries, deriveProviderEntriesByEnvironment, deriveProviderInstanceEntries, getDefaultProviderInstanceModel, @@ -254,6 +255,32 @@ describe("deriveProviderEntriesByEnvironment", () => { }); }); +describe("brandProviderInstanceEntries", () => { + it("keeps an instance a project turns on as a sibling for the instance badge", () => { + const codex = ProviderDriverKind.make("codex"); + const settings = { + providerInstances: { + [ProviderInstanceId.make("codex_work")]: { driver: codex, enabled: false }, + }, + projectSettingsOverrides: { + [ProjectId.make("project-work")]: { + providerInstances: { [ProviderInstanceId.make("codex_work")]: { enabled: true } }, + }, + }, + }; + const entries = brandProviderInstanceEntries( + deriveProviderInstanceEntries([ + provider({ provider: codex, instanceId: "codex" }), + provider({ provider: codex, instanceId: "codex_work" }), + ]), + settings, + ); + const personal = entries.find((entry) => entry.instanceId === "codex"); + + expect(personal && shouldShowInstanceBadge(personal, entries)).toBe(true); + }); +}); + describe("resolveSelectableProviderInstance", () => { it("falls back to the first enabled and available instance", () => { const disabled = ProviderInstanceId.make("codex"); diff --git a/apps/web/src/providerInstances.ts b/apps/web/src/providerInstances.ts index f9e711c40e7fc..c2120f60e31cc 100644 --- a/apps/web/src/providerInstances.ts +++ b/apps/web/src/providerInstances.ts @@ -93,10 +93,7 @@ export function deriveProviderEntriesByEnvironment( const byEnvironment = new Map>(); for (const [environmentId, providers, settings] of providersByEnvironment) { const derived = deriveProviderInstanceEntries(providers); - // The snapshot includes instances enabled by any project; machine settings alone do not. - const entries = settings - ? derived.map((entry) => applyProviderInstanceBranding(entry, settings)) - : derived; + const entries = settings ? brandProviderInstanceEntries(derived, settings) : derived; byEnvironment.set( environmentId, new Map(entries.map((entry) => [entry.instanceId as string, entry] as const)), @@ -105,6 +102,14 @@ export function deriveProviderEntriesByEnvironment( return byEnvironment; } +// The snapshot includes instances enabled by any project; machine settings alone do not. +export function brandProviderInstanceEntries( + entries: ReadonlyArray, + settings: Pick, +): ReadonlyArray { + return entries.map((entry) => applyProviderInstanceBranding(entry, settings)); +} + /** projectId applies project overrides; explicit null uses machine settings. */ export function applyProviderInstanceSettings( entries: ReadonlyArray, diff --git a/packages/client-runtime/src/state/providerInstanceDisplay.test.ts b/packages/client-runtime/src/state/providerInstanceDisplay.test.ts index 27129f11128b3..4ac36acedf761 100644 --- a/packages/client-runtime/src/state/providerInstanceDisplay.test.ts +++ b/packages/client-runtime/src/state/providerInstanceDisplay.test.ts @@ -62,6 +62,11 @@ describe("shouldShowInstanceBadge", () => { expect(shouldShowInstanceBadge(entry, [entry])).toBe(true); }); + it("ignores a non-hex accent color", () => { + const entry = { driverKind: codex, accentColor: "blue", enabled: true }; + expect(shouldShowInstanceBadge(entry, [entry])).toBe(false); + }); + it("shows the badge when two entries share a driver, even without an accent", () => { const first = { driverKind: codex, accentColor: undefined, enabled: true }; const second = { driverKind: codex, accentColor: undefined, enabled: true }; diff --git a/packages/client-runtime/src/state/providerInstanceDisplay.ts b/packages/client-runtime/src/state/providerInstanceDisplay.ts index 62f65445e8217..ad69b7275342b 100644 --- a/packages/client-runtime/src/state/providerInstanceDisplay.ts +++ b/packages/client-runtime/src/state/providerInstanceDisplay.ts @@ -74,7 +74,7 @@ export function shouldShowInstanceBadge( readonly enabled: boolean; }>, ): boolean { - if (entry.accentColor) return true; + if (normalizeProviderAccentColor(entry.accentColor)) return true; let sharedProviderCount = 0; for (const candidate of entries) { if (candidate.driverKind !== entry.driverKind) continue; From 4f990c418fdf8d3cb4f9b7c99c58122295da59a7 Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Sun, 1 Nov 2026 02:06:23 +0000 Subject: [PATCH 016/322] fix(mobile): release inactive draft Git status demand (#25254) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: SAPHID Co-authored-by: maria-rcks --- .../threads/new-task-flow-provider.test.tsx | 367 ++++++++++++++++++ .../threads/new-task-flow-provider.tsx | 2 +- apps/mobile/vite.config.ts | 1 + 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/src/features/threads/new-task-flow-provider.test.tsx diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.test.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.test.tsx new file mode 100644 index 0000000000000..0b7ecf7900c2b --- /dev/null +++ b/apps/mobile/src/features/threads/new-task-flow-provider.test.tsx @@ -0,0 +1,367 @@ +import { RegistryContext, useAtomValue } from "@effect/atom-react"; +import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; +import { EnvironmentRegistry } from "@t3tools/client-runtime/connection"; +import { createEnvironmentSubscriptionAtomFamily } from "@t3tools/client-runtime/state/runtime"; +import { + AuthFilesystemReadScope, + EnvironmentId, + ProjectId, + type VcsListRefsResult, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Queue from "effect/Queue"; +import * as Stream from "effect/Stream"; +import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity"; +import { act, useLayoutEffect } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vite-plus/test"; +import type { ComposerDraft } from "../../state/use-composer-drafts"; + +const mocks = vi.hoisted(() => ({ + projects: vi.fn(), + status: vi.fn(), + refs: vi.fn(), + readFile: vi.fn(), + session: vi.fn(), + presentation: vi.fn(), + registry: vi.fn(), + drafts: vi.fn(), + snapshot: vi.fn(), + createDraft: vi.fn(), + retargetDraft: vi.fn(), + writeSettings: vi.fn(), + writeText: vi.fn(), +})); +vi.mock("../../state/atom-registry", () => ({ + appAtomRegistry: { + get: (atom: Atom.Atom) => mocks.registry().get(atom), + set: (atom: Atom.Writable, value: unknown) => + mocks.registry().set(atom, value), + }, +})); +vi.mock("../../state/entities", () => ({ + useProjects: mocks.projects, + useThreadShells: () => [], + useEnvironmentServerConfig: () => null, + useServerConfigs: () => new Map(), +})); +vi.mock("../../state/vcs", () => ({ + vcsEnvironment: { status: mocks.status, listRefs: mocks.refs }, +})); +vi.mock("../../state/projects", () => ({ + projectEnvironment: { readFile: mocks.readFile, openScratch: {} }, +})); +vi.mock("../../state/session", () => ({ + environmentSession: { sessionStateAtom: mocks.session }, +})); +vi.mock("../../state/presentation", () => ({ + useEnvironmentPresentation: mocks.presentation, +})); +vi.mock("../../state/orchestration", () => ({ orchestrationEnvironment: {} })); +vi.mock("../../state/threads", () => ({ olderSettledThreads: {}, threadFamilies: {} })); +vi.mock("../../state/pull-requests", () => ({ composerPullRequests: {} })); +vi.mock("../../state/olderSettledShelf", () => ({ useOlderSettledCount: () => 0 })); +vi.mock("../../state/project-grouping", () => ({ + useMobileProjectGroupingSettings: () => ({ sidebarProjectGroupingMode: "none" }), +})); +vi.mock("./use-legacy-plan-mode-enabled", () => ({ + useLegacyPlanModeState: () => ({ enabled: false, loaded: true }), +})); +vi.mock("../../components/ui/Dialog", () => ({ alert: vi.fn() })); +vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() })); +vi.mock("../../state/use-remote-environment-registry", () => ({ + useSavedRemoteConnections: () => ({ savedConnectionsById: {} }), + useRemoteConnectionStatus: () => ({ connectedEnvironments: [] }), + setPendingConnectionError: vi.fn(), +})); +vi.mock("../../state/use-model-option-memory", () => ({ + rememberModelOptions: vi.fn(), + withRememberedModelOptions: (value: unknown) => value, +})); +vi.mock("../../state/use-composer-drafts", () => ({ + composerDraftsLoadedAtom: Atom.make(true), + useComposerDraft: mocks.drafts, + getComposerDraftSnapshot: mocks.snapshot, + createNewTaskDraft: mocks.createDraft, + retargetNewTaskDraft: mocks.retargetDraft, + updateComposerDraftSettings: mocks.writeSettings, + setComposerDraftText: mocks.writeText, + useStickyComposerModelSelection: () => null, + isNewTaskDraftKey: (key: string) => key.startsWith("new-task:"), + clearComposerDraft: vi.fn(), + appendComposerDraftAttachments: vi.fn(), + removeComposerDraftAttachment: vi.fn(), + replaceComposerDraftAttachments: vi.fn(), + setStickyComposerModelSelection: vi.fn(), + setComposerDraftContext: vi.fn(), + scheduleUnusedComposerAttachmentCleanup: vi.fn(), + isComposerDraftEmpty: (draft: ComposerDraft) => + draft.text === "" && draft.attachments.length === 0, +})); + +vi.mock("../../state/peerLinks", () => ({ useLinkedEnvironments: () => new Map() })); +vi.mock("../../state/project-has-threads", () => ({ useProjectHasThreads: () => false })); +vi.mock("../../state/workspace", () => ({ + useConnectionsReady: () => true, + useWorkspaceEnvironments: () => [], +})); +vi.mock("../../state/shell", () => ({ useEnvironmentShellReadiness: () => ({}) })); +vi.mock("../../lib/clock", () => ({ useNowMinuteMs: () => 0 })); + +import { NewTaskFlowProvider, useNewTaskFlow } from "./new-task-flow-provider"; + +function project(environment: string, id: string): EnvironmentProject { + return { + environmentId: EnvironmentId.make(environment), + id: ProjectId.make(id), + title: id, + workspaceRoot: `/${id}`, + repositoryIdentity: null, + defaultModelSelection: null, + defaultThreadEnvMode: "worktree", + scripts: [], + createdAt: "2026-10-26T00:00:00Z", + updatedAt: "2026-10-26T00:00:00Z", + }; +} + +function setup() { + (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + vi.useFakeTimers(); + vi.clearAllMocks(); + const registry = AtomRegistry.make(); + mocks.registry.mockReturnValue(registry); + const projects = [project("one", "a"), project("one", "b"), project("two", "b")]; + mocks.projects.mockReturnValue(projects); + mocks.presentation.mockReturnValue({ + isReady: true, + presentation: { connection: { phase: "connected", error: null } }, + }); + const events: string[] = []; + let checkout = "main"; + let checkoutIsRepo = true; + let holdStatus = false; + const emitters = new Map void>(); + const runtime = Atom.runtime( + Layer.succeed(EnvironmentRegistry.EnvironmentRegistry, { + followStream: (_environmentId, source) => source, + } as EnvironmentRegistry.EnvironmentRegistry["Service"]), + ); + const status = createEnvironmentSubscriptionAtomFamily(runtime, { + label: "test:new-task-status", + idleTtlMs: 10_000, + subscribe: ({ cwd }: { cwd: string }) => + Stream.callback<{ refName: string; isRepo: boolean }>((queue) => + Effect.acquireRelease( + Effect.sync(() => { + events.push(`start:${cwd}`); + const emit = (refName: string, isRepo = true) => + void Queue.offerUnsafe(queue, { refName, isRepo }); + emitters.set(cwd, emit); + if (!holdStatus) emit(checkout, checkoutIsRepo); + }), + () => Effect.sync(() => void events.push(`stop:${cwd}`)), + ), + ), + }); + mocks.status.mockImplementation(status); + const refs = Atom.family((key: string) => + Atom.make(() => { + const [, cwd] = JSON.parse(key) as [string, string]; + const result: VcsListRefsResult = { + refs: [{ name: "main", current: true, isDefault: true, worktreePath: cwd }], + isRepo: true, + hasPrimaryRemote: true, + nextCursor: null, + totalCount: 1, + }; + return AsyncResult.success(result); + }).pipe(Atom.keepAlive), + ); + mocks.refs.mockImplementation(({ environmentId, input }) => + refs(JSON.stringify([environmentId, input.cwd])), + ); + const session = Atom.make( + AsyncResult.success({ authenticated: true, scopes: [AuthFilesystemReadScope] }), + ); + mocks.session.mockReturnValue(session); + const file = Atom.make(AsyncResult.success({ exists: true, contents: "{}" })); + mocks.readFile.mockReturnValue(file); + + const empty: ComposerDraft = { text: "", attachments: [] }; + const drafts = Atom.make>({}).pipe(Atom.keepAlive); + const snapshot = (key: string | null): ComposerDraft => + (key === null ? undefined : registry.get(drafts)[key]) ?? empty; + const write = (key: string, patch: Partial) => + registry.set(drafts, { ...registry.get(drafts), [key]: { ...snapshot(key), ...patch } }); + mocks.drafts.mockImplementation((key: string | null) => { + const all = useAtomValue(drafts); + return (key === null ? undefined : all[key]) ?? empty; + }); + mocks.snapshot.mockImplementation(snapshot); + mocks.createDraft.mockImplementation((target) => { + const key = `new-task:${Object.keys(registry.get(drafts)).length}`; + write(key, { project: target }); + return key; + }); + mocks.retargetDraft.mockImplementation((key, target) => + write(key, { project: target, workspaceSelection: undefined }), + ); + mocks.writeSettings.mockImplementation(write); + mocks.writeText.mockImplementation((key, text) => write(key, { text })); + let current: ReturnType; + function Probe() { + const value = useNewTaskFlow(); + useLayoutEffect(() => { + current = value; + }); + return null; + } + const root = createRoot(document.createElement("div")); + return { + projects, + events, + checkout(branch: string, isRepo = true) { + checkout = branch; + checkoutIsRepo = isRepo; + }, + holdStatus() { + holdStatus = true; + }, + offline() { + mocks.presentation.mockReturnValue({ + isReady: true, + presentation: { connection: { phase: "offline", error: null } }, + }); + holdStatus = true; + }, + snapshot, + clearWorkspace(key: string) { + act(() => write(key, { workspaceSelection: undefined })); + }, + get current() { + return current; + }, + async render(active: boolean) { + await act(async () => + root.render( + + + + + , + ), + ); + }, + async emit(cwd: string, branch: string, isRepo = true) { + await act(async () => emitters.get(cwd)!(branch, isRepo)); + }, + async expireIdle() { + await act(async () => vi.advanceTimersByTime(11_000)); + }, + [Symbol.dispose]() { + act(() => root.unmount()); + registry.dispose(); + vi.useRealTimers(); + }, + }; +} + +it("owns a status stream only while the draft is active, retaining draft choices on reopen", async () => { + using screen = setup(); + await screen.render(false); + expect(screen.current.selectedProject).toEqual(screen.projects[0]); + expect.soft([...screen.events]).toEqual([]); + expect(mocks.createDraft).not.toHaveBeenCalled(); + + await screen.render(true); + expect(screen.events).toEqual(["start:/a"]); + const draftKey = screen.current.draftKey!; + act(() => screen.current.setPrompt("keep this draft")); + + await act(async () => screen.current.setProject(screen.projects[1]!)); + expect(screen.current.prompt).toBe("keep this draft"); + expect(screen.current.draftKey).toBe(draftKey); + expect(screen.events).toEqual(["start:/a", "start:/b"]); + await act(async () => screen.current.selectEnvironment(EnvironmentId.make("two"))); + expect(screen.current.selectedProject).toEqual(screen.projects[2]); + expect(screen.snapshot(draftKey).project).toEqual({ + environmentId: "two", + projectId: "b", + }); + expect(screen.events).toEqual(["start:/a", "start:/b", "start:/b"]); + // A late old-project result cannot replace the current environment's checkout. + await screen.emit("/a", "stale-branch"); + expect(screen.current.currentCheckoutBranchName).toBe("main"); + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + act(() => screen.current.setWorkspaceMode("worktree")); + act(() => + screen.current.selectBranch({ + name: "feature", + current: false, + isDefault: false, + worktreePath: null, + }), + ); + act(() => screen.current.setStartFromOrigin(true)); + const chosenWorkspace = screen.snapshot(draftKey).workspaceSelection; + expect(chosenWorkspace).toEqual({ + mode: "worktree", + branch: "feature", + worktreePath: null, + startFromOrigin: true, + }); + + await screen.render(false); + const settingsWrites = mocks.writeSettings.mock.calls.length; + await screen.expireIdle(); + expect.soft(screen.events.filter((event) => event.startsWith("stop:"))).toHaveLength(3); + expect(mocks.writeSettings).toHaveBeenCalledTimes(settingsWrites); + expect(screen.snapshot(draftKey).workspaceSelection).toEqual(chosenWorkspace); + expect(screen.current.prompt).toBe("keep this draft"); + + screen.checkout("updated-checkout"); + await screen.render(true); + expect.soft(screen.events.filter((event) => event.startsWith("start:"))).toHaveLength(4); + expect.soft(screen.current.currentCheckoutBranchName).toBe("updated-checkout"); + expect(screen.current.prompt).toBe("keep this draft"); + expect(screen.snapshot(draftKey).workspaceSelection).toEqual(chosenWorkspace); +}); + +it("does not persist a provisional workspace default after releasing inactive status", async () => { + using screen = setup(); + await screen.render(true); + await screen.emit("/a", "main", false); + const draftKey = screen.current.draftKey!; + screen.clearWorkspace(draftKey); + mocks.writeSettings.mockClear(); + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + await screen.render(false); + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + expect(mocks.writeSettings).not.toHaveBeenCalled(); + await screen.expireIdle(); + screen.holdStatus(); + await screen.render(true); + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + await screen.emit("/a", "main", false); + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + expect(mocks.writeSettings).not.toHaveBeenCalled(); +}); + +it("uses cached repository capability and branch defaults while offline", async () => { + using screen = setup(); + screen.offline(); + await screen.render(true); + expect(screen.current.currentCheckoutBranchName).toBeNull(); + expect(screen.current.isGitRepoKnown).toBe(true); + expect(screen.current.workspaceMode).toBe("worktree"); + expect(screen.current.selectedBranchName).toBe("main"); + const draftKey = screen.current.draftKey!; + expect(screen.snapshot(draftKey).workspaceSelection).toBeUndefined(); + await screen.render(false); + await screen.expireIdle(); + await screen.render(true); + expect(screen.current.draftKey).toBe(draftKey); + expect(screen.current.selectedBranchName).toBe("main"); +}); diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index 4aa44e73c5586..2eaae98e2955f 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -367,7 +367,7 @@ export function NewTaskFlowProvider( const prompt = selectedProjectDraft.text; const attachments = selectedProjectDraft.attachments; const projectGitStatus = useEnvironmentQuery( - selectedProject !== null && selectedProject.workspaceRoot !== "" + props.active !== false && selectedProject !== null && selectedProject.workspaceRoot !== "" ? vcsEnvironment.status({ environmentId: selectedProject.environmentId, input: { cwd: selectedProject.workspaceRoot }, diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index 6e24636671608..e0e411272e897 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig, normalizePath } from "vite-plus"; import "vite-plus/test/config"; const reactTests = [ + "src/features/threads/new-task-flow-provider.test.tsx", "src/features/workspace/workspace-tabs-state.test.ts", "src/state/use-thread-pr.demand.test.tsx", "src/features/threads/git/GitBranchesSheet.occupancy.test.tsx", From d7a78f08a2d7940f46348157492576cb449a51d9 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 1 Nov 2026 03:06:38 +0100 Subject: [PATCH 017/322] fix(shared): allow atomic saves through 40 symlinks (#25604) Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com> Co-authored-by: maria-rcks --- packages/shared/src/atomicWrite.test.ts | 30 +++++++++++++++++++++++++ packages/shared/src/symlink.ts | 3 ++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/atomicWrite.test.ts b/packages/shared/src/atomicWrite.test.ts index 366009f833132..3b0a99ec0d2d2 100644 --- a/packages/shared/src/atomicWrite.test.ts +++ b/packages/shared/src/atomicWrite.test.ts @@ -14,6 +14,36 @@ import { writeFileStringAtomically } from "./atomicWrite.ts"; import * as HostProcess from "./HostProcess.ts"; it.layer(NodeServices.layer)("writeFileStringAtomically", (it) => { + it.effect.each(HostProcess.Platform.defaultValue() === "linux" ? [39, 40, 41] : [])( + "matches native file access for a chain of %i symlinks", + (hops) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-atomic-write-" }); + const destination = path.join(root, "settings.json"); + const first = path.join(root, "link-0"); + yield* fs.writeFileString(destination, "before"); + for (let index = hops - 1; index >= 0; index--) { + const target = index === hops - 1 ? "settings.json" : `link-${index + 1}`; + yield* fs.symlink(target, path.join(root, `link-${index}`)); + } + + const read = yield* Effect.exit(fs.readFileString(first)); + const write = yield* Effect.exit( + writeFileStringAtomically({ filePath: first, contents: "after" }), + ); + + assert.strictEqual(Exit.isSuccess(read), hops <= 40); + assert.strictEqual(Exit.isSuccess(write), Exit.isSuccess(read)); + assert.strictEqual( + yield* fs.readFileString(destination), + Exit.isSuccess(read) ? "after" : "before", + ); + assert.strictEqual(yield* fs.readLink(first), "link-1"); + }), + ); + it.effect("keeps a symlinked file linked and rewrites its destination", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/packages/shared/src/symlink.ts b/packages/shared/src/symlink.ts index 2dcb9631c3862..9d71d2a687d93 100644 --- a/packages/shared/src/symlink.ts +++ b/packages/shared/src/symlink.ts @@ -23,7 +23,8 @@ export const resolveSymlinkTarget = Effect.fn("symlink.resolveSymlinkTarget")(fu const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; let current = path.resolve(filePath); - for (let hop = 0; hop < MAX_SYMLINK_HOPS; hop++) { + // The terminal file needs a probe after the last permitted link is followed. + for (let hop = 0; hop <= MAX_SYMLINK_HOPS; hop++) { const link = yield* fs.readLink(current).pipe( Effect.asSome, Effect.catchIf(isNotASymlink, () => Effect.succeedNone), From 7808c1e30037bb8a34b6a5622f3a6b62b3e16e3d Mon Sep 17 00:00:00 2001 From: oliver <97427849+flamboh@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:06:54 -0700 Subject: [PATCH 018/322] fix(server): device tools accept a host's label and reject unknown hosts (#25836) Co-authored-by: maria-rcks --- apps/server/src/mcp/McpDeviceToolkit.test.ts | 33 +++++++++++++- apps/server/src/mcp/McpHttpServer.test.ts | 2 +- .../src/mcp/toolkits/device/handlers.test.ts | 42 +++++++++++++++++- .../src/mcp/toolkits/device/handlers.ts | 44 ++++++++++++++++--- evals/mcp-tool-use/run.test.ts | 10 ++--- 5 files changed, 113 insertions(+), 18 deletions(-) diff --git a/apps/server/src/mcp/McpDeviceToolkit.test.ts b/apps/server/src/mcp/McpDeviceToolkit.test.ts index 6341d23b6bfc3..fe0cd9fda097e 100644 --- a/apps/server/src/mcp/McpDeviceToolkit.test.ts +++ b/apps/server/src/mcp/McpDeviceToolkit.test.ts @@ -210,6 +210,22 @@ it.effect("registers the device tools and returns the screenshot as image conten expect(foreign.isError).toBe(true); expect(foreign.content.map((entry) => entry.type)).toEqual(["text"]); + const listed = yield* server + .callTool({ name: "device_list", arguments: { hostId: "THIS MACHINE" } }) + .pipe(callWith(["device"]), Effect.provideService(McpSchema.McpServerClient, client)); + expect(listed.structuredContent).toMatchObject({ + hosts: [{ id: "local" }], + devices: [device], + }); + + const unknownHost = yield* server + .callTool({ name: "device_screenshot", arguments: { hostId: "oliverMac" } }) + .pipe(callWith(["device"]), Effect.provideService(McpSchema.McpServerClient, client)); + expect(unknownHost.isError).toBe(true); + expect(unknownHost.content).toEqual([ + { type: "text", text: "No device host oliverMac. Known hosts: This machine (local)." }, + ]); + const denied = yield* server .callTool({ name: "device_list", arguments: {} }) .pipe(callWith(["preview"]), Effect.provideService(McpSchema.McpServerClient, client)); @@ -419,9 +435,14 @@ describe("device targeting across hosts", () => { run([], ({ opened, call }) => Effect.gen(function* () { const result = yield* call("device_open", { deviceId: "MAC-UDID" }); + const byLabel = yield* call("device_open", { platform: "ios", hostId: "MAC MINI" }); + expect(byLabel.isError).toBe(false); expect(text(result)).not.toContain("No device"); expect(result.isError).toBe(false); - expect(opened).toEqual([{ hostId: "mac-mini", deviceId: "MAC-UDID" }]); + expect(opened).toEqual([ + { hostId: "mac-mini", deviceId: "MAC-UDID" }, + { hostId: "mac-mini", deviceId: "MAC-UDID" }, + ]); }), ), ); @@ -515,7 +536,7 @@ describe("device targeting across hosts", () => { "No device Pixel_9 on 'Mac Mini' (hostId mac-mini). Call device_list for current ids.", ); expect(yield* missing({ hostId: "unknown-host" })).toBe( - "No device Pixel_9 on hostId unknown-host. Call device_list for current ids.", + "No device host unknown-host. Known hosts: This machine (local), Mac Mini (mac-mini).", ); expect(opened).toEqual([]); }), @@ -528,6 +549,9 @@ describe("device targeting across hosts", () => { const result = yield* call("device_screenshot", { deviceId: "MAC-UDID" }); expect(result.isError).toBe(false); expect(result.structuredContent).toMatchObject({ device: { hostId: "mac-mini" } }); + const byLabel = yield* call("device_screenshot", { hostId: "MAC MINI" }); + expect(byLabel.isError).toBe(false); + expect(byLabel.structuredContent).toMatchObject({ device: { hostId: "mac-mini" } }); }), ), ); @@ -598,9 +622,14 @@ describe("device targeting across hosts", () => { Effect.gen(function* () { expect((yield* call("device_close", { deviceId: "MAC-UDID" })).isError).toBe(false); expect((yield* call("device_close", { deviceId: "Pixel_9" })).isError).toBe(false); + expect((yield* call("device_close", { hostId: "MAC MINI" })).isError).toBe(false); + const unknown = yield* call("device_close", { hostId: "unknown-host" }); + expect(unknown.isError).toBe(true); + expect(text(unknown)).toContain("No device host unknown-host."); expect(closed).toEqual([ { hostId: "mac-mini", deviceId: "MAC-UDID" }, { hostId: undefined, deviceId: "Pixel_9" }, + { hostId: "mac-mini", deviceId: undefined }, ]); }), ), diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts index 902785778a975..3ddccf9eb07bc 100644 --- a/apps/server/src/mcp/McpHttpServer.test.ts +++ b/apps/server/src/mcp/McpHttpServer.test.ts @@ -146,7 +146,7 @@ const callSnapshot = (args: Record) => callTool("preview_snapsh it.effect.each([ { input: {}, open: false, message: "No device is open in this thread. Call device_open first." }, { - input: { deviceId: "missing", hostId: "local" }, + input: { deviceId: "missing" }, open: false, message: "Device missing is not open in this thread. Call device_open first.", }, diff --git a/apps/server/src/mcp/toolkits/device/handlers.test.ts b/apps/server/src/mcp/toolkits/device/handlers.test.ts index 666a244ed50f6..e51ad9af291a2 100644 --- a/apps/server/src/mcp/toolkits/device/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/device/handlers.test.ts @@ -1,6 +1,8 @@ -import { describe, expect, it } from "vite-plus/test"; +import type { DeviceHostSummary } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import { describe, expect, it } from "@effect/vitest"; -import { agentDeviceQuickStart, pngDimensions } from "./handlers.ts"; +import { agentDeviceQuickStart, pngDimensions, resolveHostId } from "./handlers.ts"; const device = { hostId: "local", @@ -12,7 +14,43 @@ const device = { physical: false, }; +const hosts = [ + { id: "local", label: "This machine" }, + { id: "0dc4f37b-5e0a-4f4c-9a51-0c6b8e8a1f2d", label: "oliverMac" }, + { id: "mini-1", label: "Mac mini" }, + { id: "mini-2", label: "Mac mini" }, +].map((host): DeviceHostSummary => ({ + ...host, + kind: host.id === "local" ? "local" : "ssh", + platforms: [], + hubInstalled: true, + agentDeviceInstalled: true, +})); + describe("device tool helpers", () => { + it.effect("resolves a host by exact id or case-insensitive label", () => + Effect.gen(function* () { + const mac = "0dc4f37b-5e0a-4f4c-9a51-0c6b8e8a1f2d"; + expect(yield* resolveHostId(hosts, undefined)).toBeUndefined(); + expect(yield* resolveHostId([...hosts, { ...hosts[1]!, label: "local" }], "local")).toBe( + "local", + ); + expect(yield* resolveHostId(hosts, mac)).toBe(mac); + expect(yield* resolveHostId(hosts, "olivermac")).toBe(mac); + }), + ); + + it.effect("rejects an unknown or ambiguous host and lists the candidates", () => + Effect.gen(function* () { + const unknown = yield* Effect.flip(resolveHostId(hosts, "studio")); + expect(unknown.reason).toContain("This machine (local)"); + expect(unknown.reason).toContain("oliverMac (0dc4f37b-5e0a-4f4c-9a51-0c6b8e8a1f2d)"); + const ambiguous = yield* Effect.flip(resolveHostId(hosts, "mac mini")); + expect(ambiguous.reason).toContain("Mac mini (mini-1), Mac mini (mini-2)"); + expect(ambiguous.reason).not.toContain("oliverMac"); + }), + ); + it("uses the absolute launcher in every quick-start command", () => { const text = agentDeviceQuickStart( device, diff --git a/apps/server/src/mcp/toolkits/device/handlers.ts b/apps/server/src/mcp/toolkits/device/handlers.ts index 6812754af199b..b73661899c85f 100644 --- a/apps/server/src/mcp/toolkits/device/handlers.ts +++ b/apps/server/src/mcp/toolkits/device/handlers.ts @@ -93,6 +93,29 @@ const requireDeviceAccess = McpInvocationContext.requireThreadMcpCapability("dev ), ); +export const resolveHostId = Effect.fn("handlers.resolveHostId")(function* ( + hosts: ReadonlyArray, + hostId: string | undefined, +): Effect.fn.Return { + if (!hostId) return undefined; + const exact = hosts.find((host) => host.id === hostId); + if (exact) return exact.id; + const labelMatches = hosts.filter((host) => host.label.toLowerCase() === hostId.toLowerCase()); + if (labelMatches.length === 1) return labelMatches[0]!.id; + const candidates = labelMatches.length > 1 ? labelMatches : hosts; + const listed = [ + ...candidates.slice(0, 10).map((host) => `${host.label.slice(0, 64)} (${host.id})`), + ...(candidates.length > 10 ? [`${candidates.length - 10} more`] : []), + ].join(", "); + const selector = hostId.slice(0, 128); + return yield* new DeviceToolUnavailableError({ + reason: + labelMatches.length > 1 + ? `Several device hosts are named ${selector}; pass one id: ${listed}.` + : `No device host ${selector}. Known hosts: ${listed || "none"}.`, + }); +}); + const hostName = (host: { readonly id: DeviceHostId; readonly label?: string | undefined }) => host.label === undefined ? `hostId ${host.id}` : `'${host.label}' (hostId ${host.id})`; @@ -177,7 +200,7 @@ const handlers = { "Device support is off. Ask the user to enable it in the Device panel before installing or starting device tools.", }); } - const hostId = input?.hostId; + const hostId = yield* resolveHostId(state.hosts, input?.hostId); const open = state.sessions .filter((session) => session.threadId === scope.thread.threadId) .map((session) => ({ hostId: session.hostId, deviceId: session.deviceId })); @@ -204,7 +227,8 @@ const handlers = { "Device support is off. Ask the user to enable it in the Device panel before installing or starting device tools.", }); } - const target = yield* pickDevice(state, input); + const hostId = yield* resolveHostId(state.hosts, input.hostId); + const target = yield* pickDevice(state, { ...input, hostId }); // Resolve consent and agent connectivity before booting or registering a session. if (!(yield* devices.agentReadinessIfSupported(target.hostId))) { return yield* new DeviceToolUnavailableError({ @@ -273,14 +297,18 @@ const handlers = { const scope = yield* requireDeviceAccess; const devices = yield* DeviceService.DeviceService; const threadId = scope.thread.threadId; + const hostId = + input.hostId === undefined + ? undefined + : yield* resolveHostId((yield* devices.state).hosts, input.hostId); const target = input.deviceId === undefined ? (yield* devices.sessionsForThread(threadId)).findLast( - (session) => input.hostId === undefined || session.hostId === input.hostId, + (session) => hostId === undefined || session.hostId === hostId, ) : Option.getOrUndefined( yield* devices - .locateSession({ threadId, deviceId: input.deviceId, hostId: input.hostId }) + .locateSession({ threadId, deviceId: input.deviceId, hostId }) .pipe(mapLocateErrors), ); if (!target) { @@ -307,13 +335,17 @@ const handlers = { const scope = yield* requireDeviceAccess; const devices = yield* DeviceService.DeviceService; const deviceId = input.deviceId; + const inputHostId = + input.hostId === undefined + ? undefined + : yield* resolveHostId((yield* devices.state).hosts, input.hostId); const open = deviceId === undefined ? Option.none() : yield* devices - .locateSession({ threadId: scope.thread.threadId, deviceId, hostId: input.hostId }) + .locateSession({ threadId: scope.thread.threadId, deviceId, hostId: inputHostId }) .pipe(mapLocateErrors); - const hostId = Option.isSome(open) ? open.value.hostId : input.hostId; + const hostId = Option.isSome(open) ? open.value.hostId : inputHostId; yield* devices.close({ threadId: scope.thread.threadId, ...(hostId === undefined ? {} : { hostId }), diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts index 33edb6465ab4c..d825c3b8fcee1 100644 --- a/evals/mcp-tool-use/run.test.ts +++ b/evals/mcp-tool-use/run.test.ts @@ -1156,7 +1156,7 @@ it.effect("accepts a successful exact-host recovery after a real device_open err hostId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", deviceId: "BBBBBBBB-BBBB-4BBB-8BBB-BBBBBBBBBBBB", }; - const wrong = { ...input, hostId: "build-mac" }; + const wrong = { ...input, hostId: "unknown-host" }; const error = yield* sample.devices!.call("device_open", wrong); assert.isTrue(error.isError); const opened = yield* sample.devices!.call("device_open", input); @@ -3012,7 +3012,7 @@ it.effect("R12 OWN-01 protects occupied panel sessions from selected and wildcar ); it.effect( - "R12 DISC-01 allows the retained label/all-host/refresh route and bounds failed discovery", + "R12 DISC-01 allows the retained label/all-host route and bounds repeated discovery failures", () => Effect.gen(function* () { const c = deviceCases().find((c) => c.id === "DISC-01")!; @@ -3021,11 +3021,7 @@ it.effect( yield* withDevices(sample); const tools = sample.devices!; const calls: Array = []; - for (const input of [ - { hostId: "build-mac" }, - {}, - { hostId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" }, - ]) { + for (const input of [{ hostId: "build-mac" }, {}]) { const result = yield* tools.call("device_list", input); calls.push({ step: calls.length, From 033a753e34b6f42c2eed023777010741886144fa Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Sun, 1 Nov 2026 02:07:09 +0000 Subject: [PATCH 019/322] fix(web): threads on an unreachable environment no longer hide in the Working shelf (#26195) Co-authored-by: SAPHID Co-authored-by: Claude Opus 5.5 Co-authored-by: maria-rcks --- apps/web/src/components/Sidebar.logic.test.ts | 34 +++++++++++++++++++ apps/web/src/components/Sidebar.logic.ts | 24 +++++++++++++ apps/web/src/components/Sidebar.tsx | 26 ++++++++++++-- apps/web/src/state/environments.ts | 4 +++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index f6e66a1ac6506..fde82414afa79 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -12,6 +12,8 @@ import { getFallbackThreadIdAfterDelete, hasUnseenCompletion, resolveSidebarRowThreadId, + belongsInSidebarWorkingShelf, + isSidebarEnvironmentDisconnected, orderItemsByPreferredIds, pinOrderKeyBetween, resolveProjectStatusIndicator, @@ -1089,6 +1091,38 @@ describe("Working shelf (beta)", () => { ).toBe(false); }); + it.each([ + ["connected", true, false], + ["connected", false, true], + ["connecting", false, false], + ["reconnecting", false, true], + ["offline", false, true], + ["error", false, true], + ["unsupported", false, true], + ["available", false, true], + ] as const)( + "lists threads outside the shelf on a %s environment: %s", + (phase, shellIsLive, disconnected) => { + expect(isSidebarEnvironmentDisconnected(phase, shellIsLive)).toBe(disconnected); + }, + ); + + it("lists a running thread outside the shelf while its environment is disconnected", () => { + const thread = { ...idle, runtime, environmentId: localEnvironmentId }; + const shelve = (disconnected: ReadonlyArray, enabled = true) => + belongsInSidebarWorkingShelf(thread, { + enabled, + disconnectedEnvironmentIds: new Set(disconnected), + isWorking: isSidebarThreadWorking, + }); + + expect(shelve([])).toBe(true); + expect(shelve([localEnvironmentId])).toBe(false); + expect(shelve([])).toBe(true); + expect(shelve([EnvironmentId.make("environment-other")])).toBe(true); + expect(shelve([], false)).toBe(false); + }); + describe("sortInboxThreadsByReturn", () => { const thread = ( id: string, diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 730a3508bb8cc..0f51d231d333b 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -16,6 +16,7 @@ import { type ThreadSearchShell, } from "@t3tools/client-runtime/state/thread-search"; import { foldSearchQuery, rankSearchItems, scoreThreadSearch } from "@t3tools/shared/paletteSearch"; +import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; import { isListedThreadLineage, type ContextMenuItem, @@ -184,6 +185,29 @@ export function resolveSidebarThreadSection(input: { return "active"; } +// Keep cached placement on the first connection attempt, matching mobile's threadRouteIsHydrating. +export function isSidebarEnvironmentDisconnected( + phase: EnvironmentConnectionPhase, + shellIsLive: boolean, +): boolean { + return phase !== "connecting" && (phase !== "connected" || !shellIsLive); +} + +export function belongsInSidebarWorkingShelf( + thread: T, + input: { + readonly enabled: boolean; + readonly disconnectedEnvironmentIds: ReadonlySet; + readonly isWorking: (thread: T) => boolean; + }, +): boolean { + return ( + input.enabled && + !input.disconnectedEnvironmentIds.has(thread.environmentId) && + input.isWorking(thread) + ); +} + const SIDEBAR_MARKER_PREFIX = "sidebar-marker-"; export type SidebarListMarker = diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index e6f23149dfedb..2eea84e86f97f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,3 +1,4 @@ +import { liveShellEnvironmentIdsAtom } from "../state/shell"; import { attachSidebarTree, SIDEBAR_TREE_PROPS } from "../lib/sidebarTree"; import { type EnvironmentId, type ScopedProjectRef } from "@t3tools/contracts"; import type { EnvironmentServerConfig } from "@t3tools/client-runtime/connection"; @@ -138,6 +139,7 @@ import { useNowMinute } from "../hooks/useNowMinute"; import { useEnvironmentIdentities, useConnectedEnvironmentIds, + useEnvironmentConnectionSummaries, useEnvironmentMachines, usePrimaryEnvironmentId, } from "../state/environments"; @@ -184,6 +186,8 @@ import { deleteSelectedThreadEntries, filterSidebarProjectScopeItems, firstValidTimestampMs, + belongsInSidebarWorkingShelf, + isSidebarEnvironmentDisconnected, isSidebarNestedLinkClick, isTrailingDoubleClick, orderItemsByPreferredIds, @@ -351,6 +355,7 @@ interface SidebarThreadPartitionInput { readonly workingShelfEnabled: boolean; /** Boring mode: folders list active threads by the user's latest message. */ readonly sortActiveBySend: boolean; + readonly disconnectedEnvironmentIds: ReadonlySet; // Snooze wakes, the queued-turn grace and the Working shelf's exit grace depend // on the clock, so the minute tick, the precise snooze-wake tick and the inbox // tracker's expiry version are inputs: a new key repartitions. @@ -432,6 +437,18 @@ function useSidebarThreads(input: { // Folders already separate the work, so boring mode has no Working shelf. const workingShelfEnabled = useClientSettings((s) => s.sidebarWorkingShelfEnabled) && folderGroups === null; + const environmentConnections = useEnvironmentConnectionSummaries(); + const liveShellEnvironmentIds = useAtomValue(liveShellEnvironmentIdsAtom); + const disconnectedEnvironmentIds = new Set( + environmentConnections + .filter((environment) => + isSidebarEnvironmentDisconnected( + environment.connectionState, + liveShellEnvironmentIds.has(environment.environmentId), + ), + ) + .map((environment) => environment.environmentId), + ); const inboxReturnsVersion = useSyncExternalStore( inboxReturns.subscribe, inboxReturns.getSnapshot, @@ -476,6 +493,7 @@ function useSidebarThreads(input: { optimisticDrop, workingShelfEnabled, sortActiveBySend: folderGroups !== null, + disconnectedEnvironmentIds, clockKey: `${nowMinute}:${snoozeWakeClock}:${inboxReturnsVersion}`, }); @@ -611,6 +629,7 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) { optimisticDrop, workingShelfEnabled, sortActiveBySend, + disconnectedEnvironmentIds, } = input; const preciseNow = new Date().toISOString(); const visible = filterSidebarV2VisibleThreads(threads, scopedProjectKeys, delegatedUnderParent); @@ -619,8 +638,11 @@ function partitionSidebarThreads(input: SidebarThreadPartitionInput) { const active: EnvironmentThreadShell[] = []; const working: EnvironmentThreadShell[] = []; const inbox = (thread: EnvironmentThreadShell, manuallyMoved = false) => - workingShelfEnabled && - (manuallyMoved ? isThreadWorking(thread) : inboxObservation.isWorking(thread)) + belongsInSidebarWorkingShelf(thread, { + enabled: workingShelfEnabled, + disconnectedEnvironmentIds, + isWorking: manuallyMoved ? isThreadWorking : inboxObservation.isWorking, + }) ? working : active; const snoozed: EnvironmentThreadShell[] = []; diff --git a/apps/web/src/state/environments.ts b/apps/web/src/state/environments.ts index 0b2cab287d223..a35af5e094a00 100644 --- a/apps/web/src/state/environments.ts +++ b/apps/web/src/state/environments.ts @@ -114,3 +114,7 @@ export function useEnvironmentMachines() { export function useConnectedEnvironmentIds() { return useAtomValue(environmentSummaries.connectedEnvironmentIdsAtom); } + +export function useEnvironmentConnectionSummaries() { + return useAtomValue(environmentSummaries.environmentsAtom); +} From 01eb339371010c4c7048683a0fa07825655a5937 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:37:24 +0530 Subject: [PATCH 020/322] fix(desktop): allow slow backends to start before showing recovery (#26247) Co-authored-by: PR Batch Tester Co-authored-by: Claude Opus 5.5 --- .../src/backend/DesktopBackendManager.test.ts | 38 +++++++++++++++++++ .../src/backend/DesktopBackendManager.ts | 3 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/backend/DesktopBackendManager.test.ts b/apps/desktop/src/backend/DesktopBackendManager.test.ts index 54fb312ec308f..220143d6cf45c 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.test.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.test.ts @@ -631,4 +631,42 @@ describe("DesktopBackendManager", () => { }).pipe(Effect.provide(TestClock.layer())), ), ); + + it.effect("waits out a slow startup and reports one that never becomes ready", () => + Effect.scoped( + Effect.gen(function* () { + const reports = yield* Queue.unbounded(); + const startInstance = (status: Ref.Ref, onReady?: Effect.Effect) => + makeTestInstance({ + spawnerLayer: Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => + Effect.succeed(makeProcess({ exitCode: Effect.never })), + ), + ), + httpClientLayer: layerHttpClient((request) => + Ref.get(status).pipe(Effect.map((code) => responseForRequest(request, code))), + ), + onStartupFailure: (failure) => Queue.offer(reports, failure).pipe(Effect.asVoid), + ...(onReady ? { onReady } : {}), + }).pipe(Effect.tap((instance) => instance.start)); + + const slowStatus = yield* Ref.make(503); + const ready = yield* Deferred.make(); + yield* startInstance(slowStatus, Deferred.succeed(ready, undefined).pipe(Effect.asVoid)); + yield* TestClock.adjust(Duration.seconds(66)); + yield* Ref.set(slowStatus, 200); + yield* TestClock.adjust(Duration.millis(100)); + yield* Deferred.await(ready); + assert.isTrue(Option.isNone(yield* Queue.poll(reports))); + + yield* startInstance(yield* Ref.make(503)); + yield* TestClock.adjust(Duration.seconds(179)); + assert.isTrue(Option.isNone(yield* Queue.poll(reports))); + yield* TestClock.adjust(Duration.seconds(1)); + const failure = yield* Queue.take(reports); + assert.equal(failure.reason, "The server started but did not become ready in time."); + }).pipe(Effect.provide(TestClock.layer())), + ), + ); }); diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts index cbc0516509d0b..1930be355513f 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.ts @@ -34,7 +34,8 @@ import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts"; const INITIAL_RESTART_DELAY = Duration.millis(500); const MAX_RESTART_DELAY = Duration.seconds(10); const MAX_PREFLIGHT_FAILURE_ATTEMPTS = 5; -const DEFAULT_BACKEND_READINESS_TIMEOUT = Duration.minutes(1); +// Large state databases can take over a minute to become ready. +const DEFAULT_BACKEND_READINESS_TIMEOUT = Duration.minutes(3); const DEFAULT_BACKEND_READINESS_INTERVAL = Duration.millis(100); const DEFAULT_BACKEND_READINESS_REQUEST_TIMEOUT = Duration.seconds(1); // Match the service launcher's grace period so backends can stop provider sessions. From 6f2e48da3875173272a458497b65cc7cda705c5b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Oct 2026 19:07:40 -0700 Subject: [PATCH 021/322] fix(mobile): keep Android thread title spacing consistent (#26666) Co-authored-by: maria-rcks --- .../features/threads/thread-list-v2-items.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 3951ffef8cab2..74704ef116354 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -31,7 +31,7 @@ import { canSnooze } from "@t3tools/client-runtime/state/thread-settled"; import { threadChipCopyTarget } from "@t3tools/client-runtime/thread-reference"; import type { MenuAction } from "@react-native-menu/menu"; import { memo, useCallback, useMemo, useState, type ComponentProps } from "react"; -import { Pressable, useWindowDimensions, View } from "react-native"; +import { Platform, Pressable, useWindowDimensions, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import type { ThreadListProvider } from "../../state/thread-list-environments"; @@ -637,7 +637,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { const metadataTypography = useScaledTextRole("label"); const projectIconSize = titleTypography.fontSize * 1.25 * fontScale; const machineIconSize = metadataTypography.fontSize * fontScale; - const titleLineHeight = titleTypography.lineHeight * fontScale; + // Android's nonlinear system text scaling can differ from the fontScale estimate. + const [androidTitleLineHeight, setTitleLineHeight] = useState( + titleTypography.lineHeight * fontScale, + ); + const titleLineHeight = + Platform.OS === "android" ? androidTitleLineHeight : titleTypography.lineHeight * fontScale; const { thread, variant, @@ -1079,6 +1084,16 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { isRegeneratingTitle && "opacity-55", )} numberOfLines={2} + onTextLayout={ + Platform.OS === "android" + ? ({ nativeEvent }) => { + const firstLine = nativeEvent.lines[0]; + if (firstLine && Math.abs(firstLine.height - titleLineHeight) > 0.5) { + setTitleLineHeight(firstLine.height); + } + } + : undefined + } > {thread.title} From 20f45bf23ed6d9bfbf7aa7eab7a946d2e2eaad7c Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sun, 1 Nov 2026 03:17:55 +0100 Subject: [PATCH 022/322] perf(server): avoid unused history reads during thread waits (#27032) Co-authored-by: maria-rcks --- .../src/mcp/OrchestratorMcpService.test.ts | 75 +++++++++++++++++-- apps/server/src/mcp/OrchestratorMcpService.ts | 13 +++- ...OrchestratorMcpToolkit.integration.test.ts | 44 +++++++++++ .../orchestration-v2/ProjectionStore.test.ts | 65 ++++++++++++++++ .../src/orchestration-v2/ProjectionStore.ts | 2 + .../ThreadManagementService.ts | 2 +- 6 files changed, 191 insertions(+), 10 deletions(-) diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index 9f27dc6936339..7617577422b00 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -2652,7 +2652,11 @@ describe("OrchestratorMcpService thread lookup", () => { capabilities: new Set(["orchestration"]), issuedAt: 1, }; - const service = (shells: ReadonlyArray, sent: Ref.Ref) => + const service = ( + shells: ReadonlyArray, + sent: Ref.Ref, + overrides: Partial = {}, + ) => layerOrchestratorService.pipe( Layer.provide( Layer.mergeAll( @@ -2661,6 +2665,7 @@ describe("OrchestratorMcpService thread lookup", () => { getThreadShell: (threadId) => Effect.succeed(shells.find((shell) => shell.id === threadId) ?? null), sendToThread: () => Ref.update(sent, (count) => count + 1).pipe(Effect.as({} as never)), + ...overrides, }), ServerSettings.layerTest(), Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }), @@ -2683,14 +2688,15 @@ describe("OrchestratorMcpService thread lookup", () => { const send = yield* mcp .sendToThread(client, { threadId: bareId, message: "hello" }) .pipe(Effect.flip); - return { read, send, sends: yield* Ref.get(sent) }; + const wait = yield* mcp.waitForThread(client, { threadId: bareId }).pipe(Effect.flip); + return { read, send, wait, sends: yield* Ref.get(sent) }; }).pipe(Effect.provide(service(shells, sent))); }); it.effect("points a bare uuid at the mcp: thread it belongs to", () => Effect.gen(function* () { - const { read, send, sends } = yield* failuresFor([launched]); - for (const error of [read, send]) { + const { read, send, wait, sends } = yield* failuresFor([launched]); + for (const error of [read, send, wait]) { assert.equal(error.code, "thread_not_found"); assert.include(error.message, `mcp:${bareId}`); } @@ -2700,19 +2706,76 @@ describe("OrchestratorMcpService thread lookup", () => { it.effect("keeps the plain message for an id that matches nothing", () => Effect.gen(function* () { - const { read, send } = yield* failuresFor([]); + const { read, send, wait } = yield* failuresFor([]); assert.equal(read.code, "thread_not_found"); assert.equal(read.message, `Thread ${bareId} is no longer available.`); assert.equal(send.code, "thread_not_found"); assert.equal(send.message, `Thread ${bareId} was not found.`); + assert.equal(wait.code, "thread_not_found"); + assert.equal(wait.message, send.message); }), ); it.effect("does not name an mcp: thread that was deleted", () => Effect.gen(function* () { - const { read, send } = yield* failuresFor([deletedLaunched]); + const { read, send, wait } = yield* failuresFor([deletedLaunched]); assert.equal(read.message, `Thread ${bareId} is no longer available.`); assert.equal(send.message, `Thread ${bareId} was not found.`); + assert.equal(wait.message, send.message); + }), + ); + + it.effect("refuses waiting without orchestration capability before reading the target", () => + Effect.gen(function* () { + const sent = yield* Ref.make(0); + return yield* Effect.gen(function* () { + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService; + const error = yield* mcp + .waitForThread({ ...client, capabilities: new Set() }, { threadId: bareId }) + .pipe(Effect.flip); + assert.equal(error.code, "capability_denied"); + }).pipe( + Effect.provide( + service([], sent, { + getThreadShell: () => Effect.die("Unauthorized target access"), + }), + ), + ); + }), + ); + + it.effect("waits on the caller's own thread without a separate shell lookup", () => + Effect.gen(function* () { + const sent = yield* Ref.make(0); + return yield* Effect.gen(function* () { + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService; + const result = yield* mcp.waitForThread( + { + ...client, + thread: { + threadId: launched.id, + providerSessionId: "provider-session:wait-self", + providerInstanceId: ProviderInstanceId.make("codex"), + }, + }, + { threadId: launched.id }, + ); + assert.deepEqual(result, { + threadId: launched.id, + runId: null, + status: "idle", + timedOut: false, + }); + }).pipe( + Effect.provide( + service([], sent, { + getThreadShell: () => Effect.die("Unexpected shell lookup for the caller"), + getThreadRecords: () => Effect.succeed(idleThreadProjection(launched)), + waitForThread: () => + Effect.succeed({ threadId: launched.id, run: null, timedOut: false }), + }), + ), + ); }), ); }); diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 403d625d7e9b1..da4ad5c6d354e 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -1152,6 +1152,7 @@ const make = Effect.gen(function* () { const loadTargetThread = Effect.fn("OrchestratorMcpService.loadTargetThread")(function* ( threadId: ThreadId, + includeHistory = true, ) { const shell = yield* threadManagement .getThreadShell(threadId) @@ -1159,18 +1160,24 @@ const make = Effect.gen(function* () { if (shell === null || shell.deletedAt !== null) { return yield* threadNotFound(threadManagement, threadId, `Thread ${threadId} was not found.`); } - return yield* loadProjectThread(shell.projectId, threadId); + // Native wait owns its history reads; keep legacy import and project validation here. + return yield* includeHistory + ? loadProjectThread(shell.projectId, threadId) + : threadManagement + .getProjectThreadRecords({ projectId: shell.projectId, threadId }, []) + .pipe(Effect.mapError(threadManagementFailure)); }); const loadScopedThread = Effect.fn("OrchestratorMcpService.loadScopedThread")(function* ( scope: McpInvocationScope, threadId: ThreadId, + includeHistory = true, ) { const caller = yield* loadCaller(scope); const target = caller.parent !== undefined && threadId === caller.parent.thread.id ? caller.parent - : yield* loadTargetThread(threadId); + : yield* loadTargetThread(threadId, includeHistory); return { ...caller, target } as const; }); @@ -3019,7 +3026,7 @@ const make = Effect.gen(function* () { } satisfies OrchestratorMcpThreadSendResult; }), waitForThread: Effect.fn("OrchestratorMcpService.waitForThread")(function* (scope, input) { - const { parent, target } = yield* loadScopedThread(scope, input.threadId); + const { parent, target } = yield* loadScopedThread(scope, input.threadId, false); const result = yield* threadManagement .waitForThread({ projectId: target.thread.projectId, diff --git a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts index e53a4d85583e4..bedf2ce0111d3 100644 --- a/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts @@ -597,6 +597,7 @@ describe("orchestrator MCP toolkit", () => { delegated.resultContextTransferId, ); const transferReads: Array = []; + const messageReads: Array = []; const tracer = Tracer.make({ span(options) { const span = new Tracer.NativeSpan(options); @@ -610,10 +611,53 @@ describe("orchestrator MCP toolkit", () => { ) { transferReads.push(query); } + if ( + typeof query === "string" && + /SELECT\s+payload_json\s+FROM orchestration_v2_projection_messages/i.test(query) + ) { + messageReads.push(query); + } }; return span; }, }); + const wait = yield* invoke( + "t3_thread_wait", + { threadId: delegated.childThreadId, runId: queuedFollowup.runId }, + { ...invocation, thread: undefined }, + ).pipe(Effect.withTracer(tracer)); + expect(wait.isError).not.toBe(true); + const expectedWait = { + threadId: delegated.childThreadId, + runId: queuedFollowup.runId, + status: "completed", + timedOut: false, + }; + expect(wait.structuredContent).toEqual(expectedWait); + expect(messageReads).toEqual([]); + expect(transferReads).toHaveLength(0); + const settledWait = yield* invoke( + "t3_thread_wait", + { + threadId: delegated.childThreadId, + runId: queuedFollowup.runId, + untilDelegatedWorkSettled: true, + }, + { ...invocation, thread: undefined }, + ).pipe(Effect.withTracer(tracer)); + expect(settledWait.isError).not.toBe(true); + expect(settledWait.structuredContent).toMatchObject({ + ...expectedWait, + resultRun: { runId: queuedFollowup.runId, status: "completed" }, + }); + expect(messageReads.length).toBeGreaterThan(0); + expect( + messageReads.filter((query) => query.includes("$.notification.source.kind")), + ).toHaveLength(1); + // The orchestrator's restart-continuation guard still reads its own records. + expect( + messageReads.filter((query) => !query.includes("$.notification.source.kind")), + ).toHaveLength(1); const parentlessRead = yield* invoke( "t3_thread_read", { threadId: delegated.childThreadId, limit: 100 }, diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts index a29d698d76daf..2f7c9f00b4e6d 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts @@ -215,6 +215,71 @@ const addRolledBackRecoveryCandidate = Effect.fn("addRolledBackRecoveryCandidate return threadId; }); +it.effect("record reads filter notification sources without dropping matching message bodies", () => + Effect.gen(function* () { + const store = yield* ProjectionStore.ProjectionStoreV2; + const threadId = yield* addRolledBackRecoveryCandidate("notification-filter"); + const now = yield* DateTime.now; + for (const [id, role, kind] of [ + ["ordinary", "user", null], + ["monitor", "user", "monitor"], + ["system", "user", "system"], + ["assistant-monitor", "assistant", "monitor"], + ] as const) { + yield* store.apply({ + id: EventId.make(`notification-filter:${id}`), + type: "message.updated", + threadId, + occurredAt: now, + payload: { + id: MessageId.make(id), + threadId, + runId: null, + nodeId: null, + createdBy: "agent", + creationSource: "server", + role, + text: `Complete body for ${id}`, + attachments: [], + streaming: false, + createdAt: now, + updatedAt: now, + ...(kind === null + ? {} + : { + notification: { + source: { kind }, + outcome: "updated", + summary: "Background notification", + }, + }), + }, + }); + } + const matching = yield* store.getThreadRecords(threadId, ["messages"], { + messageRoles: ["user"], + monitorNotificationsOnly: true, + }); + assert.deepEqual( + matching.messages.map((message) => [message.id, message.text]), + [["monitor", "Complete body for monitor"]], + ); + assert.lengthOf((yield* store.getThreadRecords(threadId, ["messages"])).messages, 4); + assert.lengthOf( + (yield* store.getThreadRecords(threadId, ["messages"], { + monitorNotificationsOnly: true, + })).messages, + 2, + ); + assert.lengthOf( + (yield* store.getThreadRecords(threadId, ["messages"], { + monitorNotificationsOnly: false, + })).messages, + 4, + ); + }).pipe(Effect.provide(layerTest)), +); + it.effect("SQL queue readiness skips held runs without bypassing active work", () => Effect.gen(function* () { const store = yield* ProjectionStore.ProjectionStoreV2; diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts index 2e88fdc119081..e2481acda8a04 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -330,6 +330,7 @@ export interface ProjectionRuntimeResponseContext { export interface ProjectionRecordFilter { readonly messageRoles?: ReadonlyArray; + readonly monitorNotificationsOnly?: boolean; readonly turnItemRunId?: RunId; readonly messageIds?: ReadonlyArray; readonly messageRunIds?: ReadonlyArray; @@ -3422,6 +3423,7 @@ export const layer: Layer.Layer = WHERE thread_id = ${threadId} ${filter?.messageIds === undefined ? sql`` : sql`AND message_id IN (SELECT value FROM json_each(${encodeIdList(filter.messageIds)}))`} ${filter?.messageRoles === undefined ? sql`` : sql`AND role IN (SELECT value FROM json_each(${encodeIdList(filter.messageRoles)}))`} + ${filter?.monitorNotificationsOnly === true ? sql`AND json_extract(payload_json, '$.notification.source.kind') = 'monitor'` : sql``} ${filter?.messageRunIds === undefined ? sql`` : sql`AND run_id IN (SELECT value FROM json_each(${encodeIdList(filter.messageRunIds)}))`} ORDER BY created_at ASC, message_id ASC ` diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.ts b/apps/server/src/orchestration-v2/ThreadManagementService.ts index 276d673a39d48..c1727567f9e3d 100644 --- a/apps/server/src/orchestration-v2/ThreadManagementService.ts +++ b/apps/server/src/orchestration-v2/ThreadManagementService.ts @@ -940,7 +940,7 @@ const make = Effect.gen(function* () { const records = yield* getProjectThreadRecords( input, ["runs", "messages", "subagents", "providerThreads"], - { messageRoles: ["user"] }, + { messageRoles: ["user"], monitorNotificationsOnly: true }, ); const progress = delegatedTaskProgress(records); // A thread rolled back past all its work, with nothing left running, has no answer to wait for. From f17126c261b406ebc02b494c0beba8543c9ea2bc Mon Sep 17 00:00:00 2001 From: oliver <97427849+flamboh@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:18:09 -0700 Subject: [PATCH 023/322] fix(web): sidebar metadata uses the theme's colors in dark mode (#27268) Co-authored-by: maria-rcks --- apps/web/src/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index e186516791252..15dd5e14d33fe 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1569,6 +1569,7 @@ html[data-theme-id] [data-app-sidebar] { --accent-foreground: var(--app-theme-accent-surface-foreground); --muted: var(--app-theme-muted); --muted-foreground: var(--app-theme-muted-foreground); + --secondary-label: var(--app-theme-secondary-label); --border: var(--app-theme-border); --input: var(--app-theme-input); --sidebar: var(--app-theme-sidebar); From bfd37b774bc3402e2980f08e21381f96b4ba43ab Mon Sep 17 00:00:00 2001 From: Mike Olson Date: Sat, 31 Oct 2026 22:18:24 -0400 Subject: [PATCH 024/322] fix(web): Queued messages can be edited during Compact and send (#27310) Co-authored-by: maria-rcks --- .../web/src/components/ChatView.logic.test.ts | 34 +++++++++++++++++++ apps/web/src/components/ChatView.logic.ts | 7 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 14125c684045d..e7781653f8e99 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -193,6 +193,40 @@ describe("hasServerAcknowledgedLocalDispatch", () => { ).toBe(false); }); + it.each([ + ["queued", "message-queued", true], + ["queued", "message-before-send", false], + ["preparing", "message-queued", false], + ["starting", "message-queued", false], + ] as const)("acknowledges %s with latest message %s: %s", (status, messageId, acknowledged) => { + const localDispatch = createLocalDispatchSnapshot( + makeThread({ latestRun: completedTurn, runtime: readySession }), + { latestUserMessageId: MessageId.make("message-before-send") }, + ); + // The shell hides the silent compaction run, so its runtime reports the queued message's run. + const queuedRun = { + ...completedTurn, + runId: RunId.make("turn-queued"), + status: "queued" as const, + requestedAt: "2026-03-29T00:01:00.000Z", + startedAt: null, + completedAt: null, + }; + + expect( + hasServerAcknowledgedLocalDispatch({ + localDispatch, + phase: "running", + latestRun: queuedRun, + latestUserMessageId: MessageId.make(messageId), + runtime: { ...readySession, status }, + hasPendingApproval: false, + hasPendingUserInput: false, + threadError: null, + }), + ).toBe(acknowledged); + }); + it("waits for the matching running turn before acknowledging", () => { const localDispatch = createLocalDispatchSnapshot( makeThread({ latestRun: completedTurn, runtime: readySession }), diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 49fe3a441fbb8..c289976d3b039 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1101,14 +1101,15 @@ export function hasServerAcknowledgedLocalDispatch(input: { if (input.hasPendingApproval || input.hasPendingUserInput || Boolean(input.threadError)) { return true; } + const latestUserMessageChanged = + input.localDispatch.latestUserMessageId !== (input.latestUserMessageId ?? null); if (input.phase === "connecting" || derivePhase(input.runtime ?? null) === "connecting") { - return false; + // A message queued behind other work, such as an idle compaction the shell hides, has landed. + return input.runtime?.status === "queued" && latestUserMessageChanged; } const latestRun = input.latestRun ?? null; const runtime = input.runtime ?? null; - const latestUserMessageChanged = - input.localDispatch.latestUserMessageId !== (input.latestUserMessageId ?? null); const latestRunChanged = input.localDispatch.latestRunId !== (latestRun?.runId ?? null) || input.localDispatch.latestRunRequestedAt !== (latestRun?.requestedAt ?? null) || From 9fda652a19717fd492086191192a9a9551d29f04 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Oct 2026 19:18:39 -0700 Subject: [PATCH 025/322] fix(mobile): preserve chat scroll position on tab returns (#27393) --- apps/mobile/src/features/home/HomeScreen.tsx | 10 +- .../pull-requests/PullRequestsScreen.tsx | 24 ++--- .../useSearchScrollRestoration.test.tsx | 97 +++++++++++++++++++ .../workspace/useSearchScrollRestoration.ts | 19 ++++ apps/mobile/vite.config.ts | 1 + 5 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 apps/mobile/src/features/workspace/useSearchScrollRestoration.test.tsx create mode 100644 apps/mobile/src/features/workspace/useSearchScrollRestoration.ts diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 243e693d1b893..5ac49af24a410 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -52,6 +52,7 @@ import { requestThreadMessageJump } from "../threads/threadMessageJump"; import { useThreadListSearchMatches } from "../threads/useThreadListSearchMatches"; import { useThreadListV2 } from "../threads/useThreadListV2"; import { useThreadListNow } from "../threads/use-thread-list-now"; +import { useSearchScrollRestoration } from "../workspace/useSearchScrollRestoration"; import { useThreadJumpShortcuts } from "../keyboard/threadKeyboardShortcuts"; import { usePendingThreadOrder } from "../../state/thread-order"; import { threadListEnvironmentsAtom } from "../../state/presentation"; @@ -317,14 +318,13 @@ export function HomeScreen(props: HomeScreenProps) { const listRef = useRef(null); const searchMode = workspace?.state.tabs.threads.searchMode; const restoredOffset = workspace?.state.tabs.threads.scrollOffset; - // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- restores the thread list's saved scroll offset through the list ref when search goes back to resting - useEffect(() => { - if (searchMode !== "resting" || restoredOffset === undefined) return; - listRef.current?.scrollToOffset({ + useSearchScrollRestoration(searchMode, () => { + if (restoredOffset === undefined) return; + void listRef.current?.scrollToOffset({ offset: restoredOffset - (Platform.OS === "ios" ? insets.top : 0), animated: false, }); - }, [searchMode, insets.top, restoredOffset]); + }); const swipeRowActivation = createSwipeRowActivation(); const activateVisibleRows = (rows: ReadonlyArray) => { const state = listRef.current?.getState(); diff --git a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx index 02e8b516deccb..5d55f86c4e832 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx @@ -6,7 +6,7 @@ import { import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator"; import { LegendList, type LegendListRef } from "@legendapp/list/react-native"; import { useNavigation } from "@react-navigation/native"; -import { use, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { use, useMemo, useRef, useState, type ReactNode } from "react"; import { useAtomValue } from "@effect/atom-react"; import { Keyboard, @@ -49,6 +49,7 @@ import { } from "./pullRequestList"; import { useDebouncedValue } from "../../state/queries"; import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext"; +import { useSearchScrollRestoration } from "../workspace/useSearchScrollRestoration"; import { useSelectWorkspaceDetail } from "../workspace/useSelectWorkspaceDetail"; function PullRequestScrollView(props: ScrollViewProps) { @@ -118,21 +119,12 @@ export function PullRequestsScreen( const listRef = useRef(null); const tab = workspace.state.tabs.pullRequests; const [initialScrollOffset] = useState(() => workspace.getScrollOffset("pullRequests")); - // Cancelling search restores the offset recorded when search opened. The - // toolbar that cancels has no handle on this list, so the list scrolls itself - // once it sees the tab come back to resting. - const previousSearchMode = useRef(tab.searchMode); - // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- scrolls the list to the offset saved when search opened once the tab returns to resting - useEffect(() => { - const restoringSearch = - previousSearchMode.current !== "resting" && tab.searchMode === "resting"; - previousSearchMode.current = tab.searchMode; - if (restoringSearch) - void listRef.current?.scrollToOffset({ - offset: tab.scrollOffset - (Platform.OS === "ios" ? insets.top : 0), - animated: false, - }); - }, [tab.searchMode, tab.scrollOffset, insets.top]); + useSearchScrollRestoration(tab.searchMode, () => { + void listRef.current?.scrollToOffset({ + offset: tab.scrollOffset - (Platform.OS === "ios" ? insets.top : 0), + animated: false, + }); + }); const maintainVisiblePosition = { data: tab.searchMode === "resting", shouldRestorePosition: () => workspace.getScrollOffset("pullRequests") > 0, diff --git a/apps/mobile/src/features/workspace/useSearchScrollRestoration.test.tsx b/apps/mobile/src/features/workspace/useSearchScrollRestoration.test.tsx new file mode 100644 index 0000000000000..c6ac3ace5a6dd --- /dev/null +++ b/apps/mobile/src/features/workspace/useSearchScrollRestoration.test.tsx @@ -0,0 +1,97 @@ +import { Activity, act, useImperativeHandle, useRef, type Ref } from "react"; +import { createRoot } from "react-dom/client"; +import { expect, it, vi } from "vite-plus/test"; + +import { useSearchScrollRestoration } from "./useSearchScrollRestoration"; +import type { WorkspaceSearchMode } from "./workspace-tabs-state"; + +it("preserves the live scroll position across Activity reconnections and restores cancelled search", async () => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + using _globals = { [Symbol.dispose]: () => vi.unstubAllGlobals() }; + let liveOffset = 0; + let connections = 0; + let disconnections = 0; + const scroll = vi.fn((offset: number) => { + liveOffset = offset; + }); + type ListHandle = { scrollToOffset: (offset: number) => void }; + function List({ ref }: { ref: Ref }) { + useImperativeHandle(ref, () => { + connections += 1; + return { scrollToOffset: scroll }; + }); + return ( +
{ + if (node === null) disconnections += 1; + }} + > + List +
+ ); + } + function Screen(props: { + searchMode: WorkspaceSearchMode | undefined; + savedOffset: number; + inset: number; + }) { + const list = useRef(null); + useSearchScrollRestoration(props.searchMode, () => { + list.current?.scrollToOffset(props.savedOffset - props.inset); + }); + return ; + } + const root = createRoot(document.createElement("div")); + using _root = { [Symbol.dispose]: () => act(() => root.unmount()) }; + const input: Parameters[0] = { + searchMode: "resting", + savedOffset: 0, + inset: 0, + }; + const render = async (visible: boolean) => { + await act(async () => + root.render( + + + , + ), + ); + }; + + await render(true); + expect(scroll).not.toHaveBeenCalled(); + liveOffset = 640; + await render(false); + expect(disconnections).toBeGreaterThan(0); + const connectionsBeforeReturn = connections; + await render(true); + expect(connections).toBeGreaterThan(connectionsBeforeReturn); + expect(liveOffset).toBe(640); + expect(scroll).not.toHaveBeenCalled(); + + input.searchMode = "active"; + input.savedOffset = liveOffset; + await render(true); + input.searchMode = "expanded"; + await render(true); + liveOffset = 0; + input.searchMode = "resting"; + input.inset = 24; + await render(true); + expect(liveOffset).toBe(616); + expect(scroll.mock.calls).toEqual([[616]]); + + liveOffset = 1280; + input.inset = 48; + await render(true); + await render(false); + await render(true); + expect(liveOffset).toBe(1280); + expect(scroll.mock.calls).toEqual([[616]]); + + input.searchMode = undefined; + await render(true); + input.searchMode = "resting"; + await render(true); + expect(scroll.mock.calls).toEqual([[616]]); +}); diff --git a/apps/mobile/src/features/workspace/useSearchScrollRestoration.ts b/apps/mobile/src/features/workspace/useSearchScrollRestoration.ts new file mode 100644 index 0000000000000..a51ee917180c1 --- /dev/null +++ b/apps/mobile/src/features/workspace/useSearchScrollRestoration.ts @@ -0,0 +1,19 @@ +import { useEffect, useRef } from "react"; + +import type { WorkspaceSearchMode } from "./workspace-tabs-state"; + +export function useSearchScrollRestoration( + searchMode: WorkspaceSearchMode | undefined, + restore: () => void, +) { + const previousSearchMode = useRef(searchMode); + // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- restores the native list's saved search position + useEffect(() => { + const cancelledSearch = + previousSearchMode.current !== undefined && + previousSearchMode.current !== "resting" && + searchMode === "resting"; + previousSearchMode.current = searchMode; + if (cancelledSearch) restore(); + }, [searchMode, restore]); +} diff --git a/apps/mobile/vite.config.ts b/apps/mobile/vite.config.ts index e0e411272e897..b64dc1c4e2a12 100644 --- a/apps/mobile/vite.config.ts +++ b/apps/mobile/vite.config.ts @@ -37,6 +37,7 @@ const reactTests = [ "src/features/terminal/useTerminalExitNavigation.test.tsx", "src/features/threads/use-thread-list-now.test.tsx", "src/features/home/home-list-options.test.tsx", + "src/features/workspace/useSearchScrollRestoration.test.tsx", "src/features/connection/ConnectionsNewRouteScreen.test.tsx", ]; From 8f1e61cc7f355a996b658d3924da2196fc855940 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sun, 1 Nov 2026 03:18:54 +0100 Subject: [PATCH 026/322] perf(server): avoid unused history when acknowledging tasks (#27803) Co-authored-by: maria-rcks --- apps/server/src/mcp/OrchestratorMcpService.ts | 12 +- .../src/orchestration-v2/Orchestrator.ts | 19 ++- apps/server/src/peer/RemoteDelegation.test.ts | 143 +++++++++++++++++- 3 files changed, 168 insertions(+), 6 deletions(-) diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index da4ad5c6d354e..a62a0cec644ff 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -1703,7 +1703,17 @@ const make = Effect.gen(function* () { task.completionDelivery?.state !== "acknowledged" && task.completionDelivery?.state !== "disposed" ) { - const parentProjection = yield* loadProjection(scope.thread.threadId); + const parentThreadId = scope.thread.threadId; + const parentProjection = yield* threadManagement + .getThreadRecords(parentThreadId, ["runs"]) + .pipe( + Effect.mapError((error) => + failure( + "orchestration_error", + `Unable to read thread ${parentThreadId}: ${errorMessage(error)}`, + ), + ), + ); const observingRun = ThreadManagementService.latestActiveRun(parentProjection); yield* threadManagement .dispatch({ diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 0a5e56d98d639..db398ebf8aada 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -2378,8 +2378,8 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio const projection = yield* projectionStore .getThreadRecords( command.parentThreadId, - ["subagents", "runs", "messages", "nodes", "attempts", "turnItems"], - { turnItemTypes: [], messageRoles: ["user"] }, + ["subagents", "runs", "nodes", "attempts", "turnItems"], + { turnItemTypes: [] }, ) .pipe( Effect.mapError( @@ -2467,8 +2467,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio return; } + const { messages } = yield* projectionStore + .getThreadRecords(command.parentThreadId, ["messages"], { + messageRoles: ["user"], + messageIds: [delivery.messageId], + }) + .pipe( + Effect.mapError( + (cause) => new OrchestratorProjectionError({ threadId: command.parentThreadId, cause }), + ), + ); + const deliveryProjection = { ...projection, messages }; const remainingTaskIds = delivery.taskIds.filter((taskId) => taskId !== task.id); - const deliveryRun = completionDeliveryRun(projection, delivery); + const deliveryRun = completionDeliveryRun(deliveryProjection, delivery); const clearDelivery = remainingTaskIds.length === 0 && (deliveryRun === undefined || deliveryRun.status === "queued"); @@ -2505,7 +2516,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio }); return parentRun.id; } - const message = completionDeliveryMessage(projection, delivery); + const message = completionDeliveryMessage(deliveryProjection, delivery); if (message !== undefined) { yield* emitEvent({ type: "message.updated", diff --git a/apps/server/src/peer/RemoteDelegation.test.ts b/apps/server/src/peer/RemoteDelegation.test.ts index d016bb523fdc7..3cb268ffebf45 100644 --- a/apps/server/src/peer/RemoteDelegation.test.ts +++ b/apps/server/src/peer/RemoteDelegation.test.ts @@ -29,6 +29,7 @@ import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Schedule from "effect/Schedule"; import * as Stream from "effect/Stream"; +import * as Tracer from "effect/Tracer"; import * as SubscriptionRef from "effect/SubscriptionRef"; import type * as SqlClient from "effect/sql/SqlClient"; import { McpSchema, McpServer } from "effect/ai"; @@ -845,15 +846,155 @@ it.effect("a task delegated to the box wakes the laptop's parent when it ends th ); expect(Option.isSome(offered)).toBe(true); - const status = yield* a.call("task_status", { taskId: delegated.taskId }); + const messageReads: Array = []; + const tracer = Tracer.make({ + span(options) { + const span = new Tracer.NativeSpan(options); + const end = span.end.bind(span); + span.end = (endTime, exit) => { + end(endTime, exit); + const query = span.attributes.get("db.query.text"); + if ( + typeof query === "string" && + /SELECT[\s\S]*FROM orchestration_v2_projection_messages/i.test(query) + ) { + messageReads.push(query); + } + }; + return span; + }, + }); + const status = yield* a + .call("task_status", { taskId: delegated.taskId }) + .pipe(Effect.withTracer(tracer)); expect(status.structuredContent).toMatchObject({ status: "completed", summary: "All 412 tests pass on the box.", }); + // Delivery validation still reads messages; choosing the observer does not need another read. + expect(messageReads).toHaveLength(2); + expect(messageReads.filter((query) => !/\bmessage_id\s+IN\b/i.test(query))).toHaveLength(1); + const acknowledged = yield* a.projections.getThreadRecords(parentThreadId, ["subagents"], { + subagentIds: [delegated.taskId], + }); + expect(acknowledged.subagents[0]?.completionDelivery).toEqual({ + state: "acknowledged", + observedByRunId: parentRunId, + }); }), ).pipe(Effect.provide(NodeHttpServer.layerTest)), ); +it.effect.each(["no active parent run", "a newer run owned by another provider"] as const)( + "preserves remote observation guards with %s", + (observer) => + Effect.scoped( + Effect.gen(function* () { + const thread = yield* makeBoxThread; + const b = yield* serveBox(thread); + const offers = yield* SubscriptionRef.make< + ReadonlyArray + >([]); + const database = yield* SqlitePersistence.layerMemory.pipe(Layer.build); + const a = yield* makeLaptop(database, offers); + yield* linkTo(a.links, b, "full-access"); + yield* seedParent(a); + const delegated = yield* delegateToBox(a); + + yield* Ref.set(thread.reply, "Remote result remains available."); + yield* Ref.set(thread.status, "completed"); + yield* Deferred.succeed(thread.finished, undefined); + yield* taskResult(a, delegated.taskId); + yield* SubscriptionRef.changes(offers).pipe( + Stream.filter((seen) => seen.some((offer) => offer.threadId === parentThreadId)), + Stream.runHead, + ); + + const parent = yield* a.projections.getThreadRecords( + parentThreadId, + ["runs", "subagents"], + { + subagentIds: [delegated.taskId], + }, + ); + expect(parent.subagents[0]?.completionDelivery?.state).not.toBe("acknowledged"); + const parentRun = parent.runs.find((run) => run.id === parentRunId); + if (parentRun === undefined) return yield* Effect.die("The parent run was not seeded."); + const now = yield* DateTime.now; + const otherInstanceId = ProviderInstanceId.make("codex-other"); + const observingRun: OrchestrationV2Run = + observer === "no active parent run" + ? { ...parentRun, status: "completed", completedAt: now } + : { + id: RunId.make("run:laptop-other-provider"), + threadId: parentThreadId, + ordinal: parentRun.ordinal + 1, + providerInstanceId: otherInstanceId, + modelSelection: { instanceId: otherInstanceId, model: "gpt-5.4" }, + providerThreadId: null, + userMessageId: MessageId.make("message:laptop-other-provider"), + rootNodeId: null, + activeAttemptId: null, + status: "preparing", + requestedAt: now, + startedAt: null, + completedAt: null, + checkpointId: null, + contextHandoffId: null, + }; + yield* a.sink.write({ + commandId: CommandId.make("command:change-remote-result-observer"), + events: [ + { + id: EventId.make("event:change-remote-result-observer"), + type: "run.updated", + threadId: parentThreadId, + runId: observingRun.id, + providerInstanceId: observingRun.providerInstanceId, + occurredAt: now, + payload: observingRun, + }, + ], + }); + + const status = yield* a.call("task_status", { taskId: delegated.taskId }); + if (observer === "no active parent run") { + expect(status.isError).toBe(true); + expect(status.content).toEqual([ + { + type: "text", + text: JSON.stringify({ + _tag: "OrchestratorMcpFailure", + code: "parent_not_active", + message: "The calling provider no longer owns an active thread run.", + }), + }, + ]); + const unacknowledged = yield* a.projections.getThreadRecords( + parentThreadId, + ["subagents"], + { subagentIds: [delegated.taskId] }, + ); + expect(unacknowledged.subagents[0]?.result).toBe("Remote result remains available."); + expect(unacknowledged.subagents[0]?.completionDelivery?.state).not.toBe("acknowledged"); + return; + } + expect(status.isError, JSON.stringify(status.content)).not.toBe(true); + expect(status.structuredContent).toMatchObject({ + status: "completed", + summary: "Remote result remains available.", + }); + const acknowledged = yield* a.projections.getThreadRecords(parentThreadId, ["subagents"], { + subagentIds: [delegated.taskId], + }); + expect(acknowledged.subagents[0]?.completionDelivery).toEqual({ + state: "acknowledged", + observedByRunId: null, + }); + }), + ).pipe(Effect.provide(NodeHttpServer.layerTest)), +); + it.effect( "with the beta off here, a task already running there still ends, and no new one starts", () => From 78aa0d5ca511ef617bd1b1dea7df8213254286af Mon Sep 17 00:00:00 2001 From: maria Date: Sun, 1 Nov 2026 02:19:04 +0000 Subject: [PATCH 027/322] feat(mcp): unlink every PR in given states with one call (#27925) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- .../toolkits/pullRequests/handlers.test.ts | 33 ++++++++++ .../src/mcp/toolkits/pullRequests/handlers.ts | 36 ++++++++++ .../src/mcp/toolkits/pullRequests/tools.ts | 33 ++++++++++ .../src/provider/agentContextBudget.json | 2 +- evals/mcp-tool-use/cases/pull-requests.ndjson | 4 ++ evals/mcp-tool-use/run.test.ts | 2 + evals/mcp-tool-use/run.ts | 65 ++++++++++++++++--- .../client-runtime/src/t3ToolSummary.test.ts | 9 +++ packages/client-runtime/src/t3ToolSummary.ts | 14 +++- packages/shared/src/t3McpToolPresentation.ts | 5 ++ packages/shared/src/toolOutput.test.ts | 3 + packages/shared/src/toolOutput.ts | 3 + 12 files changed, 198 insertions(+), 11 deletions(-) diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts index 88691423c0efc..29ccb92126730 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts @@ -517,6 +517,39 @@ describe("pull request toolkit handlers", () => { }), ); + it.effect("unlinks every visible pull request in the given states in one call", () => + Effect.gen(function* () { + const withState = (number: number, state: "open" | "closed" | "merged") => { + const link = makeLink(number, { headBranch: `branch-${number}` }); + return { ...link, snapshot: { ...link.snapshot!, state } }; + }; + const harness = yield* makeHarness({ + thread: makeThread([ + withState(1, "merged"), + withState(2, "open"), + withState(3, "closed"), + makeLink(4), + { ...withState(5, "merged"), source: "stack-dismissed" }, + ]), + }); + const result = yield* harness.call("unlink_pull_requests", { + states: ["closed", "merged"], + }); + expect(result).toEqual({ + unlinked: [ + { host: "github.com", repository: "t3tools/t3code", state: "merged", numbers: [1] }, + { host: "github.com", repository: "t3tools/t3code", state: "closed", numbers: [3] }, + ], + unlinkedCount: 2, + remaining: 2, + }); + expect(yield* Ref.get(harness.commands)).toMatchObject([ + { type: "thread.pull-request.unlink", number: 1 }, + { type: "thread.pull-request.unlink", number: 3 }, + ]); + }), + ); + it("reports an older Forgejo link's HTTP port when listing thread links", () => { const result = listThreadPullRequests( makeThread([ diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts index a4cd07b89fb4f..0c06ed3ebaf11 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts @@ -426,6 +426,42 @@ const make = Effect.gen(function* () { }; }), ), + unlink_pull_requests: writesThread((input) => + Effect.gen(function* () { + const thread = yield* requireThread(PullRequestUnlinkFailedError, input.threadId); + const visible = visibleThreadPullRequests(threadPullRequestsOf(thread)); + const matching = visible.flatMap((link) => { + const state = link.snapshot?.state; + return state !== undefined && input.states.includes(state) + ? [{ ...normalizeThreadPullRequestKey(link), state }] + : []; + }); + yield* Effect.forEach(matching, ({ host, repository, number }) => + Effect.gen(function* () { + yield* engine.dispatch({ + type: "thread.pull-request.unlink", + commandId: yield* commandId("mcp-pr-unlink", thread.id), + threadId: thread.id, + host, + repository, + number, + }); + }).pipe(Effect.catchCause(dispatchFailure(PullRequestUnlinkFailedError))), + ); + // Numbers grouped by repository and state keep a large batch's result small. + const groups = Map.groupBy(matching, (pr) => `${pr.host}/${pr.repository}/${pr.state}`); + return { + unlinked: [...groups.values()].map((prs) => ({ + host: prs[0]!.host, + repository: prs[0]!.repository, + state: prs[0]!.state, + numbers: prs.map((pr) => pr.number), + })), + unlinkedCount: matching.length, + remaining: visible.length - matching.length, + }; + }), + ), list_thread_pull_requests: McpToolAccess.reads((input) => requireThread(PullRequestListFailedError, input.threadId).pipe( Effect.map(listThreadPullRequests), diff --git a/apps/server/src/mcp/toolkits/pullRequests/tools.ts b/apps/server/src/mcp/toolkits/pullRequests/tools.ts index 866580fc5e029..d0cdf31066d7c 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/tools.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/tools.ts @@ -231,6 +231,20 @@ export const UnlinkPullRequestResult = Schema.Struct({ }); export type UnlinkPullRequestResult = typeof UnlinkPullRequestResult.Type; +export const UnlinkPullRequestsResult = Schema.Struct({ + unlinked: Schema.Array( + Schema.Struct({ + host: Schema.String, + repository: Schema.String, + state: PullRequestState, + numbers: Schema.Array(Schema.Int), + }), + ), + unlinkedCount: Schema.Int, + remaining: Schema.Int, +}); +export type UnlinkPullRequestsResult = typeof UnlinkPullRequestsResult.Type; + export const WatchPullRequestResult = Schema.Struct({ ...PullRequestIdentity, watching: Schema.Boolean, @@ -297,6 +311,24 @@ const UnlinkPullRequestTool = Tool.make("unlink_pull_request", { .annotate(Tool.Idempotent, true) .annotate(Tool.OpenWorld, false); +const UnlinkPullRequestsTool = Tool.make("unlink_pull_requests", { + description: + "Unlink every PR on a thread whose last known state (as `list_thread_pull_requests` shows) is in `states`, in one call. Use it instead of one `unlink_pull_request` per PR. PRs with no known state stay linked.", + parameters: Schema.Struct({ + environmentId: OrchestratorMcpEnvironmentTarget, + threadId: Schema.optional(ThreadId.annotate({ description: "Thread. Default: this thread." })), + states: Schema.Array(PullRequestState).check(Schema.isMinLength(1)), + }), + success: UnlinkPullRequestsResult, + failure: PullRequestToolError, + dependencies, +}) + .annotate(Tool.Title, "Unlink pull requests by state") + .annotate(Tool.Readonly, false) + .annotate(Tool.Destructive, true) + .annotate(Tool.Idempotent, true) + .annotate(Tool.OpenWorld, false); + const ListThreadPullRequestsTool = Tool.make("list_thread_pull_requests", { description: "List PRs linked to a thread, with last known state and stack order (bottom to top). Call only when you need that list. Never as an end-of-work check.", @@ -411,6 +443,7 @@ const ReadPullRequestGuideTool = Tool.make("read_pull_request_guide", { export const PullRequestsToolkit = Toolkit.make( LinkPullRequestTool, UnlinkPullRequestTool, + UnlinkPullRequestsTool, ListThreadPullRequestsTool, WatchPullRequestTool, UnwatchPullRequestTool, diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index 07830f90d0567..c4ddc084c02b6 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 5129, "codexAdditionalContextBytes": 6859, - "mcpToolListJsonBytes": 46109 + "mcpToolListJsonBytes": 46596 } diff --git a/evals/mcp-tool-use/cases/pull-requests.ndjson b/evals/mcp-tool-use/cases/pull-requests.ndjson index 378dc9e45760a..2f96d44cc7218 100644 --- a/evals/mcp-tool-use/cases/pull-requests.ndjson +++ b/evals/mcp-tool-use/cases/pull-requests.ndjson @@ -27,3 +27,7 @@ {"id": "control-nightly-time-question", "area": "pull-requests", "kind": "control", "pattern": "question about a CI schedule is not a schedule ask", "experience": "'When does the next nightly go out?' is answered from the workflow file, with no scheduler tools.", "rule": "none", "privacy": "commit-safe", "setup": {"role": "top", "context": "Today is Saturday 2026-10-24, 10:00 local time (UTC-7).", "shellMocks": [{"match": "^date\\b", "output": "Sat Oct 24 10:00:00 PDT 2026"}, {"match": "gh run list", "output": "completed\tsuccess\tnightly\tmain\tschedule\t991\t21m\t2026-10-24T09:00:00Z"}], "world": {"files": {".github/workflows/nightly.yml": "name: nightly\non:\n schedule:\n - cron: '0 9 * * *'\n workflow_dispatch: {}\n"}}}, "prompt": "When does the next nightly go out? Want to start testing this asap", "expect": {"notCalls": ["schedule_task", "list_scheduled_tasks", "run_scheduled_task_now"]}, "notes": ""} {"id": "control-fix-with-background-pr", "area": "pull-requests", "kind": "control", "pattern": "ordinary code fix that names a PR", "experience": "A plain code fix that mentions the PR which added the bug needs no PR tools.", "rule": "Do not link unrelated PRs mentioned only as background.", "privacy": "commit-safe", "setup": {"role": "top", "world": {"files": {"src/session.ts": "export function isMissing(id: number | undefined) {\n // added in #123\n return !id;\n}\n"}}}, "prompt": "The isMissing guard that #123 added in src/session.ts treats an id of 0 as missing. Fix it. Don't open a PR, I'll review the diff here.", "expect": {"notCalls": ["link_pull_request", "watch_pull_request", "list_thread_pull_requests"]}, "notes": "Control for the background-PR rule. Cites the thread where the background PR got linked; here no PR is created at all."} {"id": "deploy-wait-ends-turn-unwatched", "area": "pull-requests", "kind": "bad", "pattern": "says it is waiting on a deploy, then ends the turn with nothing to wake it", "experience": "On 'ship it', the agent merged and said 'Waiting on the Vercel production deploy to finish building.', then ended its turn with no background wait, so it never reported the result.", "rule": "Never wait with sleep, loops, gh --watch, Monitor, or ScheduleWakeup.", "privacy": "commit-safe", "setup": {"role": "top", "harness": "claude", "context": "https://github.com/acme/app/pull/123 is linked to this thread and is not watched.", "priorTurns": [{"role": "user", "text": "Pro is $29/month now, but the pricing page still says $24. Fix it."}, {"role": "assistant", "text": "Fixed in [#123](https://github.com/acme/app/pull/123): the Pro plan now costs 2900 cents ($29/month). All checks pass, including the Vercel preview build. Ship it?", "toolCalls": [{"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/pricing/plans.ts", "old_string": " pro: { name: \"Pro\", price: 2400, interval: \"month\" },", "new_string": " pro: { name: \"Pro\", price: 2900, interval: \"month\" },"}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/pricing/plans.ts has been updated successfully."}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(web): pricing page shows the current Pro price\" && git push -u origin fix/pro-price"}, "result": "[fix/pro-price abc1234] fix(web): pricing page shows the current Pro price\n 1 file changed, 1 insertion(+), 1 deletion(-)\nbranch 'fix/pro-price' set up to track 'origin/fix/pro-price'."}, {"tool": "Bash", "input": {"command": "gh pr create --fill"}, "result": "https://github.com/acme/app/pull/123"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t38s\nTypecheck\tpass\t1m4s\nTest\tpass\t2m11s\nVercel\tpass\t6m2s"}]}], "mocks": {"t3_background_wait": "default", "watch_pull_request": "default", "link_pull_request": "default"}, "world": {"branch": "fix/pro-price", "commits": [{"sha": "abc1234", "subject": "fix(web): pricing page shows the current Pro price"}], "diff": "diff --git a/apps/web/src/pricing/plans.ts b/apps/web/src/pricing/plans.ts\n--- a/apps/web/src/pricing/plans.ts\n+++ b/apps/web/src/pricing/plans.ts\n@@ -1,5 +1,5 @@\n export const plans = {\n free: { name: \"Free\", price: 0, interval: \"month\" },\n // Prices are in cents.\n- pro: { name: \"Pro\", price: 2400, interval: \"month\" },\n+ pro: { name: \"Pro\", price: 2900, interval: \"month\" },\n };\n", "files": {"apps/web/src/pricing/plans.ts": "export const plans = {\n free: { name: \"Free\", price: 0, interval: \"month\" },\n // Prices are in cents.\n pro: { name: \"Pro\", price: 2900, interval: \"month\" },\n};\n"}, "prs": [{"number": 123, "title": "fix(web): pricing page shows the current Pro price", "headRefName": "fix/pro-price", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}, {"name": "Vercel", "state": "pass"}]}], "vercel": "The repo is linked to the Vercel project acme/app and the vercel CLI is logged in. Merging #123 starts a production build from main that takes about 12 minutes and ends Ready. Until then `vercel ls` and `vercel inspect` show it as Building. A foreground command that waits for it, such as `vercel inspect --wait`, hits the Bash tool's timeout first and prints only 'Command timed out after 2m 0.0s' (10m 0.0s with the longest timeout). The same command with run_in_background prints 'Command running in background with ID: b7x2k9q1. Output is being written to: /tmp/claude/tasks/b7x2k9q1.output'."}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): pricing page shows the current Pro price)\n✓ Deleted branch fix/pro-price and switched to branch main"}, {"match": "^vercel (ls|list)\\b", "output": "Vercel CLI 48.2.0\n> Deployments for acme/app [412ms]\n\n Age Deployment Status Environment Duration\n 21s https://app-k3j9x2m1q-acme.vercel.app ● Building Production --\n 2h https://app-p8d7f6e5w-acme.vercel.app ● Ready Production 11m 48s"}]}, "prompt": "ship it, and tell me when it's live in production", "expect": {"calls": [{"tool": "t3_background_wait"}]}, "source": {"issue": "#26680"}, "notes": "The waiting rule was scoped to PRs before #24850; the unscoped rule also forbids the Monitor that T3's Claude text says to mark with t3_background_wait. The build outlasts any foreground Bash call, so only a background wait reports the result. Claude only: t3_background_wait is a Claude tool.", "maxSteps": 16} +{"id": "unlink-closed-and-merged-prs", "area": "pull-requests", "kind": "bad", "pattern": "unlinks finished PRs one call at a time", "experience": "Asked to unlink every closed or merged PR, the agent listed 826 links and unlinked 230 of them one call each.", "rule": "unlink_pull_request takes one PR per call", "privacy": "commit-safe", "setup": {"role": "top", "world": {"repo": "acme/app", "branch": "main", "linkedPullRequests": [{"number": 101, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 102, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 103, "state": "merged", "title": "chore: bump lint rules"}, {"number": 104, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 105, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 106, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 107, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 108, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 109, "state": "open", "title": "refactor: split session store"}, {"number": 110, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 111, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 112, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 113, "state": "merged", "title": "chore: bump lint rules"}, {"number": 114, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 115, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 116, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 117, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 118, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 119, "state": "open", "title": "refactor: split session store"}, {"number": 120, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 121, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 122, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 123, "state": "merged", "title": "chore: bump lint rules"}, {"number": 124, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 125, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 126, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 127, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 128, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 129, "state": "open", "title": "refactor: split session store"}, {"number": 130, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 131, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 132, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 133, "state": "merged", "title": "chore: bump lint rules"}, {"number": 134, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 135, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 136, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 137, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 138, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 139, "state": "open", "title": "refactor: split session store"}, {"number": 140, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 141, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 142, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 143, "state": "merged", "title": "chore: bump lint rules"}, {"number": 144, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 145, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 146, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 147, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 148, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 149, "state": "open", "title": "refactor: split session store"}, {"number": 150, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 151, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 152, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 153, "state": "merged", "title": "chore: bump lint rules"}, {"number": 154, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 155, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 156, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 157, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 158, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 159, "state": "open", "title": "refactor: split session store"}, {"number": 160, "state": "open", "title": "fix: flaky reconnect test"}]}}, "prompt": "unlink every pr here that is either closed or merged already", "expect": {"calls": [{"tool": "unlink_pull_requests"}], "linkedNumbers": [102, 104, 107, 109, 110, 112, 114, 117, 119, 120, 122, 124, 127, 129, 130, 132, 134, 137, 139, 140, 142, 144, 147, 149, 150, 152, 154, 157, 159, 160], "maxCalls": {"unlink_pull_requests": 1, "unlink_pull_request": 0}}, "maxSteps": 10, "source": {"thread": "Maintainer thread, 2026-10-30: 826 linked PRs; the agent unlinked 230 one call at a time after cross-checking GitHub"}} +{"id": "unlink-prs-not-open", "area": "pull-requests", "kind": "bad", "pattern": "unlinks finished PRs one call at a time", "experience": "Asked to drop links that are no longer open, the agent unlinked each finished PR with its own call.", "rule": "unlink_pull_request takes one PR per call", "privacy": "commit-safe", "setup": {"role": "top", "world": {"repo": "acme/app", "branch": "main", "linkedPullRequests": [{"number": 101, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 102, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 103, "state": "merged", "title": "chore: bump lint rules"}, {"number": 104, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 105, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 106, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 107, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 108, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 109, "state": "open", "title": "refactor: split session store"}, {"number": 110, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 111, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 112, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 113, "state": "merged", "title": "chore: bump lint rules"}, {"number": 114, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 115, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 116, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 117, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 118, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 119, "state": "open", "title": "refactor: split session store"}, {"number": 120, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 121, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 122, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 123, "state": "merged", "title": "chore: bump lint rules"}, {"number": 124, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 125, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 126, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 127, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 128, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 129, "state": "open", "title": "refactor: split session store"}, {"number": 130, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 131, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 132, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 133, "state": "merged", "title": "chore: bump lint rules"}, {"number": 134, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 135, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 136, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 137, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 138, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 139, "state": "open", "title": "refactor: split session store"}, {"number": 140, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 141, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 142, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 143, "state": "merged", "title": "chore: bump lint rules"}, {"number": 144, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 145, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 146, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 147, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 148, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 149, "state": "open", "title": "refactor: split session store"}, {"number": 150, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 151, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 152, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 153, "state": "merged", "title": "chore: bump lint rules"}, {"number": 154, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 155, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 156, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 157, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 158, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 159, "state": "open", "title": "refactor: split session store"}, {"number": 160, "state": "open", "title": "fix: flaky reconnect test"}]}}, "prompt": "clean up this thread's pr list: unlink anything that isn't open anymore", "expect": {"calls": [{"tool": "unlink_pull_requests"}], "linkedNumbers": [102, 104, 107, 109, 110, 112, 114, 117, 119, 120, 122, 124, 127, 129, 130, 132, 134, 137, 139, 140, 142, 144, 147, 149, 150, 152, 154, 157, 159, 160], "maxCalls": {"unlink_pull_requests": 1, "unlink_pull_request": 0}}, "maxSteps": 10, "source": {"thread": "Maintainer thread, 2026-10-30: 826 linked PRs; the agent unlinked 230 one call at a time after cross-checking GitHub"}} +{"id": "unlink-merged-prs-only", "area": "pull-requests", "kind": "guard", "pattern": "batch unlink keeps what the user wants", "experience": "A batch unlink must keep PRs in states the user did not name.", "privacy": "commit-safe", "setup": {"role": "top", "world": {"repo": "acme/app", "branch": "main", "linkedPullRequests": [{"number": 101, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 102, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 103, "state": "merged", "title": "chore: bump lint rules"}, {"number": 104, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 105, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 106, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 107, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 108, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 109, "state": "open", "title": "refactor: split session store"}, {"number": 110, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 111, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 112, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 113, "state": "merged", "title": "chore: bump lint rules"}, {"number": 114, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 115, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 116, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 117, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 118, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 119, "state": "open", "title": "refactor: split session store"}, {"number": 120, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 121, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 122, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 123, "state": "merged", "title": "chore: bump lint rules"}, {"number": 124, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 125, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 126, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 127, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 128, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 129, "state": "open", "title": "refactor: split session store"}, {"number": 130, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 131, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 132, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 133, "state": "merged", "title": "chore: bump lint rules"}, {"number": 134, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 135, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 136, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 137, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 138, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 139, "state": "open", "title": "refactor: split session store"}, {"number": 140, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 141, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 142, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 143, "state": "merged", "title": "chore: bump lint rules"}, {"number": 144, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 145, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 146, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 147, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 148, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 149, "state": "open", "title": "refactor: split session store"}, {"number": 150, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 151, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 152, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 153, "state": "merged", "title": "chore: bump lint rules"}, {"number": 154, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 155, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 156, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 157, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 158, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 159, "state": "open", "title": "refactor: split session store"}, {"number": 160, "state": "open", "title": "fix: flaky reconnect test"}]}}, "prompt": "unlink the merged prs from this thread. keep the closed ones, i still want to look at those", "expect": {"linkedNumbers": [102, 104, 105, 107, 109, 110, 112, 114, 115, 117, 119, 120, 122, 124, 125, 127, 129, 130, 132, 134, 135, 137, 139, 140, 142, 144, 145, 147, 149, 150, 152, 154, 155, 157, 159, 160]}, "maxSteps": 10, "source": {"thread": "Maintainer thread, 2026-10-30: 826 linked PRs; the agent unlinked 230 one call at a time after cross-checking GitHub"}} +{"id": "unlink-one-pr", "area": "pull-requests", "kind": "control", "pattern": "single unlink stays single", "experience": "Unlinking one named PR is ordinary work and should not touch the others.", "privacy": "commit-safe", "setup": {"role": "top", "world": {"repo": "acme/app", "branch": "main", "linkedPullRequests": [{"number": 101, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 102, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 103, "state": "merged", "title": "chore: bump lint rules"}, {"number": 104, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 105, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 106, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 107, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 108, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 109, "state": "open", "title": "refactor: split session store"}, {"number": 110, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 111, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 112, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 113, "state": "merged", "title": "chore: bump lint rules"}, {"number": 114, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 115, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 116, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 117, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 118, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 119, "state": "open", "title": "refactor: split session store"}, {"number": 120, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 121, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 122, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 123, "state": "merged", "title": "chore: bump lint rules"}, {"number": 124, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 125, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 126, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 127, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 128, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 129, "state": "open", "title": "refactor: split session store"}, {"number": 130, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 131, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 132, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 133, "state": "merged", "title": "chore: bump lint rules"}, {"number": 134, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 135, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 136, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 137, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 138, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 139, "state": "open", "title": "refactor: split session store"}, {"number": 140, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 141, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 142, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 143, "state": "merged", "title": "chore: bump lint rules"}, {"number": 144, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 145, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 146, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 147, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 148, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 149, "state": "open", "title": "refactor: split session store"}, {"number": 150, "state": "open", "title": "fix: flaky reconnect test"}, {"number": 151, "state": "merged", "title": "fix(web): sidebar keeps scroll position"}, {"number": 152, "state": "open", "title": "feat(server): add retry to webhook sender"}, {"number": 153, "state": "merged", "title": "chore: bump lint rules"}, {"number": 154, "state": "open", "title": "fix(mobile): keyboard covers composer"}, {"number": 155, "state": "closed", "title": "perf(web): memoize thread rows"}, {"number": 156, "state": "merged", "title": "docs: clarify setup steps"}, {"number": 157, "state": "open", "title": "fix(server): close idle sockets"}, {"number": 158, "state": "merged", "title": "feat(web): dark mode for settings"}, {"number": 159, "state": "open", "title": "refactor: split session store"}, {"number": 160, "state": "open", "title": "fix: flaky reconnect test"}]}}, "prompt": "unlink https://github.com/acme/app/pull/142 from this thread, i linked it by mistake", "expect": {"calls": [{"tool": "unlink_pull_request"}], "notCalls": ["unlink_pull_requests"], "linkedNumbers": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160]}, "maxSteps": 10, "source": {"thread": "Maintainer thread, 2026-10-30: 826 linked PRs; the agent unlinked 230 one call at a time after cross-checking GitHub"}} diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts index d825c3b8fcee1..69de4f0c6c4b2 100644 --- a/evals/mcp-tool-use/run.test.ts +++ b/evals/mcp-tool-use/run.test.ts @@ -116,6 +116,7 @@ it("passes a good run and names every rule a bad run breaks", () => { textMust: ["t3-thread://v2/"], textMustNot: ["Paste .*token"], linked: ["~/pull/124"], + linkedNumbers: [124], }; const good: SampleRun = { calls: [ @@ -158,6 +159,7 @@ it("passes a good run and names every rule a bad run breaks", () => { "textMust /t3-thread://v2//", "textMustNot /Paste .*token/", "linked ~/pull/124", + "linkedNumbers", ]); }); diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts index 7cad56ae98048..b58dee92081bc 100644 --- a/evals/mcp-tool-use/run.ts +++ b/evals/mcp-tool-use/run.ts @@ -35,7 +35,12 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { taskPrompt } from "../../apps/server/src/mcp/delegatedTaskModes.ts"; import * as McpInvocationContext from "../../apps/server/src/mcp/McpInvocationContext.ts"; import { advertisedJsonSchema } from "../../apps/server/src/mcp/advertisedJsonSchema.ts"; -import { EnvironmentId, ORCHESTRATOR_MCP_RUNNING_TASK_NEXT, ThreadId } from "@t3tools/contracts"; +import { + EnvironmentId, + ORCHESTRATOR_MCP_RUNNING_TASK_NEXT, + PullRequestState, + ThreadId, +} from "@t3tools/contracts"; import { projectComposerContextForProvider } from "@t3tools/shared/composerContextReferences"; import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { buildCodexAdditionalContext } from "../../apps/server/src/provider/CodexDeveloperInstructions.ts"; @@ -166,6 +171,8 @@ const McpToolUseExpect = Schema.Struct({ textMustNot: Schema.optional(Schema.Array(Schema.String)), /** Each entry matches a PR URL linked at the end, as `args` values match. */ linked: Schema.optional(Schema.Array(Schema.String)), + /** The PR numbers linked at the end, exactly. */ + linkedNumbers: Schema.optional(Schema.Array(Schema.Number)), /** Device manager checks: child briefs, simulator UI over the shell, failures, recordings. */ device: Schema.optional(DeviceExpect), }); @@ -210,6 +217,12 @@ const McpToolUseWorld = Schema.StructWithRest( deviceInvalidCapture: Schema.optional(Schema.Boolean), /** Devices another thread's agent-device session holds, by device id: that thread's id. */ deviceOwners: Schema.optional(Schema.Record(Schema.String, Schema.String)), + /** PRs already linked to the thread, with the state T3 last saw. */ + linkedPullRequests: Schema.optional( + Schema.Array( + Schema.Struct({ number: Schema.Number, state: PullRequestState, title: Schema.String }), + ), + ), }), Rest, ); @@ -731,11 +744,16 @@ export const makeSample = (testCase: McpToolUseCase, base: Base): Sample => { const prior = testCase.setup.priorTurns ?? []; const priorCalls = prior.flatMap((turn) => turn.toolCalls ?? []); const repo = testCase.setup.world?.repo ?? "acme/app"; - const urls = priorCalls.flatMap((call) => - ["link_pull_request", "watch_pull_request"].includes(call.tool) && isRecord(call.input) - ? [pullRequest(repo, call.input).url] - : [], - ); + const urls = [ + ...(testCase.setup.world?.linkedPullRequests ?? []).map( + ({ number }) => pullRequest(repo, { number }).url, + ), + ...priorCalls.flatMap((call) => + ["link_pull_request", "watch_pull_request"].includes(call.tool) && isRecord(call.input) + ? [pullRequest(repo, call.input).url] + : [], + ), + ]; const taskNumbers = [...valueText(prior).matchAll(/task-(\d+)/g)].map((match) => Number(match[1]), ); @@ -952,6 +970,15 @@ const pullRequest = (repo: string, input: Readonly>) => }; }; +/** A linked PR as `list_thread_pull_requests` shows it. Links made in the sample are open. */ +const linkedEntry = (sample: Sample, url: string) => { + const pr = pullRequest(sample.repo, { url }); + const seeded = sample.testCase.setup.world?.linkedPullRequests?.find( + ({ number }) => number === pr.number && pr.repository === sample.repo, + ); + return { ...pr, state: seeded?.state ?? "open", title: seeded?.title ?? null }; +}; + /** A believable T3 result for a tool the case does not mock. */ const t3Default = (sample: Sample, name: string, input: Readonly>) => { const pr = () => pullRequest(sample.repo, input); @@ -1006,6 +1033,24 @@ const t3Default = (sample: Sample, name: string, input: Readonly linkedEntry(sample, url)) + .filter((entry) => states.includes(entry.state)); + for (const { url } of unlinked) sample.linked.delete(url); + const groups = Map.groupBy(unlinked, (entry) => `${entry.repository}/${entry.state}`); + return { + unlinked: [...groups.values()].map((entries) => ({ + host: "github.com", + repository: entries[0]!.repository, + state: entries[0]!.state, + numbers: entries.map((entry) => entry.number), + })), + unlinkedCount: unlinked.length, + remaining: sample.linked.size, + }; + } case "watch_pull_request": sample.linked.add(pr().url); return { ...pr(), watching: true, wasWatching: false }; @@ -1014,10 +1059,9 @@ const t3Default = (sample: Sample, name: string, input: Readonly ({ - ...pullRequest(sample.repo, { url }), + ...linkedEntry(sample, url), source: "agent", watching: false, - state: "open", })), chains: [], }; @@ -1403,6 +1447,11 @@ export const gradeSample = (expect: McpToolUseCase["expect"], run: SampleRun) => for (const p of expect.textMustNot ?? []) add(`textMustNot /${p}/`, !test(p, run.text)); const isLinked = (url: string) => (run.linked ?? []).some((at) => matches(url, at)); for (const url of expect.linked ?? []) add(`linked ${url}`, isLinked(url)); + if (expect.linkedNumbers !== undefined) { + const numbers = (run.linked ?? []).map((url) => Number(/(\d+)\/?$/.exec(url)?.[1])); + const exact = valueText(numbers.toSorted((a, b) => a - b)); + add("linkedNumbers", exact === valueText(expect.linkedNumbers.toSorted((a, b) => a - b))); + } checks.push(...gradeDevice(expect.device, run)); return checks; }; diff --git a/packages/client-runtime/src/t3ToolSummary.test.ts b/packages/client-runtime/src/t3ToolSummary.test.ts index 0a1b3c4d793f4..a96d2256d0053 100644 --- a/packages/client-runtime/src/t3ToolSummary.test.ts +++ b/packages/client-runtime/src/t3ToolSummary.test.ts @@ -35,6 +35,15 @@ describe("summarizeT3ToolCalls", () => { ).toBe("Sent 2 messages to 1 thread"); }); + it("counts the pull requests a batch unlink removed", () => { + const batch = completed({ states: ["merged"] }, { unlinkedCount: 2 }); + expect(summarizeT3ToolCalls("unlink-pr", [batch, completed({ number: 3 })]).label).toBe( + "Unlinked 3 pull requests", + ); + const uncounted = completed({ states: ["merged"] }, { isError: false }); + expect(summarizeT3ToolCalls("unlink-pr", [uncounted]).label).toBe("Unlinked pull requests"); + }); + it("excludes failed and unfinished sends even if the provider reports completed", () => { const calls: T3ToolSummaryCall[] = [ completed({ threadId: "success" }, { messageId: "ok", threadId: "success" }), diff --git a/packages/client-runtime/src/t3ToolSummary.ts b/packages/client-runtime/src/t3ToolSummary.ts index 1e9629d5803aa..854a8583e95e3 100644 --- a/packages/client-runtime/src/t3ToolSummary.ts +++ b/packages/client-runtime/src/t3ToolSummary.ts @@ -391,9 +391,19 @@ export function summarizeT3ToolCalls( case "link-pr": label = phrase("Linked", "link", quantity(selected.length, "pull request")); break; - case "unlink-pr": - label = phrase("Unlinked", "unlink", quantity(selected.length, "pull request")); + case "unlink-pr": { + let count = 0; + let countKnown = true; + for (const call of selected) { + if (typeof call.output?.unlinkedCount === "number") count += call.output.unlinkedCount; + // A batch result too large to compact loses its count, so the label drops the number. + else if (Array.isArray(call.input?.states)) countKnown = false; + else count += 1; + } + const object = countKnown ? quantity(count, "pull request") : "pull requests"; + label = phrase("Unlinked", "unlink", object); break; + } case "watch-pr": label = phrase("Watching", "watch", quantity(selected.length, "pull request")); break; diff --git a/packages/shared/src/t3McpToolPresentation.ts b/packages/shared/src/t3McpToolPresentation.ts index 5848861f47ac4..fd3f1621bb036 100644 --- a/packages/shared/src/t3McpToolPresentation.ts +++ b/packages/shared/src/t3McpToolPresentation.ts @@ -99,6 +99,11 @@ const T3_MCP_TOOLS: Readonly> = { "unlink-pr", "pull-request", ), + unlink_pull_requests: tool( + ["Unlink", "Unlinking", "Unlinked", "pull requests by state"], + "unlink-pr", + "pull-request", + ), list_thread_pull_requests: tool( ["Check", "Checking", "Checked", "linked pull requests"], "list-prs", diff --git a/packages/shared/src/toolOutput.test.ts b/packages/shared/src/toolOutput.test.ts index 405905bf2fad9..e54c9408cde3d 100644 --- a/packages/shared/src/toolOutput.test.ts +++ b/packages/shared/src/toolOutput.test.ts @@ -29,11 +29,14 @@ describe("compactDynamicToolOutput", () => { scheduledTaskId: "scheduled-1", status: "failed", summary: "A child failed; the tool itself succeeded.", + unlinked: [{ repository: "acme/app", state: "merged", numbers: [1] }], + unlinkedCount: 1, }; expect(compactDynamicToolOutput(output)).toEqual({ thread: { threadId: "thread-1" }, taskId: "task-1", scheduledTaskId: "scheduled-1", + unlinkedCount: 1, }); expect(output.thread.messages).toEqual(["Private message"]); }); diff --git a/packages/shared/src/toolOutput.ts b/packages/shared/src/toolOutput.ts index 2a80641d13b5e..8b07deee54da0 100644 --- a/packages/shared/src/toolOutput.ts +++ b/packages/shared/src/toolOutput.ts @@ -43,6 +43,7 @@ interface CompactToolOutput { [MCP_APP_OUTPUT_KEY]?: McpAppReference; thread?: { threadId: string }; threads?: Array<{ threadId?: string; status?: "rolled_back" }>; + unlinkedCount?: number; } function readResult(value: unknown, budget: ResultReadBudget, depth = 0): ResultEnvelope { @@ -123,6 +124,8 @@ export function compactDynamicToolOutput(value: unknown): CompactToolOutput | un if (id !== undefined) output[key] = id; } if (data.status === "rolled_back") output.status = "rolled_back"; + if (typeof data.unlinkedCount === "number" && Number.isSafeInteger(data.unlinkedCount)) + output.unlinkedCount = data.unlinkedCount; const htmlRender = readHtmlRenderReference(data.htmlRender); if (htmlRender !== undefined) output.htmlRender = htmlRender; const mcpApp = readMcpAppReference(data[MCP_APP_OUTPUT_KEY]); From 01bd0a270253e8045053b564669ff1908bf82205 Mon Sep 17 00:00:00 2001 From: maria Date: Sun, 1 Nov 2026 02:19:04 +0000 Subject: [PATCH 028/322] feat(mcp): link several PRs to a thread with one call (#27994) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- .../toolkits/pullRequests/handlers.test.ts | 22 +++++++++ .../src/mcp/toolkits/pullRequests/handlers.ts | 49 +++++++++++++++++++ .../src/mcp/toolkits/pullRequests/tools.ts | 39 ++++++++++++++- .../multi_turn/muse_transcript.ndjson | 12 ++--- .../muse_permission/muse_transcript.ndjson | 12 ++--- .../muse_transcript.ndjson | 6 +-- .../muse_transcript.ndjson | 6 +-- .../muse_workflow/muse_transcript.ndjson | 6 +-- .../fixtures/simple/muse_transcript.ndjson | 6 +-- .../subagent_v2/muse_transcript.ndjson | 6 +-- .../turn_interrupt/muse_transcript.ndjson | 6 +-- .../src/provider/agentContextBudget.json | 6 +-- evals/mcp-tool-use/cases/pull-requests.ndjson | 23 +++++---- .../mcp-tool-use/cases/visual-preview.ndjson | 14 +++--- evals/mcp-tool-use/run.ts | 32 +++++++++++- .../client-runtime/src/t3ToolSummary.test.ts | 10 +++- packages/client-runtime/src/t3ToolSummary.ts | 37 ++++++++------ .../src/server/runtimeInstructions.test.ts | 2 +- .../src/server/runtimeInstructions.ts | 2 +- packages/shared/src/t3McpToolPresentation.ts | 5 ++ packages/shared/src/toolOutput.test.ts | 2 + packages/shared/src/toolOutput.ts | 7 ++- 22 files changed, 239 insertions(+), 71 deletions(-) diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts index 29ccb92126730..52802bfbb10fd 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts @@ -517,6 +517,28 @@ describe("pull request toolkit handlers", () => { }), ); + it.effect("links several pull requests in one call", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + thread: makeThread([makeLink(1), { ...makeLink(2), source: "stack-dismissed" }]), + }); + const url = (number: number) => `https://github.com/t3tools/t3code/pull/${number}`; + const result = yield* harness.call("link_pull_requests", { + urls: [url(1), url(2), url(3), url(3), "https://example.com/not-a-pr"], + }); + expect(result).toEqual({ + linked: [{ host: "github.com", repository: "t3tools/t3code", numbers: [2, 3] }], + alreadyLinked: [{ host: "github.com", repository: "t3tools/t3code", numbers: [1] }], + linkedCount: 3, + invalidUrls: ["https://example.com/not-a-pr"], + }); + expect(yield* Ref.get(harness.commands)).toMatchObject([ + { type: "thread.pull-request.link", number: 2, source: "agent" }, + { type: "thread.pull-request.link", number: 3, source: "agent" }, + ]); + }), + ); + it.effect("unlinks every visible pull request in the given states in one call", () => Effect.gen(function* () { const withState = (number: number, state: "open" | "closed" | "merged") => { diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts index 0c06ed3ebaf11..581dc6708dc22 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts @@ -392,6 +392,55 @@ const make = Effect.gen(function* () { return { ...target, alreadyLinked, ...next }; }), ), + link_pull_requests: writesThread((input) => + Effect.gen(function* () { + const thread = yield* requireThread(PullRequestLinkFailedError, input.threadId); + const targets: Array = []; + const invalidUrls: Array = []; + for (const url of input.urls) { + const target = yield* resolveTarget({ url }, undefined).pipe(Effect.option); + if (Option.isNone(target)) invalidUrls.push(url); + else if (!targets.some((seen) => threadPullRequestKeysEqual(seen, target.value))) + targets.push(target.value); + } + const links = threadPullRequestsOf(thread); + const isLinked = (target: ResolvedTarget) => + links.some( + (link) => link.source !== "stack-dismissed" && threadPullRequestKeysEqual(link, target), + ); + const fresh = targets.filter((target) => !isLinked(target)); + const alreadyLinked = targets.filter(isLinked); + yield* Effect.forEach(fresh, (target) => + Effect.gen(function* () { + yield* engine.dispatch({ + type: "thread.pull-request.link", + commandId: yield* commandId("mcp-pr-link", thread.id), + threadId: thread.id, + host: target.host, + repository: target.repository, + number: target.number, + url: target.url, + source: "agent", + }); + }).pipe(Effect.catchCause(dispatchFailure(PullRequestLinkFailedError))), + ); + const byRepository = (prs: ReadonlyArray) => + [...Map.groupBy(prs, (pr) => `${pr.host}/${pr.repository}`).values()].map((group) => ({ + host: group[0]!.host, + repository: group[0]!.repository, + numbers: group.map((pr) => pr.number), + })); + return { + linked: byRepository(fresh), + alreadyLinked: byRepository(alreadyLinked), + linkedCount: targets.length, + ...(invalidUrls.length > 0 ? { invalidUrls } : {}), + ...(thread.lineage.relationshipToParent === "subagent" + ? { next: CHILD_PULL_REQUEST_REPORT_NEXT } + : {}), + }; + }), + ), unlink_pull_request: writesThread((input) => Effect.gen(function* () { const thread = yield* requireThread(PullRequestUnlinkFailedError, input.threadId); diff --git a/apps/server/src/mcp/toolkits/pullRequests/tools.ts b/apps/server/src/mcp/toolkits/pullRequests/tools.ts index d0cdf31066d7c..789e9fe125235 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/tools.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/tools.ts @@ -223,6 +223,23 @@ export const LinkPullRequestResult = Schema.Struct({ }); export type LinkPullRequestResult = typeof LinkPullRequestResult.Type; +const PullRequestNumbersByRepository = Schema.Array( + Schema.Struct({ + host: Schema.String, + repository: Schema.String, + numbers: Schema.Array(Schema.Int), + }), +); + +export const LinkPullRequestsResult = Schema.Struct({ + linked: PullRequestNumbersByRepository, + alreadyLinked: PullRequestNumbersByRepository, + linkedCount: Schema.Int, + invalidUrls: Schema.optional(Schema.Array(Schema.String)), + next: Schema.optional(Schema.String), +}); +export type LinkPullRequestsResult = typeof LinkPullRequestsResult.Type; + export const UnlinkPullRequestResult = Schema.Struct({ host: Schema.String, repository: Schema.String, @@ -285,7 +302,8 @@ export const ListThreadPullRequestsResult = Schema.Struct({ export type ListThreadPullRequestsResult = typeof ListThreadPullRequestsResult.Type; const LinkPullRequestTool = Tool.make("link_pull_request", { - description: "Link a PR to this thread. T3 shows its status and settles the thread on merge.", + description: + "Link a PR to this thread. T3 shows its status and settles the thread on merge. For several PRs, use `link_pull_requests`.", parameters: PullRequestTargetInput, success: LinkPullRequestResult, failure: PullRequestToolError, @@ -297,6 +315,24 @@ const LinkPullRequestTool = Tool.make("link_pull_request", { .annotate(Tool.Idempotent, true) .annotate(Tool.OpenWorld, false); +const LinkPullRequestsTool = Tool.make("link_pull_requests", { + description: + "Link several PRs to a thread in one call. Use it instead of one `link_pull_request` per PR. URLs that are not PRs come back in `invalidUrls`.", + parameters: Schema.Struct({ + environmentId: OrchestratorMcpEnvironmentTarget, + threadId: Schema.optional(ThreadId.annotate({ description: "Thread. Default: this thread." })), + urls: Schema.Array(TrimmedNonEmptyString).check(Schema.isMinLength(1)), + }), + success: LinkPullRequestsResult, + failure: PullRequestToolError, + dependencies, +}) + .annotate(Tool.Title, "Link pull requests to thread") + .annotate(Tool.Readonly, false) + .annotate(Tool.Destructive, false) + .annotate(Tool.Idempotent, true) + .annotate(Tool.OpenWorld, false); + const UnlinkPullRequestTool = Tool.make("unlink_pull_request", { description: "Remove a PR link from this thread. Succeeds with `wasLinked=false` when it was not linked.", @@ -442,6 +478,7 @@ const ReadPullRequestGuideTool = Tool.make("read_pull_request_guide", { export const PullRequestsToolkit = Toolkit.make( LinkPullRequestTool, + LinkPullRequestsTool, UnlinkPullRequestTool, UnlinkPullRequestsTool, ListThreadPullRequestsTool, diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson index f9d3a0ede6cbf..f7d7d8ed08c11 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6b-707e-7000-9bb5-4079c7772ad0/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:30:11.238412Z","updatedAt":"2026-10-08T07:30:11.238416Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791444611408}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6b-707e-7000-9bb5-4079c7772ad0/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:30:11.238412Z","updatedAt":"2026-10-08T07:30:11.238416Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:1","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"37b19bd1-b212-4532-a0c9-a6677f303e26","sequence":5},"last":{"id":"37b19bd1-b212-4532-a0c9-a6677f303e26","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a11a6b-707e-7001-b2f4-23db74e60097","clientName":"t3_code"},"emittedAtMs":1791444611412}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with exactly: first fixture turn complete"}],"displayText":"Respond with exactly: first fixture turn complete","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with exactly: first fixture turn complete"}],"displayText":"Respond with exactly: first fixture turn complete","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:2","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"2c530748-943b-4467-8473-da8b04c511c3","sequence":16},"last":{"id":"2c530748-943b-4467-8473-da8b04c511c3","sequence":16}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444611661}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","status":"accepted","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"running","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:3"},"emittedAtMs":1791444611674}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:3","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18},"last":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18}},"turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"},"emittedAtMs":1791444611675}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18},"last":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18}},"item":{"itemId":"1b470279-ee8c-4c74-a298-f37184145584","kind":"userMessage","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":1,"status":"completed","recordedAt":"2026-10-08T07:30:11.66263Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: first fixture turn complete","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}},"emittedAtMs":1791444611675}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"31b500d6-40fc-4f4c-b097-6b7be8d77cee","sequence":20},"last":{"id":"31b500d6-40fc-4f4c-b097-6b7be8d77cee","sequence":20}},"item":{"itemId":"1b470279-ee8c-4c74-a298-f37184145584","kind":"userMessage","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:11.680942Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: first fixture turn complete","displayText":"Respond with exactly: first fixture turn complete","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}},"emittedAtMs":1791444611689}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18},"last":{"id":"2be0c9b5-9dc4-48f0-9b86-06aef4de31f3","sequence":18}},"item":{"itemId":"1b470279-ee8c-4c74-a298-f37184145584","kind":"userMessage","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":1,"status":"completed","recordedAt":"2026-10-08T07:30:11.66263Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: first fixture turn complete","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}},"emittedAtMs":1791444611675}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"31b500d6-40fc-4f4c-b097-6b7be8d77cee","sequence":20},"last":{"id":"31b500d6-40fc-4f4c-b097-6b7be8d77cee","sequence":20}},"item":{"itemId":"1b470279-ee8c-4c74-a298-f37184145584","kind":"userMessage","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:11.680942Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: first fixture turn complete","displayText":"Respond with exactly: first fixture turn complete","commandId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4"}},"emittedAtMs":1791444611689}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:6","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"077780ea-0dc9-4d7a-879c-ee368f3adbe0","sequence":37},"last":{"id":"077780ea-0dc9-4d7a-879c-ee368f3adbe0","sequence":37}},"item":{"itemId":"077780ea-0dc9-4d7a-879c-ee368f3adbe0","kind":"reminderChild","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:30:11.790301Z","fallbackText":"Reminder child session","childSessionId":"c13379db-6761-40b4-a974-b0f1a58717aa","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6b-72ac-7972-a1df-b343b7804b38"}},"emittedAtMs":1791444611795}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:6.1","item":{"itemId":"f99b90e1-9d8b-4bf2-8e7a-e240e1cf9321","kind":"agentMessage","turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","revision":1,"status":"inProgress","text":""}},"emittedAtMs":1791444618068}} {"type":"emit_inbound","label":"item/delta","frame":{"jsonrpc":"2.0","method":"item/delta","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:6.2","itemId":"f99b90e1-9d8b-4bf2-8e7a-e240e1cf9321","field":"text","delta":"first fixture turn complete"},"emittedAtMs":1791444618068}} @@ -29,12 +29,12 @@ {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"idle","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:16"},"emittedAtMs":1791444621046}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:16","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"688a69c5-e353-4db9-bd8c-1beb033902ba","sequence":79},"last":{"id":"688a69c5-e353-4db9-bd8c-1beb033902ba","sequence":79}},"turnId":"01a11a6b-722c-7000-a73a-1d49a3a7f4c4","terminal":"completed","durationMs":9378,"timeToFirstTokenMs":6406},"emittedAtMs":1791444621047}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:17","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"36753b8a-26ce-4935-8127-bc9191cb328b","sequence":80},"last":{"id":"36753b8a-26ce-4935-8127-bc9191cb328b","sequence":80}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444621054}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with exactly: second fixture turn complete"}],"displayText":"Respond with exactly: second fixture turn complete","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":4,"method":"turn/start","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with exactly: second fixture turn complete"}],"displayText":"Respond with exactly: second fixture turn complete","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":4,"result":{"commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","status":"accepted","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","status":"running","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:18"},"emittedAtMs":1791444621333}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:18","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87},"last":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87}},"turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"},"emittedAtMs":1791444621334}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:19","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87},"last":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87}},"item":{"itemId":"90719a5d-d2c1-46dc-ad4d-274adda02344","kind":"userMessage","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":1,"status":"completed","recordedAt":"2026-10-08T07:30:21.321391Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: second fixture turn complete","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}},"emittedAtMs":1791444621334}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:20","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"dc502e2c-d81c-4f0e-ad86-dd11d1368677","sequence":89},"last":{"id":"dc502e2c-d81c-4f0e-ad86-dd11d1368677","sequence":89}},"item":{"itemId":"90719a5d-d2c1-46dc-ad4d-274adda02344","kind":"userMessage","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:21.338944Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: second fixture turn complete","displayText":"Respond with exactly: second fixture turn complete","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}},"emittedAtMs":1791444621347}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:19","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87},"last":{"id":"4aba96d5-f984-4bc0-bda4-7fb97f527686","sequence":87}},"item":{"itemId":"90719a5d-d2c1-46dc-ad4d-274adda02344","kind":"userMessage","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":1,"status":"completed","recordedAt":"2026-10-08T07:30:21.321391Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: second fixture turn complete","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}},"emittedAtMs":1791444621334}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:20","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"dc502e2c-d81c-4f0e-ad86-dd11d1368677","sequence":89},"last":{"id":"dc502e2c-d81c-4f0e-ad86-dd11d1368677","sequence":89}},"item":{"itemId":"90719a5d-d2c1-46dc-ad4d-274adda02344","kind":"userMessage","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":2,"status":"completed","recordedAt":"2026-10-08T07:30:21.338944Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with exactly: second fixture turn complete","displayText":"Respond with exactly: second fixture turn complete","commandId":"01a11a6b-97ec-7000-bd1b-d7afbea11045"}},"emittedAtMs":1791444621347}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:21","sourceRange":{"stream":{"kind":"session","id":"01a11a6b-707e-7000-9bb5-4079c7772ad0"},"first":{"id":"09cb4c2a-03c2-4089-ba12-8d5d31f7ea48","sequence":106},"last":{"id":"09cb4c2a-03c2-4089-ba12-8d5d31f7ea48","sequence":106}},"item":{"itemId":"09cb4c2a-03c2-4089-ba12-8d5d31f7ea48","kind":"reminderChild","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:30:21.446966Z","fallbackText":"Reminder child session","childSessionId":"cd97261a-3ea9-480f-9e54-5e59bb077da2","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6b-9869-7061-a4f3-42daa3e3a8f1"}},"emittedAtMs":1791444621455}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:21.1","item":{"itemId":"2de26c0d-26b4-4533-8810-918ee44fcd82","kind":"agentMessage","turnId":"01a11a6b-97ec-7000-bd1b-d7afbea11045","revision":1,"status":"inProgress","text":""}},"emittedAtMs":1791444623127}} {"type":"emit_inbound","label":"item/delta","frame":{"jsonrpc":"2.0","method":"item/delta","params":{"sessionId":"01a11a6b-707e-7000-9bb5-4079c7772ad0","viewCursor":"v:01a11a6b-707e-7000-9bb5-4079c7772ad0:21.2","itemId":"2de26c0d-26b4-4533-8810-918ee44fcd82","field":"text","delta":"second fixture turn complete"},"emittedAtMs":1791444623127}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson index 0d6e3e282feb7..a744381b64632 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_permission/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6d-39bb-7000-8c93-4594c98e6a11/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:32:08.30084Z","updatedAt":"2026-10-08T07:32:08.300844Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"promptUnmatched","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791444728496}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6d-39bb-7000-8c93-4594c98e6a11/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:32:08.30084Z","updatedAt":"2026-10-08T07:32:08.300844Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"promptUnmatched","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:1","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"b05fb8ec-7b58-4cf7-a369-379938395230","sequence":5},"last":{"id":"b05fb8ec-7b58-4cf7-a369-379938395230","sequence":5}},"mode":"promptUnmatched","source":"approvalReconfigure","commandId":"01a11a6d-39bb-7001-923e-425cc6129ce7","clientName":"t3_code"},"emittedAtMs":1791444728501}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE."}],"displayText":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE."}],"displayText":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:2","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"6159e84a-ae1e-4fae-ae8b-49f41ca34509","sequence":16},"last":{"id":"6159e84a-ae1e-4fae-ae8b-49f41ca34509","sequence":16}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444728769}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","status":"accepted","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"running","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:3"},"emittedAtMs":1791444728783}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:3","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18},"last":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18}},"turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"},"emittedAtMs":1791444728783}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18},"last":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18}},"item":{"itemId":"dde9573b-650f-434b-ab99-60cfc820a0a5","kind":"userMessage","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:08.769894Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}},"emittedAtMs":1791444728784}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"76c57706-1378-4167-8bbf-f76d959804e8","sequence":20},"last":{"id":"76c57706-1378-4167-8bbf-f76d959804e8","sequence":20}},"item":{"itemId":"dde9573b-650f-434b-ab99-60cfc820a0a5","kind":"userMessage","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:08.791728Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","displayText":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}},"emittedAtMs":1791444728800}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18},"last":{"id":"09218c3a-88fd-4bc8-ab45-d1aa69bea95a","sequence":18}},"item":{"itemId":"dde9573b-650f-434b-ab99-60cfc820a0a5","kind":"userMessage","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:08.769894Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}},"emittedAtMs":1791444728784}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"76c57706-1378-4167-8bbf-f76d959804e8","sequence":20},"last":{"id":"76c57706-1378-4167-8bbf-f76d959804e8","sequence":20}},"item":{"itemId":"dde9573b-650f-434b-ab99-60cfc820a0a5","kind":"userMessage","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:08.791728Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","displayText":"Run the shell command `touch muse-approved.txt` with your shell tool, then reply DONE.","commandId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6"}},"emittedAtMs":1791444728800}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:6","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"29180105-84c4-4f9e-8bfd-003392b93b73","sequence":38},"last":{"id":"29180105-84c4-4f9e-8bfd-003392b93b73","sequence":38}},"item":{"itemId":"29180105-84c4-4f9e-8bfd-003392b93b73","kind":"reminderChild","turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:32:08.905989Z","fallbackText":"Reminder child session","childSessionId":"7c9485f9-26aa-4b56-8ca2-f36cb9cfb8c4","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6d-3c27-73f2-8c27-cd06a3b7dadf"}},"emittedAtMs":1791444728910}} {"type":"emit_inbound","label":"session/tokenUsage","frame":{"jsonrpc":"2.0","method":"session/tokenUsage","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:7","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"b1dab1f5-3c9e-4d92-b955-841a2269c23c","sequence":54},"last":{"id":"b1dab1f5-3c9e-4d92-b955-841a2269c23c","sequence":54}},"turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","modelId":"muse-spark-1.3-contributor","usage":{"inputTokens":32618,"outputTokens":315,"cachedTokens":0,"cacheWriteTokens":0,"cacheReadTokens":0,"reasoningTokens":227},"promptTokens":32618,"totalTokens":32933,"finishReason":"tool_calls","durationMs":2684,"cumulative":{"promptTokens":32618,"outputTokens":315,"totalTokens":32933,"cost":{"usd":0.0033248,"partial":false}}},"emittedAtMs":1791444731684}} {"type":"emit_inbound","label":"session/contextUsage","frame":{"jsonrpc":"2.0","method":"session/contextUsage","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:8","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"b1dab1f5-3c9e-4d92-b955-841a2269c23c","sequence":54},"last":{"id":"b1dab1f5-3c9e-4d92-b955-841a2269c23c","sequence":54}},"windowTokens":1007997,"usedTokens":32933,"pressure":"normal"},"emittedAtMs":1791444731684}} @@ -42,12 +42,12 @@ {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"idle","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:23"},"emittedAtMs":1791444747386}} {"type":"emit_inbound","label":"turn/completed","frame":{"jsonrpc":"2.0","method":"turn/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:23","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"7c29ac76-7e09-48c1-a18f-f336126e27d1","sequence":120},"last":{"id":"7c29ac76-7e09-48c1-a18f-f336126e27d1","sequence":120}},"turnId":"01a11a6d-3b9f-7000-b6ad-ab05e6b973b6","terminal":"completed","durationMs":18608,"timeToFirstTokenMs":4791},"emittedAtMs":1791444747387}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:24","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"1e4afe30-d924-444c-8665-5e763f524e70","sequence":121},"last":{"id":"1e4afe30-d924-444c-8665-5e763f524e70","sequence":121}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444747394}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":5,"method":"turn/start","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED."}],"displayText":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":5,"method":"turn/start","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED."}],"displayText":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":5,"result":{"commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29","status":"accepted","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","status":"running","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:25"},"emittedAtMs":1791444747660}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:25","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128},"last":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128}},"turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"},"emittedAtMs":1791444747660}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:26","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128},"last":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128}},"item":{"itemId":"27473708-71e0-4d3c-811c-a8bae6bbaa3b","kind":"userMessage","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:27.650913Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}},"emittedAtMs":1791444747660}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:27","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"4a056a03-469a-4c9f-94a9-78eb815023ad","sequence":130},"last":{"id":"4a056a03-469a-4c9f-94a9-78eb815023ad","sequence":130}},"item":{"itemId":"27473708-71e0-4d3c-811c-a8bae6bbaa3b","kind":"userMessage","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:27.66745Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","displayText":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}},"emittedAtMs":1791444747676}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:26","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128},"last":{"id":"4a1cf90d-7457-448d-a2b5-d82108636b3b","sequence":128}},"item":{"itemId":"27473708-71e0-4d3c-811c-a8bae6bbaa3b","kind":"userMessage","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:27.650913Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}},"emittedAtMs":1791444747660}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:27","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"4a056a03-469a-4c9f-94a9-78eb815023ad","sequence":130},"last":{"id":"4a056a03-469a-4c9f-94a9-78eb815023ad","sequence":130}},"item":{"itemId":"27473708-71e0-4d3c-811c-a8bae6bbaa3b","kind":"userMessage","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:27.66745Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRun the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","displayText":"Run the shell command `touch muse-declined.txt` with your shell tool once. If it is rejected, do not retry; reply REJECTED.","commandId":"01a11a6d-8566-7000-8ad1-6269ac187a29"}},"emittedAtMs":1791444747676}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:28","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"3b28955d-a87a-4acb-97e9-14cad87fd41f","sequence":148},"last":{"id":"3b28955d-a87a-4acb-97e9-14cad87fd41f","sequence":148}},"item":{"itemId":"3b28955d-a87a-4acb-97e9-14cad87fd41f","kind":"reminderChild","turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:32:27.787099Z","fallbackText":"Reminder child session","childSessionId":"efd21bf1-4fd1-49a2-9c55-91b7fc099765","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6d-85ed-7b91-8d91-c4ce76b6dfef"}},"emittedAtMs":1791444747794}} {"type":"emit_inbound","label":"session/tokenUsage","frame":{"jsonrpc":"2.0","method":"session/tokenUsage","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:29","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"b032c01d-eae1-437e-910c-8756aedc6084","sequence":164},"last":{"id":"b032c01d-eae1-437e-910c-8756aedc6084","sequence":164}},"turnId":"01a11a6d-8566-7000-8ad1-6269ac187a29","modelId":"muse-spark-1.3-contributor","usage":{"inputTokens":33401,"outputTokens":201,"cachedTokens":33009,"cacheWriteTokens":0,"cacheReadTokens":33009,"reasoningTokens":108},"promptTokens":33401,"totalTokens":33602,"finishReason":"tool_calls","durationMs":2522,"cumulative":{"promptTokens":99045,"outputTokens":538,"totalTokens":99583,"cacheReadTokens":65506,"cost":{"usd":0.003592512,"partial":false}}},"emittedAtMs":1791444750395}} {"type":"emit_inbound","label":"session/contextUsage","frame":{"jsonrpc":"2.0","method":"session/contextUsage","params":{"sessionId":"01a11a6d-39bb-7000-8c93-4594c98e6a11","viewCursor":"v:01a11a6d-39bb-7000-8c93-4594c98e6a11:30","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-39bb-7000-8c93-4594c98e6a11"},"first":{"id":"b032c01d-eae1-437e-910c-8756aedc6084","sequence":164},"last":{"id":"b032c01d-eae1-437e-910c-8756aedc6084","sequence":164}},"windowTokens":1007997,"usedTokens":33602,"pressure":"normal"},"emittedAtMs":1791444750395}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson index 1766b2ce67d8b..9d7c7e624126b 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_approval/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","path":"~/.local/share/muse/sessions/2026/10/11/01a12d1d-a1b7-7000-9697-d430c2e2afd8/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:37:59.14136Z","updatedAt":"2026-10-11T22:37:59.141364Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"promptUnmatched","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791758279294}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","path":"~/.local/share/muse/sessions/2026/10/11/01a12d1d-a1b7-7000-9697-d430c2e2afd8/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:37:59.14136Z","updatedAt":"2026-10-11T22:37:59.141364Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"promptUnmatched","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:1","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"ed95a9db-eada-4751-a7e6-22ccc9bdabd5","sequence":5},"last":{"id":"ed95a9db-eada-4751-a7e6-22ccc9bdabd5","sequence":5}},"mode":"promptUnmatched","source":"approvalReconfigure","commandId":"01a12d1d-a1b8-7000-8d6c-f1324b724cdb","clientName":"t3_code"},"emittedAtMs":1791758279297}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:2","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"4f170229-d428-4a64-9516-7a355ff3bcdb","sequence":15},"last":{"id":"4f170229-d428-4a64-9516-7a355ff3bcdb","sequence":15}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791758279531}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","status":"accepted","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","status":"running","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:3"},"emittedAtMs":1791758279541}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:3","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17},"last":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17}},"turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"},"emittedAtMs":1791758279542}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:4","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17},"last":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17}},"item":{"itemId":"a4b3de8d-842b-4065-8769-aed18e62e9fc","kind":"userMessage","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":1,"status":"completed","recordedAt":"2026-10-11T22:37:59.531477Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}},"emittedAtMs":1791758279542}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:5","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"5a63e348-5bf6-4262-b37f-d31d307ea6d8","sequence":19},"last":{"id":"5a63e348-5bf6-4262-b37f-d31d307ea6d8","sequence":19}},"item":{"itemId":"a4b3de8d-842b-4065-8769-aed18e62e9fc","kind":"userMessage","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":2,"status":"completed","recordedAt":"2026-10-11T22:37:59.545908Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","displayText":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}},"emittedAtMs":1791758279553}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:4","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17},"last":{"id":"429e8ecf-d532-4b14-a303-b9f832c36a51","sequence":17}},"item":{"itemId":"a4b3de8d-842b-4065-8769-aed18e62e9fc","kind":"userMessage","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":1,"status":"completed","recordedAt":"2026-10-11T22:37:59.531477Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}},"emittedAtMs":1791758279542}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:5","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"5a63e348-5bf6-4262-b37f-d31d307ea6d8","sequence":19},"last":{"id":"5a63e348-5bf6-4262-b37f-d31d307ea6d8","sequence":19}},"item":{"itemId":"a4b3de8d-842b-4065-8769-aed18e62e9fc","kind":"userMessage","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":2,"status":"completed","recordedAt":"2026-10-11T22:37:59.545908Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","displayText":"Do not run any commands yourself. Spawn one subagent whose only task is to run this exact shell command: printf 'subagent approval fixture' > subagent-approval.txt and then reply with exactly: Written. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d1d-a348-7000-ae97-9d27b5f80bce"}},"emittedAtMs":1791758279553}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:6","sourceRange":{"stream":{"kind":"session","id":"01a12d1d-a1b7-7000-9697-d430c2e2afd8"},"first":{"id":"d55286a9-a9e2-4d7b-be97-d44360bcca2b","sequence":37},"last":{"id":"d55286a9-a9e2-4d7b-be97-d44360bcca2b","sequence":37}},"item":{"itemId":"d55286a9-a9e2-4d7b-be97-d44360bcca2b","kind":"reminderChild","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":1,"status":"inProgress","recordedAt":"2026-10-11T22:37:59.660065Z","fallbackText":"Reminder child session","childSessionId":"0779d605-54b8-45dc-90b6-91dc58b9a52c","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a12d1d-a3d4-7712-8b98-26f3c4f9bf38"}},"emittedAtMs":1791758279666}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:6.1","item":{"itemId":"21b74b34-fe9f-457c-83f0-cf49257b5948","kind":"reasoning","turnId":"01a12d1d-a348-7000-ae97-9d27b5f80bce","revision":1,"status":"inProgress","summary":[""]}},"emittedAtMs":1791758282481}} {"type":"emit_inbound","label":"item/delta","frame":{"jsonrpc":"2.0","method":"item/delta","params":{"sessionId":"01a12d1d-a1b7-7000-9697-d430c2e2afd8","viewCursor":"v:01a12d1d-a1b7-7000-9697-d430c2e2afd8:6.2","itemId":"21b74b34-fe9f-457c-83f0-cf49257b5948","field":"summary.0","delta":"Spawning a subagent via muse_subagent_spawn to run a shell command and return its exact output, while avoiding direct command execution."},"emittedAtMs":1791758282481}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_interrupt/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_interrupt/muse_transcript.ndjson index 84d1a816f8af3..589ac53afc7b6 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_interrupt/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_subagent_interrupt/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","path":"~/.local/share/muse/sessions/2026/10/11/01a12d28-7788-7000-a946-c407f380868e/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:49:49.235205Z","updatedAt":"2026-10-11T22:49:49.235209Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791758989388}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","path":"~/.local/share/muse/sessions/2026/10/11/01a12d28-7788-7000-a946-c407f380868e/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:49:49.235205Z","updatedAt":"2026-10-11T22:49:49.235209Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:1","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"29d4e769-78f7-48fa-90cd-3ac9f8fa04bf","sequence":5},"last":{"id":"29d4e769-78f7-48fa-90cd-3ac9f8fa04bf","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a12d28-7788-7001-a223-c10e52187cdf","clientName":"t3_code"},"emittedAtMs":1791758989391}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:2","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"660e2cfd-318a-4d2d-87bb-0b1375d79fdf","sequence":15},"last":{"id":"660e2cfd-318a-4d2d-87bb-0b1375d79fdf","sequence":15}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791758989619}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a12d28-7912-7000-993a-afd019e6ac5c","status":"accepted","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","status":"running","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:3"},"emittedAtMs":1791758989632}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:3","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17},"last":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17}},"turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"},"emittedAtMs":1791758989632}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:4","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17},"last":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17}},"item":{"itemId":"5e234d9d-0744-4757-9629-761ea4e7f062","kind":"userMessage","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","revision":1,"status":"completed","recordedAt":"2026-10-11T22:49:49.620067Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}},"emittedAtMs":1791758989632}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:5","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"5724636e-9398-4d23-87ac-1e9a2b83ad9f","sequence":19},"last":{"id":"5724636e-9398-4d23-87ac-1e9a2b83ad9f","sequence":19}},"item":{"itemId":"5e234d9d-0744-4757-9629-761ea4e7f062","kind":"userMessage","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","revision":2,"status":"completed","recordedAt":"2026-10-11T22:49:49.635395Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","displayText":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}},"emittedAtMs":1791758989642}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:4","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17},"last":{"id":"5cb199bd-04e5-40d9-a299-8760be80e0a4","sequence":17}},"item":{"itemId":"5e234d9d-0744-4757-9629-761ea4e7f062","kind":"userMessage","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","revision":1,"status":"completed","recordedAt":"2026-10-11T22:49:49.620067Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}},"emittedAtMs":1791758989632}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:5","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"5724636e-9398-4d23-87ac-1e9a2b83ad9f","sequence":19},"last":{"id":"5724636e-9398-4d23-87ac-1e9a2b83ad9f","sequence":19}},"item":{"itemId":"5e234d9d-0744-4757-9629-761ea4e7f062","kind":"userMessage","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","revision":2,"status":"completed","recordedAt":"2026-10-11T22:49:49.635395Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","displayText":"Spawn one subagent whose only task is to run the shell command `sleep 30` and then reply with exactly: Done. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d28-7912-7000-993a-afd019e6ac5c"}},"emittedAtMs":1791758989642}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:6","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"44afb415-d6e4-4d51-9002-4deb196d463a","sequence":36},"last":{"id":"44afb415-d6e4-4d51-9002-4deb196d463a","sequence":36}},"item":{"itemId":"44afb415-d6e4-4d51-9002-4deb196d463a","kind":"reminderChild","turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","revision":1,"status":"inProgress","recordedAt":"2026-10-11T22:49:49.743418Z","fallbackText":"Reminder child session","childSessionId":"4dd3511e-2115-4fbb-a4c4-383cbb383d63","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a12d28-7997-79d0-8a69-8ac0acf92a56"}},"emittedAtMs":1791758989750}} {"type":"emit_inbound","label":"session/tokenUsage","frame":{"jsonrpc":"2.0","method":"session/tokenUsage","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:7","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"c94f5484-20b7-4057-a736-303e97bc097c","sequence":52},"last":{"id":"c94f5484-20b7-4057-a736-303e97bc097c","sequence":52}},"turnId":"01a12d28-7912-7000-993a-afd019e6ac5c","modelId":"muse-spark-1.3-contributor","usage":{"inputTokens":32911,"outputTokens":264,"cachedTokens":0,"cacheWriteTokens":0,"cacheReadTokens":0,"reasoningTokens":143},"promptTokens":32911,"totalTokens":33175,"finishReason":"tool_calls","durationMs":3651,"cumulative":{"promptTokens":32911,"outputTokens":264,"totalTokens":33175,"cost":{"usd":0.0033439000000000003,"partial":false}}},"emittedAtMs":1791758993485}} {"type":"emit_inbound","label":"session/contextUsage","frame":{"jsonrpc":"2.0","method":"session/contextUsage","params":{"sessionId":"01a12d28-7788-7000-a946-c407f380868e","viewCursor":"v:01a12d28-7788-7000-a946-c407f380868e:8","sourceRange":{"stream":{"kind":"session","id":"01a12d28-7788-7000-a946-c407f380868e"},"first":{"id":"c94f5484-20b7-4057-a736-303e97bc097c","sequence":52},"last":{"id":"c94f5484-20b7-4057-a736-303e97bc097c","sequence":52}},"windowTokens":1007997,"usedTokens":33175,"pressure":"normal"},"emittedAtMs":1791758993485}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson index 608135031ff09..b8616cdee6a16 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/muse_workflow/muse_transcript.ndjson @@ -8,13 +8,13 @@ {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","path":"~/.local/share/muse/sessions/2026/10/11/01a12d5d-405f-7000-89a3-5aa62af6ea23/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T23:47:28.587276Z","updatedAt":"2026-10-11T23:47:28.587281Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:2"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:1","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"0c13b14b-877c-4c5b-b05f-8ad250dfd381","sequence":5},"last":{"id":"0c13b14b-877c-4c5b-b05f-8ad250dfd381","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a12d5d-4060-7000-b6cd-aaabe26f80de","clientName":"t3_code"},"emittedAtMs":1791762448807}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:2","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"28b1f3f4-4448-4a21-a1a0-2df72ad1d326","sequence":6},"last":{"id":"28b1f3f4-4448-4a21-a1a0-2df72ad1d326","sequence":6}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791762448807}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it."}],"displayText":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it."}],"displayText":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:3","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"3f1880e1-81a0-4f21-b536-6b4fa5cf9d12","sequence":16},"last":{"id":"3f1880e1-81a0-4f21-b536-6b4fa5cf9d12","sequence":16}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791762449097}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","status":"accepted","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","status":"running","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:4"},"emittedAtMs":1791762449102}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:4","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18},"last":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18}},"turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"},"emittedAtMs":1791762449103}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:5","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18},"last":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18}},"item":{"itemId":"0c1ceda9-41f7-4c3e-990e-4c53873c7ef5","kind":"userMessage","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":1,"status":"completed","recordedAt":"2026-10-11T23:47:29.098117Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nUse your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}},"emittedAtMs":1791762449103}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:6","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"0411a5c8-2c8a-4c9e-bb67-25fa3cef21a3","sequence":20},"last":{"id":"0411a5c8-2c8a-4c9e-bb67-25fa3cef21a3","sequence":20}},"item":{"itemId":"0c1ceda9-41f7-4c3e-990e-4c53873c7ef5","kind":"userMessage","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":2,"status":"completed","recordedAt":"2026-10-11T23:47:29.107019Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nUse your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","displayText":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}},"emittedAtMs":1791762449131}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:5","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18},"last":{"id":"7a830825-f571-4781-8e4f-a4ef5e085bb7","sequence":18}},"item":{"itemId":"0c1ceda9-41f7-4c3e-990e-4c53873c7ef5","kind":"userMessage","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":1,"status":"completed","recordedAt":"2026-10-11T23:47:29.098117Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nUse your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}},"emittedAtMs":1791762449103}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:6","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"0411a5c8-2c8a-4c9e-bb67-25fa3cef21a3","sequence":20},"last":{"id":"0411a5c8-2c8a-4c9e-bb67-25fa3cef21a3","sequence":20}},"item":{"itemId":"0c1ceda9-41f7-4c3e-990e-4c53873c7ef5","kind":"userMessage","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":2,"status":"completed","recordedAt":"2026-10-11T23:47:29.107019Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nUse your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","displayText":"Use your workflow tool to launch a workflow with exactly TWO agents: one labeled 'ping' that replies with the word PING, and one labeled 'pong' that replies with the word PONG. Launch it and end your turn; do not wait for it.","commandId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5"}},"emittedAtMs":1791762449131}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:7","sourceRange":{"stream":{"kind":"session","id":"01a12d5d-405f-7000-89a3-5aa62af6ea23"},"first":{"id":"95574bfe-853f-44cc-81f6-88b05d6c8c16","sequence":38},"last":{"id":"95574bfe-853f-44cc-81f6-88b05d6c8c16","sequence":38}},"item":{"itemId":"95574bfe-853f-44cc-81f6-88b05d6c8c16","kind":"reminderChild","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":1,"status":"inProgress","recordedAt":"2026-10-11T23:47:29.328939Z","fallbackText":"Reminder child session","childSessionId":"b16dbb52-7f62-4a5a-84c2-f68d7ea22b18","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a12d5d-4375-7d72-b5b2-4b096d0df507"}},"emittedAtMs":1791762449336}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:7.1","item":{"itemId":"3ce724bc-dacf-400e-af9a-995104717cb1","kind":"reasoning","turnId":"01a12d5d-42c3-7000-ab94-c7f56ea457e5","revision":1,"status":"inProgress","summary":[""]}},"emittedAtMs":1791762451554}} {"type":"emit_inbound","label":"item/delta","frame":{"jsonrpc":"2.0","method":"item/delta","params":{"sessionId":"01a12d5d-405f-7000-89a3-5aa62af6ea23","viewCursor":"v:01a12d5d-405f-7000-89a3-5aa62af6ea23:7.2","itemId":"3ce724bc-dacf-400e-af9a-995104717cb1","field":"summary.0","delta":"Launching a two-agent ping-pong workflow via the workflow tool while skipping skill reading for a trivial task."},"emittedAtMs":1791762451554}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson index ad68f2bbf7574..1dc9f52719343 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6a-bbb5-7000-ad0b-0c9e728406e5/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:29:25.018803Z","updatedAt":"2026-10-08T07:29:25.018807Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791444565334}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6a-bbb5-7000-ad0b-0c9e728406e5/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:29:25.018803Z","updatedAt":"2026-10-08T07:29:25.018807Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:1","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"cfd043e8-8728-493d-ba28-252ef91e8a80","sequence":5},"last":{"id":"cfd043e8-8728-493d-ba28-252ef91e8a80","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a11a6a-bbb6-7000-b1a4-a01ea59676ef","clientName":"t3_code"},"emittedAtMs":1791444565340}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with the following text: fixture simple ok"}],"displayText":"Respond with the following text: fixture simple ok","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Respond with the following text: fixture simple ok"}],"displayText":"Respond with the following text: fixture simple ok","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:2","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"0f64f4ea-a08c-43e1-af53-cf85cee1a11c","sequence":16},"last":{"id":"0f64f4ea-a08c-43e1-af53-cf85cee1a11c","sequence":16}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444565612}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","status":"accepted","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","status":"running","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:3"},"emittedAtMs":1791444565635}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:3","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18},"last":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18}},"turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"},"emittedAtMs":1791444565635}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18},"last":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18}},"item":{"itemId":"23294b54-8e67-4896-a7f3-681119e1ddbe","kind":"userMessage","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":1,"status":"completed","recordedAt":"2026-10-08T07:29:25.627042Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with the following text: fixture simple ok","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}},"emittedAtMs":1791444565635}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"8c00aa21-7809-4cb2-89f7-b499aaa8746d","sequence":20},"last":{"id":"8c00aa21-7809-4cb2-89f7-b499aaa8746d","sequence":20}},"item":{"itemId":"23294b54-8e67-4896-a7f3-681119e1ddbe","kind":"userMessage","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":2,"status":"completed","recordedAt":"2026-10-08T07:29:25.643029Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with the following text: fixture simple ok","displayText":"Respond with the following text: fixture simple ok","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}},"emittedAtMs":1791444565652}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18},"last":{"id":"1f33a521-7829-4cc3-97db-a3b5a6e7deaf","sequence":18}},"item":{"itemId":"23294b54-8e67-4896-a7f3-681119e1ddbe","kind":"userMessage","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":1,"status":"completed","recordedAt":"2026-10-08T07:29:25.627042Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with the following text: fixture simple ok","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}},"emittedAtMs":1791444565635}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"8c00aa21-7809-4cb2-89f7-b499aaa8746d","sequence":20},"last":{"id":"8c00aa21-7809-4cb2-89f7-b499aaa8746d","sequence":20}},"item":{"itemId":"23294b54-8e67-4896-a7f3-681119e1ddbe","kind":"userMessage","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":2,"status":"completed","recordedAt":"2026-10-08T07:29:25.643029Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nRespond with the following text: fixture simple ok","displayText":"Respond with the following text: fixture simple ok","commandId":"01a11a6a-be41-7000-8db9-1d84c1b8be48"}},"emittedAtMs":1791444565652}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:6","sourceRange":{"stream":{"kind":"session","id":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5"},"first":{"id":"ed1680a5-6f99-4b0f-b9ea-d12193f0de1f","sequence":37},"last":{"id":"ed1680a5-6f99-4b0f-b9ea-d12193f0de1f","sequence":37}},"item":{"itemId":"ed1680a5-6f99-4b0f-b9ea-d12193f0de1f","kind":"reminderChild","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:29:25.781511Z","fallbackText":"Reminder child session","childSessionId":"9785bf9d-382d-4781-ab3b-3817c9e6a604","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6a-bef1-7032-974a-059b8765c487"}},"emittedAtMs":1791444565789}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:6.1","item":{"itemId":"0a68e576-f554-4700-8853-a68ad2b7179b","kind":"agentMessage","turnId":"01a11a6a-be41-7000-8db9-1d84c1b8be48","revision":1,"status":"inProgress","text":""}},"emittedAtMs":1791444567707}} {"type":"emit_inbound","label":"item/delta","frame":{"jsonrpc":"2.0","method":"item/delta","params":{"sessionId":"01a11a6a-bbb5-7000-ad0b-0c9e728406e5","viewCursor":"v:01a11a6a-bbb5-7000-ad0b-0c9e728406e5:6.2","itemId":"0a68e576-f554-4700-8853-a68ad2b7179b","field":"text","delta":"fixture simple ok"},"emittedAtMs":1791444567707}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson index be30486990816..aac7fd36214a7 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/muse_transcript.ndjson @@ -7,13 +7,13 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","path":"~/.local/share/muse/sessions/2026/10/11/01a12d17-75fa-7000-9157-e8365ce94492/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:31:14.73121Z","updatedAt":"2026-10-11T22:31:14.731214Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791757874894}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","path":"~/.local/share/muse/sessions/2026/10/11/01a12d17-75fa-7000-9157-e8365ce94492/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-11T22:31:14.73121Z","updatedAt":"2026-10-11T22:31:14.731214Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"sideFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:1","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"94d1e1ed-1d3e-4cd5-91bf-a51394fc8a0a","sequence":5},"last":{"id":"94d1e1ed-1d3e-4cd5-91bf-a51394fc8a0a","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a12d17-75fa-7001-a940-79b5d7c648a4","clientName":"t3_code"},"emittedAtMs":1791757874898}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said."}],"displayText":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:2","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"1dc98410-05c3-4618-a431-3638055e20df","sequence":15},"last":{"id":"1dc98410-05c3-4618-a431-3638055e20df","sequence":15}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791757875120}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a12d17-7790-7000-967d-3f2246f38a60","status":"accepted","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","startedNewTurn":true,"disposition":"started"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","status":"running","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:3"},"emittedAtMs":1791757875131}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:3","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17},"last":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17}},"turnId":"01a12d17-7790-7000-967d-3f2246f38a60","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"},"emittedAtMs":1791757875132}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:4","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17},"last":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17}},"item":{"itemId":"95c179c8-e8f8-4629-80df-e28243a234ea","kind":"userMessage","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":1,"status":"completed","recordedAt":"2026-10-11T22:31:15.120499Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}},"emittedAtMs":1791757875132}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:5","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"dcf03486-5149-4377-b325-dea3e118628a","sequence":19},"last":{"id":"dcf03486-5149-4377-b325-dea3e118628a","sequence":19}},"item":{"itemId":"95c179c8-e8f8-4629-80df-e28243a234ea","kind":"userMessage","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":2,"status":"completed","recordedAt":"2026-10-11T22:31:15.136047Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","displayText":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}},"emittedAtMs":1791757875144}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:4","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17},"last":{"id":"766cb840-a6f1-41f2-8c7e-8c372d0470cd","sequence":17}},"item":{"itemId":"95c179c8-e8f8-4629-80df-e28243a234ea","kind":"userMessage","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":1,"status":"completed","recordedAt":"2026-10-11T22:31:15.120499Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}},"emittedAtMs":1791757875132}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:5","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"dcf03486-5149-4377-b325-dea3e118628a","sequence":19},"last":{"id":"dcf03486-5149-4377-b325-dea3e118628a","sequence":19}},"item":{"itemId":"95c179c8-e8f8-4629-80df-e28243a234ea","kind":"userMessage","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":2,"status":"completed","recordedAt":"2026-10-11T22:31:15.136047Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nSpawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","displayText":"Spawn one subagent whose only task is to reply with exactly: Hello. Wait for it to finish, then reply with exactly what it said.","commandId":"01a12d17-7790-7000-967d-3f2246f38a60"}},"emittedAtMs":1791757875144}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:6","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"599a220a-d212-4a81-a60d-7284ff290284","sequence":36},"last":{"id":"599a220a-d212-4a81-a60d-7284ff290284","sequence":36}},"item":{"itemId":"599a220a-d212-4a81-a60d-7284ff290284","kind":"reminderChild","turnId":"01a12d17-7790-7000-967d-3f2246f38a60","revision":1,"status":"inProgress","recordedAt":"2026-10-11T22:31:15.249949Z","fallbackText":"Reminder child session","childSessionId":"0b3f4f01-850a-43bd-af36-9b011699354f","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a12d17-7819-7212-8057-3558c2bdd46d"}},"emittedAtMs":1791757875258}} {"type":"emit_inbound","label":"session/tokenUsage","frame":{"jsonrpc":"2.0","method":"session/tokenUsage","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:7","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"9198dd8d-eede-487a-9109-a48d2563c371","sequence":51},"last":{"id":"9198dd8d-eede-487a-9109-a48d2563c371","sequence":51}},"turnId":"01a12d17-7790-7000-967d-3f2246f38a60","modelId":"muse-spark-1.3-contributor","usage":{"inputTokens":32900,"outputTokens":129,"cachedTokens":0,"cacheWriteTokens":0,"cacheReadTokens":0,"reasoningTokens":36},"promptTokens":32900,"totalTokens":33029,"finishReason":"tool_calls","durationMs":2354,"cumulative":{"promptTokens":32900,"outputTokens":129,"totalTokens":33029,"cost":{"usd":0.0033158000000000003,"partial":false}}},"emittedAtMs":1791757877764}} {"type":"emit_inbound","label":"session/contextUsage","frame":{"jsonrpc":"2.0","method":"session/contextUsage","params":{"sessionId":"01a12d17-75fa-7000-9157-e8365ce94492","viewCursor":"v:01a12d17-75fa-7000-9157-e8365ce94492:8","sourceRange":{"stream":{"kind":"session","id":"01a12d17-75fa-7000-9157-e8365ce94492"},"first":{"id":"9198dd8d-eede-487a-9109-a48d2563c371","sequence":51},"last":{"id":"9198dd8d-eede-487a-9109-a48d2563c371","sequence":51}},"windowTokens":1007997,"usedTokens":33029,"pressure":"normal"},"emittedAtMs":1791757877764}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/muse_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/muse_transcript.ndjson index 0f5ffeaca508c..7557908979c6a 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/muse_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/muse_transcript.ndjson @@ -7,14 +7,14 @@ {"type":"emit_inbound","label":"session/started","frame":{"jsonrpc":"2.0","method":"session/started","params":{"session":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6d-2d3d-7000-8178-411fdb90bc34/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:32:05.094285Z","updatedAt":"2026-10-08T07:32:05.094289Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"}},"emittedAtMs":1791444725265}} {"type":"emit_inbound","label":"response:session/start","frame":{"jsonrpc":"2.0","id":2,"result":{"session":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","path":"~/.local/share/muse/sessions/2026/10/08/01a11a6d-2d3d-7000-8178-411fdb90bc34/session.jsonl","status":"idle","activeTurnId":null,"createdAt":"2026-10-08T07:32:05.094285Z","updatedAt":"2026-10-08T07:32:05.094289Z","workspaceRoot":"","providerId":"meta","modelId":"muse-spark-1.3-contributor","turnCount":0,"forkedFrom":null,"approvalMode":{"mode":"allowAll","source":"startup","lastCommandId":null},"createdByClient":{"name":"t3_code","version":"1"},"kind":"root"},"viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:1"}}} {"type":"emit_inbound","label":"session/approvalModeChanged","frame":{"jsonrpc":"2.0","method":"session/approvalModeChanged","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:1","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"9865268e-0a39-43a0-89d0-251a820ec8a3","sequence":5},"last":{"id":"9865268e-0a39-43a0-89d0-251a820ec8a3","sequence":5}},"mode":"allowAll","source":"approvalReconfigure","commandId":"01a11a6d-2d3e-7000-b0e7-f04c53ee070b","clientName":"t3_code"},"emittedAtMs":1791444725268}} -{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."}],"displayText":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}}} +{"type":"expect_outbound","label":"turn/start","frame":{"jsonrpc":"2.0","id":3,"method":"turn/start","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","input":[{"type":"text","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\n"},{"type":"text","text":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally."}],"displayText":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","workspaceRoots":[""],"ifBusy":"queue","reasoningEffort":"high","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}}} {"type":"emit_inbound","label":"session/branchChanged","frame":{"jsonrpc":"2.0","method":"session/branchChanged","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:2","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"531b0e9e-c96d-4fa5-a6dd-5cc7d4569543","sequence":16},"last":{"id":"531b0e9e-c96d-4fa5-a6dd-5cc7d4569543","sequence":16}},"workspaceRoot":"","branch":"main","vcs":"git"},"emittedAtMs":1791444725509}} {"type":"emit_inbound","label":"response:turn/start","frame":{"jsonrpc":"2.0","id":3,"result":{"commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","status":"accepted","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","startedNewTurn":true,"disposition":"started"}}} {"type":"expect_outbound","label":"turn/interrupt","frame":{"jsonrpc":"2.0","id":4,"method":"turn/interrupt","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","commandId":"01a11a6d-2f0b-7000-a1d4-359cf54e471a"}}} {"type":"emit_inbound","label":"session/statusChanged","frame":{"jsonrpc":"2.0","method":"session/statusChanged","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","status":"running","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:3"},"emittedAtMs":1791444725522}} {"type":"emit_inbound","label":"turn/started","frame":{"jsonrpc":"2.0","method":"turn/started","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:3","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18},"last":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18}},"turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"},"emittedAtMs":1791444725523}} -{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18},"last":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18}},"item":{"itemId":"cfa989ad-2359-4da2-9fca-562855cc0ae8","kind":"userMessage","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:05.510508Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}},"emittedAtMs":1791444725523}} -{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"5db75d92-0e52-4bce-bb81-40e254b40bc3","sequence":20},"last":{"id":"5db75d92-0e52-4bce-bb81-40e254b40bc3","sequence":20}},"item":{"itemId":"cfa989ad-2359-4da2-9fca-562855cc0ae8","kind":"userMessage","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:05.530992Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: one link_pull_request per PR, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","displayText":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}},"emittedAtMs":1791444725539}} +{"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:4","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18},"last":{"id":"50d1fd8c-232b-47b9-9bfa-9b543961bbc9","sequence":18}},"item":{"itemId":"cfa989ad-2359-4da2-9fca-562855cc0ae8","kind":"userMessage","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":1,"status":"completed","recordedAt":"2026-10-08T07:32:05.510508Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}},"emittedAtMs":1791444725523}} +{"type":"emit_inbound","label":"item/updated","frame":{"jsonrpc":"2.0","method":"item/updated","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:5","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"5db75d92-0e52-4bce-bb81-40e254b40bc3","sequence":20},"last":{"id":"5db75d92-0e52-4bce-bb81-40e254b40bc3","sequence":20}},"item":{"itemId":"cfa989ad-2359-4da2-9fca-562855cc0ae8","kind":"userMessage","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":2,"status":"completed","recordedAt":"2026-10-08T07:32:05.530992Z","text":"In case you're asked: you are running in T3 Code through the Muse Code harness, as muse-spark-1.3-contributor with high reasoning effort. No need to mention this otherwise. You can embed images, video, and audio in your response using Markdown image syntax with absolute file paths. Link any other file you produce using its absolute path so the user can open it in T3 Code's files viewer.\n\n\nLink each PR you create, take over, or push to: link_pull_request, or link_pull_requests for several, full URL, every stack layer, as your next call. Host tools do not link for you. If linking fails, say so. Skip PRs you only read or review, and PRs named only as background. Call watch_pull_request when asked to monitor or babysit, or when work you were asked to do, such as a merge, still waits on checks or reviews, even after an earlier unwatch. Finish any needed branch update and push first, then watch and end your turn. Otherwise act now or hand back. Delegated children never watch, even if the brief says babysit. After PR work, a child's last line is: PR , head , checks and reviews . Links and watches persist across pushes. For PR waits, no sleep, loops, gh --watch, Monitor or ScheduleWakeup. Before handback, call unwatch_pull_request on any PR you watch.\nDo not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","displayText":"Do not answer immediately. First run the local shell command `sleep 30`, then respond with exactly: interrupt fixture should not finish naturally.","commandId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}},"emittedAtMs":1791444725539}} {"type":"emit_inbound","label":"item/started","frame":{"jsonrpc":"2.0","method":"item/started","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:6","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"ae604001-22b9-479c-a45b-3dcfed45996c","sequence":37},"last":{"id":"ae604001-22b9-479c-a45b-3dcfed45996c","sequence":37}},"item":{"itemId":"ae604001-22b9-479c-a45b-3dcfed45996c","kind":"reminderChild","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":1,"status":"inProgress","recordedAt":"2026-10-08T07:32:05.638142Z","fallbackText":"Reminder child session","childSessionId":"1f58196a-6bf6-42da-9499-8f53a439e6e1","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6d-2f65-70b1-bb8f-69b33bb4fc62"}},"emittedAtMs":1791444725646}} {"type":"emit_inbound","label":"response:turn/interrupt","frame":{"jsonrpc":"2.0","id":4,"result":{"commandId":"01a11a6d-2f0b-7000-a1d4-359cf54e471a","status":"accepted","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5"}}} {"type":"emit_inbound","label":"item/completed","frame":{"jsonrpc":"2.0","method":"item/completed","params":{"sessionId":"01a11a6d-2d3d-7000-8178-411fdb90bc34","viewCursor":"v:01a11a6d-2d3d-7000-8178-411fdb90bc34:7","sourceRange":{"stream":{"kind":"session","id":"01a11a6d-2d3d-7000-8178-411fdb90bc34"},"first":{"id":"ae604001-22b9-479c-a45b-3dcfed45996c","sequence":37},"last":{"id":"3962d8d1-4608-4dda-8fdc-39a1804474aa","sequence":50}},"item":{"itemId":"ae604001-22b9-479c-a45b-3dcfed45996c","kind":"reminderChild","turnId":"01a11a6d-2ee0-7000-be19-f20eda7ce5d5","revision":2,"status":"cancelled","recordedAt":"2026-10-08T07:32:05.727236Z","fallbackText":"Reminder child session","failureReason":"main run cancelled","childSessionId":"1f58196a-6bf6-42da-9499-8f53a439e6e1","reminderAgentId":"skill-reminder","generationId":1,"taskId":"01a11a6d-2f65-70b1-bb8f-69b33bb4fc62"}},"emittedAtMs":1791444725734}} diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index c4ddc084c02b6..8f73039d971e7 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { - "claudeSystemAppendBytes": 5129, - "codexAdditionalContextBytes": 6859, - "mcpToolListJsonBytes": 46596 + "claudeSystemAppendBytes": 5153, + "codexAdditionalContextBytes": 6883, + "mcpToolListJsonBytes": 47020 } diff --git a/evals/mcp-tool-use/cases/pull-requests.ndjson b/evals/mcp-tool-use/cases/pull-requests.ndjson index 2f96d44cc7218..57ea8334c9df3 100644 --- a/evals/mcp-tool-use/cases/pull-requests.ndjson +++ b/evals/mcp-tool-use/cases/pull-requests.ndjson @@ -1,17 +1,17 @@ -{"id": "review-child-links-reviewed-pr", "area": "pull-requests", "kind": "bad", "pattern": "review-only child links the PR it reviews", "experience": "A read-only review child linked the PR it only reviewed, then called list_thread_pull_requests. Parent had already linked it.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. Your parent thread already linked https://github.com/acme/app/pull/123. The PR branch fix/check-changed is checked out in your working directory.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "delegateRole": "review", "world": {"branch": "fix/check-changed", "commits": [{"sha": "5b7e2d9", "subject": "feat(scripts): one command checks what the branch changed"}], "diff": "diff --git a/scripts/check-changed.ts b/scripts/check-changed.ts\nnew file mode 100644\n--- /dev/null\n+++ b/scripts/check-changed.ts\n@@ -0,0 +1,9 @@\n+import { execSync } from \"node:child_process\";\n+const base = process.argv[2] ?? \"origin/main\";\n+const files = execSync(`git diff --name-only ${base}`).toString().split(\"\\n\");\n+const ts = files.filter((f) => f.endsWith(\".ts\"));\n+if (ts.length > 0) execSync(`vp lint ${ts.join(\" \")}`, { stdio: \"inherit\" });\n+const diff = execSync(`git diff ${base}`).toString();\n+if (diff.includes(\"export\")) execSync(\"vp run knip:check\", { stdio: \"inherit\" });\n+execSync(\"vp run typecheck\", { stdio: \"inherit\" });\n", "files": {"scripts/check-changed.ts": "import { execSync } from \"node:child_process\";\nconst base = process.argv[2] ?? \"origin/main\";\nconst files = execSync(`git diff --name-only ${base}`).toString().split(\"\\n\");\nconst ts = files.filter((f) => f.endsWith(\".ts\"));\nif (ts.length > 0) execSync(`vp lint ${ts.join(\" \")}`, { stdio: \"inherit\" });\nconst diff = execSync(`git diff ${base}`).toString();\nif (diff.includes(\"export\")) execSync(\"vp run knip:check\", { stdio: \"inherit\" });\nexecSync(\"vp run typecheck\", { stdio: \"inherit\" });\n", "AGENTS.md": "Keep things simple. Run focused tests only.\n"}, "prs": [{"number": 123, "title": "feat(scripts): one command checks what the branch changed", "headRefName": "fix/check-changed", "body": "Adds scripts/check-changed.ts. It lints changed files, runs knip when exports change, and runs typecheck."}]}}, "prompt": "Review pull request https://github.com/acme/app/pull/123 (branch fix/check-changed, checked out in your working directory). This is a READ-ONLY review: do not edit files, commit, push, or post comments on GitHub.\n\nRead `gh pr view 123` and `git diff origin/main...HEAD`.\n\nReport:\n1. Overall verdict in one or two sentences.\n2. Real bugs, each with a failure scenario and file:line.\n3. Design concerns, ranked.\nKeep it short.", "expect": {"notCalls": ["link_pull_request", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "27 of 27 Codex review or scoring children did this. Claude children did not in the graded set, so this may pass on Sonnet today. Keep it as the main A/B target for the PR block rewrite."} -{"id": "scorer-child-links-scored-prs", "area": "pull-requests", "kind": "bad", "pattern": "scoring child links every PR it scores", "experience": "A scoring child linked all 9 PRs it only read and scored, so the child thread carried 9 PRs it never changed.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. No pull requests are linked to this thread.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": {"pullRequests": []}}, "delegateRole": "review", "world": {"files": {"/tmp/audit/SCORE.md": "# Score open pull requests\nFor each PR, read `gh pr view -R acme/app` and `gh pr diff -R acme/app`.\nScore value 1-5 and risk 1-5. Write one JSON line per PR to the output file:\n{\"pr\":n,\"value\":v,\"risk\":r,\"verdict\":\"MERGE|TAKEOVER|SKIP\",\"why\":\"one sentence\"}\nRead-only: never comment, review, label, or merge.\n"}, "prs": [{"number": 101, "title": "fix(web): small bug fix", "headRefName": "fix/small", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/session.ts b/src/session.ts\n--- a/src/session.ts\n+++ b/src/session.ts\n@@ -40,7 +40,9 @@ export function stopTask(task: Task) {\n- if (task.status !== \"running\") return;\n+ if (task.status === \"cancelled\") {\n+ emitTaskRow(task); // resend so stale clients catch up\n+ return;\n+ }\n cancel(task);\n"}, {"number": 102, "title": "feat(server): add a health endpoint", "headRefName": "feat/health", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/server/health.ts b/src/server/health.ts\n--- a/src/server/health.ts\n+++ b/src/server/health.ts\n@@ -1,0 +1,1 @@\n+export const health = () => ({ ok: true });\n"}, {"number": 103, "title": "chore: rename internal helpers", "headRefName": "chore/rename", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/util/names.ts b/src/util/names.ts\n--- a/src/util/names.ts\n+++ b/src/util/names.ts\n@@ -3,1 +3,1 @@\n-export function fmt(x: string) {\n+export function formatName(x: string) {\n"}]}}, "prompt": "Read /tmp/audit/SCORE.md and follow it exactly. Score each PR as it is now.\n\nPRs: 101 102 103\nOutput file: /tmp/audit/round5/scores-b.jsonl", "expect": {"notCalls": ["link_pull_request", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Same root cause as review-child-links-reviewed-pr. Over-linking also makes T3 poll more PRs."} -{"id": "review-round-child-links-pr", "area": "pull-requests", "kind": "bad", "pattern": "round-2 review child links the PR under review", "experience": "A round-2 review child was told 'do not watch it' but still linked the PR it reviewed and listed PRs at the end.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. Your parent thread already linked https://github.com/acme/app/pull/123. Branch fix/orphaned-stop is checked out in your working directory.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "delegateRole": "review", "world": {"branch": "fix/orphaned-stop", "commits": [{"sha": "abc1234", "subject": "fix(server): resend the task row when Stop hits a cancelled task"}, {"sha": "9d8e7f6", "subject": "fix(server): Stop always clears an orphaned delegate"}], "diff": "diff --git a/src/session.ts b/src/session.ts\n--- a/src/session.ts\n+++ b/src/session.ts\n@@ -40,7 +40,9 @@ export function stopTask(task: Task) {\n- if (task.status !== \"running\") return;\n+ if (task.status === \"cancelled\") {\n+ emitTaskRow(task); // resend so stale clients catch up\n+ return;\n+ }\n cancel(task);\n", "files": {"src/session.ts": "export function stopTask(task: Task) {\n if (task.status === \"cancelled\") {\n emitTaskRow(task); // resend so stale clients catch up\n return;\n }\n cancel(task);\n}\n"}, "prs": [{"number": 123, "title": "fix(server): Stop always clears an orphaned delegate", "headRefName": "fix/orphaned-stop"}]}}, "prompt": "You are an adversarial code reviewer, round 2. Review `git diff origin/main...HEAD` in your working directory (branch fix/orphaned-stop, PR https://github.com/acme/app/pull/123). The new commit since round 1 is HEAD (abc1234). Do NOT edit files, commit, push, comment on the PR, or watch it. Read-only.\n\n## Your round 1 findings\n1. P1: Stop does nothing when the task row is already cancelled but the client still shows it running.\n2. P2: The new helper writes the parent node twice.\n\n## Author response\nBoth fixed in abc1234.\n\nReport: VERDICT (ship or fix first), whether each round 1 finding is fixed, and any new bugs with file:line.", "expect": {"notCalls": ["link_pull_request", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Shows that 'do not watch' in the task does not stop the link. The 'work on' wording wins."} -{"id": "top-links-background-pr", "area": "pull-requests", "kind": "bad", "pattern": "links a background PR named in the task", "experience": "Asked to fix what PR #123 does not fix, the agent opened its own PR and linked it, then also linked #123, which it never changed.", "rule": "Do not link unrelated PRs mentioned only as background. / register every pull request you create or work on for this thread", "privacy": "commit-safe", "setup": {"role": "top", "context": "No pull requests are linked to this thread. You are on branch fix/reuse-socket, which has one commit (abc1234) with the fix. Nothing is pushed yet.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "world": {"branch": "fix/reuse-socket", "commits": [{"sha": "abc1234", "subject": "fix(web): keep a healthy socket on visibility change"}], "diff": "diff --git a/apps/web/src/socket.ts b/apps/web/src/socket.ts\n--- a/apps/web/src/socket.ts\n+++ b/apps/web/src/socket.ts\n@@ -1,6 +1,6 @@\n let socket: WebSocket | undefined;\n \n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return;\n- socket = openSocket();\n+ if (!socket || socket.readyState !== WebSocket.OPEN) socket = openSocket();\n });\n", "files": {"apps/web/src/socket.ts": "let socket: WebSocket | undefined;\n\ndocument.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return;\n if (!socket || socket.readyState !== WebSocket.OPEN) socket = openSocket();\n});\n"}, "prs": [{"number": 123, "title": "fix(ws): jittered reconnect backoff", "headRefName": "fix/ws-backoff", "diff": "diff --git a/apps/web/src/ws/backoff.ts b/apps/web/src/ws/backoff.ts\n--- a/apps/web/src/ws/backoff.ts\n+++ b/apps/web/src/ws/backoff.ts\n@@ -4,1 +4,1 @@\n- return base * 2 ** attempt;\n+ return base * 2 ** attempt * (0.5 + Math.random());\n"}]}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "# Fix the reconnect storms that #123 does not fix\n\n[#123](https://github.com/acme/app/pull/123) (open, branch fix/ws-backoff) adds jittered backoff. It fixes only about 2% of the storm connects. The rest come from the client opening a new socket on every visibility change while the old one is healthy.\n\nI committed the fix on branch fix/reuse-socket. Push it and open a separate PR. Mention #123 in the body and say how the two PRs relate.", "expect": {"maxCalls": {"link_pull_request": 1, "list_thread_pull_requests": 0}, "linked": ["~/pull/124"], "callsMustNot": [{"tool": "link_pull_request|watch_pull_request", "args": {"url": "~/pull/123"}}]}, "notes": "Claude Opus did this, so it is a good Sonnet reproduction candidate. Pre-fix era (Oct 3), same 'work on' text. Watch is not required (no babysit ask). Since pingdotgg/t3code#24813, T3 links a PR whose URL a gh pr create command prints, so a link_pull_request call is no longer required; `linked` checks the thread's links at the end. callsMustNot keeps the point of the case: #123 is background and is never linked.", "maxSteps": 8} +{"id": "review-child-links-reviewed-pr", "area": "pull-requests", "kind": "bad", "pattern": "review-only child links the PR it reviews", "experience": "A read-only review child linked the PR it only reviewed, then called list_thread_pull_requests. Parent had already linked it.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. Your parent thread already linked https://github.com/acme/app/pull/123. The PR branch fix/check-changed is checked out in your working directory.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "delegateRole": "review", "world": {"branch": "fix/check-changed", "commits": [{"sha": "5b7e2d9", "subject": "feat(scripts): one command checks what the branch changed"}], "diff": "diff --git a/scripts/check-changed.ts b/scripts/check-changed.ts\nnew file mode 100644\n--- /dev/null\n+++ b/scripts/check-changed.ts\n@@ -0,0 +1,9 @@\n+import { execSync } from \"node:child_process\";\n+const base = process.argv[2] ?? \"origin/main\";\n+const files = execSync(`git diff --name-only ${base}`).toString().split(\"\\n\");\n+const ts = files.filter((f) => f.endsWith(\".ts\"));\n+if (ts.length > 0) execSync(`vp lint ${ts.join(\" \")}`, { stdio: \"inherit\" });\n+const diff = execSync(`git diff ${base}`).toString();\n+if (diff.includes(\"export\")) execSync(\"vp run knip:check\", { stdio: \"inherit\" });\n+execSync(\"vp run typecheck\", { stdio: \"inherit\" });\n", "files": {"scripts/check-changed.ts": "import { execSync } from \"node:child_process\";\nconst base = process.argv[2] ?? \"origin/main\";\nconst files = execSync(`git diff --name-only ${base}`).toString().split(\"\\n\");\nconst ts = files.filter((f) => f.endsWith(\".ts\"));\nif (ts.length > 0) execSync(`vp lint ${ts.join(\" \")}`, { stdio: \"inherit\" });\nconst diff = execSync(`git diff ${base}`).toString();\nif (diff.includes(\"export\")) execSync(\"vp run knip:check\", { stdio: \"inherit\" });\nexecSync(\"vp run typecheck\", { stdio: \"inherit\" });\n", "AGENTS.md": "Keep things simple. Run focused tests only.\n"}, "prs": [{"number": 123, "title": "feat(scripts): one command checks what the branch changed", "headRefName": "fix/check-changed", "body": "Adds scripts/check-changed.ts. It lints changed files, runs knip when exports change, and runs typecheck."}]}}, "prompt": "Review pull request https://github.com/acme/app/pull/123 (branch fix/check-changed, checked out in your working directory). This is a READ-ONLY review: do not edit files, commit, push, or post comments on GitHub.\n\nRead `gh pr view 123` and `git diff origin/main...HEAD`.\n\nReport:\n1. Overall verdict in one or two sentences.\n2. Real bugs, each with a failure scenario and file:line.\n3. Design concerns, ranked.\nKeep it short.", "expect": {"notCalls": ["link_pull_request", "link_pull_requests", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "27 of 27 Codex review or scoring children did this. Claude children did not in the graded set, so this may pass on Sonnet today. Keep it as the main A/B target for the PR block rewrite."} +{"id": "scorer-child-links-scored-prs", "area": "pull-requests", "kind": "bad", "pattern": "scoring child links every PR it scores", "experience": "A scoring child linked all 9 PRs it only read and scored, so the child thread carried 9 PRs it never changed.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. No pull requests are linked to this thread.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": {"pullRequests": []}}, "delegateRole": "review", "world": {"files": {"/tmp/audit/SCORE.md": "# Score open pull requests\nFor each PR, read `gh pr view -R acme/app` and `gh pr diff -R acme/app`.\nScore value 1-5 and risk 1-5. Write one JSON line per PR to the output file:\n{\"pr\":n,\"value\":v,\"risk\":r,\"verdict\":\"MERGE|TAKEOVER|SKIP\",\"why\":\"one sentence\"}\nRead-only: never comment, review, label, or merge.\n"}, "prs": [{"number": 101, "title": "fix(web): small bug fix", "headRefName": "fix/small", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/session.ts b/src/session.ts\n--- a/src/session.ts\n+++ b/src/session.ts\n@@ -40,7 +40,9 @@ export function stopTask(task: Task) {\n- if (task.status !== \"running\") return;\n+ if (task.status === \"cancelled\") {\n+ emitTaskRow(task); // resend so stale clients catch up\n+ return;\n+ }\n cancel(task);\n"}, {"number": 102, "title": "feat(server): add a health endpoint", "headRefName": "feat/health", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/server/health.ts b/src/server/health.ts\n--- a/src/server/health.ts\n+++ b/src/server/health.ts\n@@ -1,0 +1,1 @@\n+export const health = () => ({ ok: true });\n"}, {"number": 103, "title": "chore: rename internal helpers", "headRefName": "chore/rename", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}], "diff": "diff --git a/src/util/names.ts b/src/util/names.ts\n--- a/src/util/names.ts\n+++ b/src/util/names.ts\n@@ -3,1 +3,1 @@\n-export function fmt(x: string) {\n+export function formatName(x: string) {\n"}]}}, "prompt": "Read /tmp/audit/SCORE.md and follow it exactly. Score each PR as it is now.\n\nPRs: 101 102 103\nOutput file: /tmp/audit/round5/scores-b.jsonl", "expect": {"notCalls": ["link_pull_request", "link_pull_requests", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Same root cause as review-child-links-reviewed-pr. Over-linking also makes T3 poll more PRs."} +{"id": "review-round-child-links-pr", "area": "pull-requests", "kind": "bad", "pattern": "round-2 review child links the PR under review", "experience": "A round-2 review child was told 'do not watch it' but still linked the PR it reviewed and listed PRs at the end.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task. Your parent thread already linked https://github.com/acme/app/pull/123. Branch fix/orphaned-stop is checked out in your working directory.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "delegateRole": "review", "world": {"branch": "fix/orphaned-stop", "commits": [{"sha": "abc1234", "subject": "fix(server): resend the task row when Stop hits a cancelled task"}, {"sha": "9d8e7f6", "subject": "fix(server): Stop always clears an orphaned delegate"}], "diff": "diff --git a/src/session.ts b/src/session.ts\n--- a/src/session.ts\n+++ b/src/session.ts\n@@ -40,7 +40,9 @@ export function stopTask(task: Task) {\n- if (task.status !== \"running\") return;\n+ if (task.status === \"cancelled\") {\n+ emitTaskRow(task); // resend so stale clients catch up\n+ return;\n+ }\n cancel(task);\n", "files": {"src/session.ts": "export function stopTask(task: Task) {\n if (task.status === \"cancelled\") {\n emitTaskRow(task); // resend so stale clients catch up\n return;\n }\n cancel(task);\n}\n"}, "prs": [{"number": 123, "title": "fix(server): Stop always clears an orphaned delegate", "headRefName": "fix/orphaned-stop"}]}}, "prompt": "You are an adversarial code reviewer, round 2. Review `git diff origin/main...HEAD` in your working directory (branch fix/orphaned-stop, PR https://github.com/acme/app/pull/123). The new commit since round 1 is HEAD (abc1234). Do NOT edit files, commit, push, comment on the PR, or watch it. Read-only.\n\n## Your round 1 findings\n1. P1: Stop does nothing when the task row is already cancelled but the client still shows it running.\n2. P2: The new helper writes the parent node twice.\n\n## Author response\nBoth fixed in abc1234.\n\nReport: VERDICT (ship or fix first), whether each round 1 finding is fixed, and any new bugs with file:line.", "expect": {"notCalls": ["link_pull_request", "link_pull_requests", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Shows that 'do not watch' in the task does not stop the link. The 'work on' wording wins."} +{"id": "top-links-background-pr", "area": "pull-requests", "kind": "bad", "pattern": "links a background PR named in the task", "experience": "Asked to fix what PR #123 does not fix, the agent opened its own PR and linked it, then also linked #123, which it never changed.", "rule": "Do not link unrelated PRs mentioned only as background. / register every pull request you create or work on for this thread", "privacy": "commit-safe", "setup": {"role": "top", "context": "No pull requests are linked to this thread. You are on branch fix/reuse-socket, which has one commit (abc1234) with the fix. Nothing is pushed yet.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": "default"}, "world": {"branch": "fix/reuse-socket", "commits": [{"sha": "abc1234", "subject": "fix(web): keep a healthy socket on visibility change"}], "diff": "diff --git a/apps/web/src/socket.ts b/apps/web/src/socket.ts\n--- a/apps/web/src/socket.ts\n+++ b/apps/web/src/socket.ts\n@@ -1,6 +1,6 @@\n let socket: WebSocket | undefined;\n \n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return;\n- socket = openSocket();\n+ if (!socket || socket.readyState !== WebSocket.OPEN) socket = openSocket();\n });\n", "files": {"apps/web/src/socket.ts": "let socket: WebSocket | undefined;\n\ndocument.addEventListener('visibilitychange', () => {\n if (document.visibilityState !== 'visible') return;\n if (!socket || socket.readyState !== WebSocket.OPEN) socket = openSocket();\n});\n"}, "prs": [{"number": 123, "title": "fix(ws): jittered reconnect backoff", "headRefName": "fix/ws-backoff", "diff": "diff --git a/apps/web/src/ws/backoff.ts b/apps/web/src/ws/backoff.ts\n--- a/apps/web/src/ws/backoff.ts\n+++ b/apps/web/src/ws/backoff.ts\n@@ -4,1 +4,1 @@\n- return base * 2 ** attempt;\n+ return base * 2 ** attempt * (0.5 + Math.random());\n"}]}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "# Fix the reconnect storms that #123 does not fix\n\n[#123](https://github.com/acme/app/pull/123) (open, branch fix/ws-backoff) adds jittered backoff. It fixes only about 2% of the storm connects. The rest come from the client opening a new socket on every visibility change while the old one is healthy.\n\nI committed the fix on branch fix/reuse-socket. Push it and open a separate PR. Mention #123 in the body and say how the two PRs relate.", "expect": {"maxCalls": {"link_pull_request": 1, "list_thread_pull_requests": 0, "link_pull_requests": 0}, "linked": ["~/pull/124"], "callsMustNot": [{"tool": "link_pull_request|watch_pull_request", "args": {"url": "~/pull/123"}}]}, "notes": "Claude Opus did this, so it is a good Sonnet reproduction candidate. Pre-fix era (Oct 3), same 'work on' text. Watch is not required (no babysit ask). Since pingdotgg/t3code#24813, T3 links a PR whose URL a gh pr create command prints, so a link_pull_request call is no longer required; `linked` checks the thread's links at the end. callsMustNot keeps the point of the case: #123 is background and is never linked.", "maxSteps": 8} {"id": "list-prs-first-no-pr-work", "area": "pull-requests", "kind": "bad", "pattern": "list_thread_pull_requests with no PR work", "experience": "On 'why is CI failing', the first T3 call was list_thread_pull_requests on a thread with no PRs. It returned an empty list.", "rule": "Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "top", "context": "No pull requests are linked to this thread.", "mocks": {"list_thread_pull_requests": {"pullRequests": [], "chains": []}}, "shellMocks": [{"match": "gh run list", "output": "completed\tfailure\tci\tmain\tpush\t9912345\t3m\t2026-10-24T08:40:00Z"}, {"match": "gh run view", "output": "Typecheck\tRun vp run typecheck\tsrc/timeline.test.tsx(49,45): error TS2307: Cannot find module 'mermaid' or its corresponding type declarations."}], "world": {"commits": [{"sha": "f9dd259", "subject": "test(web): warm up mermaid before first render"}, {"sha": "fcb20e4", "subject": "refactor(web): replace mermaid with a lazy renderer"}], "files": {"src/timeline.test.tsx": "import { warmMermaid } from 'mermaid';\n\nbeforeAll(() => warmMermaid());\n", "src/timeline/renderDiagram.ts": "export async function renderDiagram(code: string) {\n const { render } = await import('./lazyRenderer');\n return render(code);\n}\n"}}}, "prompt": "Why the hell is CI failing on main? Fix it right now.", "expect": {"notCalls": ["list_thread_pull_requests"]}, "notes": "Codex made this call in 139 runs with no PR work before the fixes. If the agent opens a PR, linking it is fine; only the list is graded."} -{"id": "list-after-linking-only-pr", "area": "pull-requests", "kind": "bad", "pattern": "list_thread_pull_requests right after linking the only PR", "experience": "Agent opened and linked a revert PR, merged it, then called list_thread_pull_requests to check the one PR it had just linked.", "rule": "Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "top", "context": "No pull requests are linked to this thread. Branch revert-120 has one commit that reverts #120. Nothing is pushed yet. The user has admin rights and allows an admin merge.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": {"pullRequests": [{"number": 124, "url": "https://github.com/acme/app/pull/124", "state": "merged"}]}}, "world": {"branch": "revert-120", "commits": [{"sha": "abc1234", "subject": "Revert \"refactor(web): move thread selector to the shared list\""}], "diff": "diff --git a/apps/web/src/ThreadSelector.tsx b/apps/web/src/ThreadSelector.tsx\n--- a/apps/web/src/ThreadSelector.tsx\n+++ b/apps/web/src/ThreadSelector.tsx\n@@ -10,1 +10,1 @@\n- return ;\n+ return ;\n", "prs": [{"number": 120, "title": "refactor(web): move thread selector to the shared list", "state": "MERGED", "headRefName": "refactor/selector"}]}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "#120 broke project grouping in the new thread selector. File a revert PR and merge it right now. Admin merge is fine.", "expect": {"notCalls": ["list_thread_pull_requests"], "maxCalls": {"link_pull_request": 1}}, "notes": "31 of 32 graded list calls added nothing. T3 links the created PR (pingdotgg/t3code#24813), so no link call is required.", "maxSteps": 7} -{"id": "relink-and-list-on-merge-ask", "area": "pull-requests", "kind": "bad", "pattern": "re-link and list on a later turn", "experience": "On 'Merge it now', the agent linked the already-linked PR again (alreadyLinked) and listed PRs after the merge.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Release builds failing. Fix. Get a PR up asap."}, {"role": "assistant", "text": "Opened [#123](https://github.com/acme/app/pull/123). It ignores the build output folder in the deploy upload, which caused both release failures.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh run view 9931 --log-failed | tail -5"}, "result": "upload\tError: payload too large: dist/ (412 MB) exceeds the 250 MB upload limit"}, {"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/scripts/deploy.ts", "old_string": " include: [\"**/*\"],", "new_string": " include: [\"**/*\"], exclude: [\"dist/**\"],"}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/scripts/deploy.ts has been updated successfully."}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(release): skip the build output folder in the deploy upload\""}, "result": "[fix/release-upload abc1234] fix(release): skip the build output folder in the deploy upload\n 1 file changed"}, {"tool": "Bash", "input": {"command": "git push -u origin fix/release-upload"}, "result": "To github.com:acme/app.git\n * [new branch] fix/release-upload -> fix/release-upload\nbranch 'fix/release-upload' set up to track 'origin/fix/release-upload'."}, {"tool": "Bash", "input": {"command": "gh pr create --base main --title \"fix(release): skip the build output folder in the deploy upload\" --fill"}, "result": "https://github.com/acme/app/pull/123"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"link_pull_request": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": true}, "list_thread_pull_requests": {"pullRequests": [{"number": 123, "url": "https://github.com/acme/app/pull/123", "state": "merged"}]}}, "world": {"branch": "fix/release-upload", "commits": [{"sha": "abc1234", "subject": "fix(release): skip the build output folder in the deploy upload"}], "diff": "diff --git a/scripts/deploy.ts b/scripts/deploy.ts\n--- a/scripts/deploy.ts\n+++ b/scripts/deploy.ts\n@@ -12,1 +12,1 @@\n- include: [\"**/*\"],\n+ include: [\"**/*\"], exclude: [\"dist/**\"],\n", "prs": [{"number": 123, "title": "fix(release): skip the build output folder in the deploy upload", "headRefName": "fix/release-upload", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}]}]}}, "prompt": "Merge it now", "expect": {"notCalls": ["link_pull_request", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Codex per-turn ceremony. Seen in 3 more threads (see source.also)."} +{"id": "list-after-linking-only-pr", "area": "pull-requests", "kind": "bad", "pattern": "list_thread_pull_requests right after linking the only PR", "experience": "Agent opened and linked a revert PR, merged it, then called list_thread_pull_requests to check the one PR it had just linked.", "rule": "Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "top", "context": "No pull requests are linked to this thread. Branch revert-120 has one commit that reverts #120. Nothing is pushed yet. The user has admin rights and allows an admin merge.", "mocks": {"link_pull_request": "default", "list_thread_pull_requests": {"pullRequests": [{"number": 124, "url": "https://github.com/acme/app/pull/124", "state": "merged"}]}}, "world": {"branch": "revert-120", "commits": [{"sha": "abc1234", "subject": "Revert \"refactor(web): move thread selector to the shared list\""}], "diff": "diff --git a/apps/web/src/ThreadSelector.tsx b/apps/web/src/ThreadSelector.tsx\n--- a/apps/web/src/ThreadSelector.tsx\n+++ b/apps/web/src/ThreadSelector.tsx\n@@ -10,1 +10,1 @@\n- return ;\n+ return ;\n", "prs": [{"number": 120, "title": "refactor(web): move thread selector to the shared list", "state": "MERGED", "headRefName": "refactor/selector"}]}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "#120 broke project grouping in the new thread selector. File a revert PR and merge it right now. Admin merge is fine.", "expect": {"notCalls": ["list_thread_pull_requests"], "maxCalls": {"link_pull_request": 1, "link_pull_requests": 0}}, "notes": "31 of 32 graded list calls added nothing. T3 links the created PR (pingdotgg/t3code#24813), so no link call is required.", "maxSteps": 7} +{"id": "relink-and-list-on-merge-ask", "area": "pull-requests", "kind": "bad", "pattern": "re-link and list on a later turn", "experience": "On 'Merge it now', the agent linked the already-linked PR again (alreadyLinked) and listed PRs after the merge.", "rule": "register every pull request you create or work on for this thread / Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Release builds failing. Fix. Get a PR up asap."}, {"role": "assistant", "text": "Opened [#123](https://github.com/acme/app/pull/123). It ignores the build output folder in the deploy upload, which caused both release failures.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh run view 9931 --log-failed | tail -5"}, "result": "upload\tError: payload too large: dist/ (412 MB) exceeds the 250 MB upload limit"}, {"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/scripts/deploy.ts", "old_string": " include: [\"**/*\"],", "new_string": " include: [\"**/*\"], exclude: [\"dist/**\"],"}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/scripts/deploy.ts has been updated successfully."}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(release): skip the build output folder in the deploy upload\""}, "result": "[fix/release-upload abc1234] fix(release): skip the build output folder in the deploy upload\n 1 file changed"}, {"tool": "Bash", "input": {"command": "git push -u origin fix/release-upload"}, "result": "To github.com:acme/app.git\n * [new branch] fix/release-upload -> fix/release-upload\nbranch 'fix/release-upload' set up to track 'origin/fix/release-upload'."}, {"tool": "Bash", "input": {"command": "gh pr create --base main --title \"fix(release): skip the build output folder in the deploy upload\" --fill"}, "result": "https://github.com/acme/app/pull/123"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"link_pull_request": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": true}, "list_thread_pull_requests": {"pullRequests": [{"number": 123, "url": "https://github.com/acme/app/pull/123", "state": "merged"}]}}, "world": {"branch": "fix/release-upload", "commits": [{"sha": "abc1234", "subject": "fix(release): skip the build output folder in the deploy upload"}], "diff": "diff --git a/scripts/deploy.ts b/scripts/deploy.ts\n--- a/scripts/deploy.ts\n+++ b/scripts/deploy.ts\n@@ -12,1 +12,1 @@\n- include: [\"**/*\"],\n+ include: [\"**/*\"], exclude: [\"dist/**\"],\n", "prs": [{"number": 123, "title": "fix(release): skip the build output folder in the deploy upload", "headRefName": "fix/release-upload", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Test", "state": "pass"}]}]}}, "prompt": "Merge it now", "expect": {"notCalls": ["link_pull_request", "link_pull_requests", "list_thread_pull_requests", "watch_pull_request"]}, "notes": "Codex per-turn ceremony. Seen in 3 more threads (see source.also)."} {"id": "pr-created-not-linked", "area": "pull-requests", "kind": "bad", "pattern": "PR created but never linked", "experience": "In a long non-PR conversation, the user asked if the changes were pushed. The agent pushed, opened a PR, and never linked it.", "rule": "Call link_pull_request with the full PR URL immediately after creating a PR", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Mount /tmp on disk, not RAM, and make swap at least 32 GB by default in the image builder."}, {"role": "assistant", "text": "Done on branch feat/disk-defaults (commit abc1234). /tmp is now on disk and swap defaults to 32 GB. The 11 builder tests pass. Nothing is pushed yet.", "toolCalls": [{"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/builder/defaults.py", "old_string": "TMP_ON_TMPFS = True\nSWAP_GB = 8", "new_string": "TMP_ON_TMPFS = False\nSWAP_GB = 32"}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/builder/defaults.py has been updated successfully."}, {"tool": "Bash", "input": {"command": "git checkout -b feat/disk-defaults"}, "result": "Switched to a new branch 'feat/disk-defaults'"}, {"tool": "Bash", "input": {"command": "python3 -m unittest discover builder"}, "result": "...........\n----------------------------------------------------------------------\nRan 11 tests in 0.4s\n\nOK"}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"feat(builder): mount /tmp on disk and default swap to 32 GB\""}, "result": "[feat/disk-defaults abc1234] feat(builder): mount /tmp on disk and default swap to 32 GB\n 1 file changed"}]}], "mocks": {"link_pull_request": "default"}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}, {"match": "python3 -m unittest|pytest", "output": "Ran 11 tests in 0.4s\n\nOK"}], "world": {"branch": "feat/disk-defaults", "commits": [{"sha": "abc1234", "subject": "feat(builder): mount /tmp on disk and default swap to 32 GB"}], "diff": "diff --git a/builder/defaults.py b/builder/defaults.py\n--- a/builder/defaults.py\n+++ b/builder/defaults.py\n@@ -10,2 +10,2 @@\n-TMP_ON_TMPFS = True\n-SWAP_GB = 8\n+TMP_ON_TMPFS = False\n+SWAP_GB = 32\n", "files": {"builder/defaults.py": "TMP_ON_TMPFS = False\nSWAP_GB = 32\n"}}}, "prompt": "Did you push up your changes? Open a PR so we can get this on main.", "expect": {"linked": ["~/pull/124"]}, "notes": "Rare (3 of 240 v2 PR threads). The real prompt did not say 'PR'; I added 'Open a PR' so the case is gradable. The real user then said 'No need to PR it'. Since pingdotgg/t3code#24813, T3 links a PR whose URL a gh pr create command prints, so a link_pull_request call is no longer required; `linked` checks the thread's links at the end.", "maxSteps": 10} {"id": "child-pr-not-linked", "area": "pull-requests", "kind": "bad", "pattern": "delegated child creates PR, never links it", "experience": "An implementation child opened its PR and did not link it, so the PR was not tracked on the child thread.", "rule": "Call link_pull_request with the full PR URL immediately after creating a PR / As a delegated child task, do not watch or poll PRs; return PR URLs, head SHA, and pending review/check state to your parent.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task in your own worktree. Branch t3/sql-lock-wait has one commit with the change. Nothing is pushed yet.", "mocks": {"link_pull_request": "default", "watch_pull_request": "default"}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}, {"match": "git (fetch|rebase|pull)", "output": "Successfully rebased and updated refs/heads/t3/sql-lock-wait."}], "delegateRole": "implementation", "world": {"branch": "t3/sql-lock-wait", "commits": [{"sha": "abc1234", "subject": "feat(server): show lock wait time on SQL spans"}], "diff": "diff --git a/apps/server/src/db/trace.ts b/apps/server/src/db/trace.ts\n--- a/apps/server/src/db/trace.ts\n+++ b/apps/server/src/db/trace.ts\n@@ -1,4 +1,5 @@\n export function traceQuery(span: Span, result: QueryResult) {\n span.setAttribute(\"db.rows\", result.rowCount);\n+ span.setAttribute(\"db.lock_wait_ms\", result.lockWaitMs);\n span.end();\n }\n", "files": {"apps/server/src/db/trace.ts": "export function traceQuery(span: Span, result: QueryResult) {\n span.setAttribute(\"db.rows\", result.rowCount);\n span.setAttribute(\"db.lock_wait_ms\", result.lockWaitMs);\n span.end();\n}\n"}}}, "prompt": "Goal: open ONE real (non-draft) PR in acme/app for the commit already on your branch (t3/sql-lock-wait). It makes SQL spans show lock wait time. Rebase onto latest origin/main first. Use a conventional commit title and plain English in the PR body. Report back when the PR is open.", "expect": {"notCalls": ["watch_pull_request"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "\\bfor \\w+ in [^\\n]*;\\s*do[^\\n]*\\bgh pr checks\\b", "pr-wait"], "textMust": ["github\\.com/acme/app/pull/124", "\\b[0-9a-f]{7,40}\\b"], "linked": ["~/pull/124"]}, "notes": "Also checks the child return contract: PR URL and head SHA in the final text, no watch, no poll. Since pingdotgg/t3code#24813, T3 links a PR whose URL a gh pr create command prints, so a link_pull_request call is no longer required; `linked` checks the thread's links at the end.", "maxSteps": 8} {"id": "child-babysit-calls-watch", "area": "pull-requests", "kind": "bad", "pattern": "delegated child calls watch_pull_request", "experience": "The parent's brief said take it to a merge-ready PR. The child linked its PR, then called watch_pull_request, which fails for subagents.", "rule": "As a delegated child task, do not watch or poll PRs; return PR URLs, head SHA, and pending review/check state to your parent.", "privacy": "commit-safe", "setup": {"role": "delegated-child", "context": "You are a delegated child task in your own worktree. Branch t3/connect-diagnostics has one commit with the change. Nothing is pushed yet.", "mocks": {"link_pull_request": "default", "watch_pull_request": "default"}, "delegateRole": "implementation", "world": {"branch": "t3/connect-diagnostics", "commits": [{"sha": "abc1234", "subject": "feat(web): show which step of a remote connection failed"}], "diff": "diff --git a/apps/web/src/connect/errors.ts b/apps/web/src/connect/errors.ts\n--- a/apps/web/src/connect/errors.ts\n+++ b/apps/web/src/connect/errors.ts\n@@ -1,5 +1,5 @@\n export type ConnectStep = 'dns' | 'tls' | 'auth' | 'socket';\n \n export function connectErrorMessage(step: ConnectStep) {\n- return 'Connection failed';\n+ return `Connection failed at the ${step} step`;\n }\n", "files": {"apps/web/src/connect/errors.ts": "export type ConnectStep = 'dns' | 'tls' | 'auth' | 'socket';\n\nexport function connectErrorMessage(step: ConnectStep) {\n return `Connection failed at the ${step} step`;\n}\n"}}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "Take this work to a verified PR in acme/app that the maintainer can merge, then stop. Do not merge. Babysit the PR until CI is green and every bot review comment is handled. Register the PR with `link_pull_request` when it exists.\n\nThe change is committed on your branch t3/connect-diagnostics. It shows which part of a remote connection failed. Push it and open the PR.", "expect": {"notCalls": ["watch_pull_request", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "\\bfor \\w+ in [^\\n]*;\\s*do[^\\n]*\\bgh pr checks\\b", "pr-wait"], "textMust": ["github\\.com/acme/app/pull/124", "\\b[0-9a-f]{7,40}\\b"]}, "notes": "The real brief said 'Use the full-stop skill', which says babysit. The case puts 'babysit' in the brief so it does not need the skill. Good: return URL, head SHA, and pending checks. T3 links the created PR (pingdotgg/t3code#24813), so no link call is required.", "maxSteps": 12} {"id": "babysit-uses-monitor", "area": "pull-requests", "kind": "bad", "pattern": "babysit with Monitor/ScheduleWakeup instead of watch_pull_request", "experience": "On 'file the PR and babysit it', the agent linked the PR and armed a Monitor script, re-arming it 4 times, with no watch_pull_request.", "rule": "when asked to monitor, watch, or babysit a PR and watch_pull_request is available, call it and end your turn ... do not poll or run your own watcher", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "cmd+enter no longer sends a new thread to the background. Fix it."}, {"role": "assistant", "text": "Fixed on branch fix/mod-enter-background (commit abc1234). The new-thread shortcut handler did not pass the background flag. The shortcut tests pass, including a new one for cmd+enter. Want me to open a PR?", "toolCalls": [{"tool": "Bash", "input": {"command": "rg -n \"createThread\" apps/web/src/shortcuts.ts"}, "result": "apps/web/src/shortcuts.ts:2: createThread({ text });"}, {"tool": "Bash", "input": {"command": "git checkout -b fix/mod-enter-background"}, "result": "Switched to a new branch 'fix/mod-enter-background'"}, {"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/shortcuts.ts", "old_string": " createThread({ text });", "new_string": " createThread({ text, background: event.metaKey });"}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/shortcuts.ts has been updated successfully."}, {"tool": "Bash", "input": {"command": "pnpm vitest run apps/web/src/shortcuts.test.ts"}, "result": " Test Files 1 passed (1)\n Tests 6 passed (6)"}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(web): cmd+enter sends a new thread to the background again\""}, "result": "[fix/mod-enter-background abc1234] fix(web): cmd+enter sends a new thread to the background again\n 1 file changed"}]}], "mocks": {"link_pull_request": "default", "watch_pull_request": "default"}, "world": {"branch": "fix/mod-enter-background", "commits": [{"sha": "abc1234", "subject": "fix(web): cmd+enter sends a new thread to the background again"}], "diff": "diff --git a/apps/web/src/shortcuts.ts b/apps/web/src/shortcuts.ts\n--- a/apps/web/src/shortcuts.ts\n+++ b/apps/web/src/shortcuts.ts\n@@ -3,4 +3,4 @@\n // Enter sends; cmd/ctrl+enter sends the new thread to the background.\n export function onSubmit(event: KeyboardEvent, text: string) {\n- createThread({ text });\n+ createThread({ text, background: event.metaKey || event.ctrlKey });\n }\n", "files": {"apps/web/src/shortcuts.ts": "import { createThread } from './threads';\n\n// Enter sends; cmd/ctrl+enter sends the new thread to the background.\nexport function onSubmit(event: KeyboardEvent, text: string) {\n createThread({ text, background: event.metaKey || event.ctrlKey });\n}\n", "apps/web/src/shortcuts.test.ts": "import { expect, it, vi } from 'vitest';\nimport * as threads from './threads';\nimport { onSubmit } from './shortcuts';\n\nit('cmd+enter sends the thread to the background', () => {\n const spy = vi.spyOn(threads, 'createThread').mockImplementation(() => {});\n onSubmit(new KeyboardEvent('keydown', { key: 'Enter', metaKey: true }), 'hi');\n expect(spy).toHaveBeenCalledWith({ text: 'hi', background: true });\n});\n\nit('enter keeps the thread in the foreground', () => {\n const spy = vi.spyOn(threads, 'createThread').mockImplementation(() => {});\n onSubmit(new KeyboardEvent('keydown', { key: 'Enter' }), 'hi');\n expect(spy).toHaveBeenCalledWith({ text: 'hi', background: false });\n});\n"}}, "shellMocks": [{"match": "gh pr create", "output": "https://github.com/acme/app/pull/124"}]}, "prompt": "oh I'm sure it's fine, just file the PR and babysit it already", "expect": {"calls": [{"tool": "watch_pull_request", "args": {"url": "~/pull/124"}}], "notCalls": ["Monitor", "ScheduleWakeup", "CronCreate"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "\\bfor \\w+ in [^\\n]*;\\s*do[^\\n]*\\bgh pr checks\\b", "pr-wait"], "endsTurnAfter": "watch_pull_request"}, "notes": "Oct 3, when the babysit-pr skill ran pr-wait in Monitor. 6 of 38 v2 babysit asks used Monitor or ScheduleWakeup. watch_pull_request links the PR, so a separate link call is optional.", "maxSteps": 16} {"id": "merge-when-ready-poll-loop", "area": "pull-requests", "kind": "bad", "pattern": "watch then own gh poll loop", "experience": "On 'merge when you think it's ready', the agent called watch_pull_request, then ran an until-loop on gh pr checks with sleep 20 in the same turn.", "rule": "when asked to monitor, watch, or babysit a PR and watch_pull_request is available, call it and end your turn ... do not poll or run your own watcher", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default"}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return