Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0056362
🤖 refactor: dedupe memory sweep recordUsage callbacks
mux-bot[bot] Jul 8, 2026
8871922
🤖 refactor: dedupe memory scope-full cap check into helper
mux-bot[bot] Jul 9, 2026
e58eed9
refactor: dedupe blockquote line formatting in bash monitor wake prompt
mux-bot[bot] Jul 9, 2026
0436616
refactor: dedupe tool_search removal in prepareToolSearch
mux-bot[bot] Jul 9, 2026
e931155
refactor: dedupe anthropic cache-create token extraction in usageHelpers
mux-bot[bot] Jul 10, 2026
fd83814
refactor: dedupe capability-model thinking policy resolution
mux-bot[bot] Jul 10, 2026
4e844af
refactor: dedupe queue entry clear-callback projection in MessageQueue
mux-bot[bot] Jul 11, 2026
3dedfc3
refactor: dedupe OpenAI-origin model check in cacheStrategy
mux-bot[bot] Jul 12, 2026
9e8368e
refactor: dedupe tool-call-execution-start emit in StreamManager
mux-bot[bot] Jul 13, 2026
ce9794d
refactor: dedupe model-parameter extras merge in aiService
mux-bot[bot] Jul 13, 2026
ca68b69
refactor: unify legacy tool_search part rename helper in toolCatalog
mux-bot[bot] Jul 14, 2026
3b9c8e5
refactor: drop duplicated context-cap rationale comment in codexOAuth
mux-bot[bot] Jul 14, 2026
eb75599
refactor: dedupe flat-section pinned block resolution in pinnedReorder
mux-bot[bot] Jul 15, 2026
4cf3524
refactor: dedupe JSON-wrapped tool-output unwrap in workflowRunMessages
mux-bot[bot] Jul 15, 2026
028f36f
refactor: hoist errorType local in finalizeWorkspaceTurnFromStreamError
mux-bot[bot] Jul 15, 2026
cbefdbe
refactor: extract buildSkillDescriptor helper for skill discovery
mux-bot[bot] Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions src/browser/utils/ui/pinnedReorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ function collectFlatSectionRows(
return orderMultiProjectSectionRows(Array.from(byId.values()));
}

/**
* Resolve the pinned block for a flat-rendered section (multi-project or
* scratch). Both render as a single block regardless of source bucket, so the
* block's `fullOrder` and `blockIds` are identical: every pinned id of the
* section, in rendered order. Returns null when `meta` is not one of them.
*/
function locateFlatSectionPinnedBlock(
meta: FrontendWorkspaceMetadata,
sortedWorkspacesByProject: Map<string, FrontendWorkspaceMetadata[]>,
includeRow: (row: FrontendWorkspaceMetadata) => boolean
): PinnedBlock | null {
const pinnedIds = collectFlatSectionRows(sortedWorkspacesByProject, includeRow)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
}

/**
* Resolve the pinned block containing `meta`, mirroring the sidebar renderer:
* multi-project rows form one flat block; regular rows partition by their
Expand All @@ -65,22 +83,15 @@ export function locatePinnedBlock(
// partitioning below would isolate every row into a block of one and
// swallow reorders. Treat them like the multi-project section instead.
if (meta.kind === "scratch") {
const pinnedIds = collectFlatSectionRows(
return locateFlatSectionPinnedBlock(
meta,
sortedWorkspacesByProject,
(row) => row.kind === "scratch"
)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
);
}

if (isMultiProject(meta)) {
const pinnedIds = collectFlatSectionRows(sortedWorkspacesByProject, isMultiProject)
.filter(isWorkspacePinned)
.map((row) => row.id);
if (!pinnedIds.includes(meta.id)) return null;
return { fullOrder: pinnedIds, blockIds: pinnedIds };
return locateFlatSectionPinnedBlock(meta, sortedWorkspacesByProject, isMultiProject);
}

const rows = sortedWorkspacesByProject.get(meta.projectPath) ?? [];
Expand Down
1 change: 0 additions & 1 deletion src/common/constants/codexOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export const CODEX_OAUTH_REQUIRED_MODELS = new Set<string>([
const CODEX_OAUTH_CONTEXT_WINDOW_OVERRIDES: Record<string, number> = {
// The public API exposes a 1.05M window for these models, but the ChatGPT/Codex
// model catalog publishes smaller context windows (372K for the GPT-5.6 family).
// Keep auth-route caps separate so API-key requests retain the full public window.
"gpt-5.5": 272_000,
"gpt-5.6": 372_000,
"gpt-5.6-sol": 372_000,
Expand Down
1 change: 1 addition & 0 deletions src/common/orpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
AgentSkillPackageSchema,
AgentSkillScopeSchema,
SkillNameSchema,
buildSkillDescriptor,
resolveSkillAdvertise,
resolveSkillUserInvocable,
resolveSkillWhenToUse,
Expand Down
23 changes: 23 additions & 0 deletions src/common/orpc/schemas/agentSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ export const AgentSkillDescriptorSchema = z.object({
whenToUse: z.string().min(1).max(1024).optional(),
});

/**
* Map validated SKILL.md frontmatter to the normalized AgentSkillDescriptor shape.
*
* Centralizes the frontmatter→descriptor field mapping (advertise / user-invocable /
* argument-hint / when-to-use normalization) so every discovery path produces byte-identical
* descriptors. Callers still validate the result with AgentSkillDescriptorSchema, since they
* handle validation failures differently (tool listing vs. diagnostics-collecting discovery).
*/
export function buildSkillDescriptor(
frontmatter: z.infer<typeof AgentSkillFrontmatterSchema>,
scope: z.infer<typeof AgentSkillScopeSchema>
): z.infer<typeof AgentSkillDescriptorSchema> {
return {
name: frontmatter.name,
description: frontmatter.description,
scope,
advertise: resolveSkillAdvertise(frontmatter),
userInvocable: resolveSkillUserInvocable(frontmatter),
argumentHint: frontmatter["argument-hint"],
whenToUse: resolveSkillWhenToUse(frontmatter),
};
}

export const AgentSkillPackageSchema = z
.object({
scope: AgentSkillScopeSchema,
Expand Down
16 changes: 12 additions & 4 deletions src/common/utils/ai/cacheStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ function isOfficialOpenAIBaseUrl(baseUrl: string): boolean {
);
}

/**
* Whether a canonical `provider:model` string has an `openai` origin with a
* non-empty model name. Used to gate both the request model and its resolved
* capability target in openaiExplicitPromptCachingAvailable.
*/
function isOpenAIOriginModel(canonical: string): boolean {
const [origin, modelName] = canonical.split(":", 2);
return origin === "openai" && !!modelName;
}

/**
* Route-aware eligibility for GPT-5.6 explicit prompt cache breakpoints.
*
Expand Down Expand Up @@ -200,16 +210,14 @@ export function openaiExplicitPromptCachingAvailable(
}

const normalized = normalizeToCanonical(modelString);
const [origin, modelName] = normalized.split(":", 2);
if (origin !== "openai" || !modelName) {
if (!isOpenAIOriginModel(normalized)) {
return false;
}

// Mapped aliases inherit eligibility only when the resolved capability
// target is also an OpenAI GPT-5.6-family model.
const capabilityModel = resolveModelForMetadata(normalized, providersConfig);
const [capabilityOrigin, capabilityModelName] = capabilityModel.split(":", 2);
if (capabilityOrigin !== "openai" || !capabilityModelName) {
if (!isOpenAIOriginModel(capabilityModel)) {
return false;
}
if (!isGpt56FamilyModel(capabilityModel)) {
Expand Down
22 changes: 16 additions & 6 deletions src/common/utils/thinking/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ export function getThinkingPolicyForModel(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): ThinkingPolicy {
const capabilityModel = resolveModelForMetadata(modelString, providersConfig ?? null);
return getExplicitThinkingPolicy(capabilityModel) ?? DEFAULT_THINKING_POLICY;
return getExplicitThinkingPolicyForModel(modelString, providersConfig) ?? DEFAULT_THINKING_POLICY;
}

/**
Expand Down Expand Up @@ -172,6 +171,20 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null {
return null;
}

/**
* Resolve a model to its capability model (following `mappedToModel` aliases via
* {@link resolveModelForMetadata}) and return its explicit reasoning policy, or
* `null` when none matches. Shared by {@link getThinkingPolicyForModel} and
* {@link hasExplicitThinkingPolicy}, which must resolve aliases identically
* before rule matching so a mapped alias inherits its target's policy.
*/
function getExplicitThinkingPolicyForModel(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): ThinkingPolicy | null {
return getExplicitThinkingPolicy(resolveModelForMetadata(modelString, providersConfig ?? null));
}

/** Canonical ordering index for a level (off=0 … max=5). */
function thinkingLevelIndex(level: ThinkingLevel): number {
return THINKING_LEVELS.indexOf(level);
Expand Down Expand Up @@ -213,10 +226,7 @@ export function hasExplicitThinkingPolicy(
modelString: string,
providersConfig?: ProvidersConfigMap | null
): boolean {
return (
getExplicitThinkingPolicy(resolveModelForMetadata(modelString, providersConfig ?? null)) !==
null
);
return getExplicitThinkingPolicyForModel(modelString, providersConfig) !== null;
}

/**
Expand Down
19 changes: 13 additions & 6 deletions src/common/utils/tokens/usageHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ export function addUsage(
};
}

/**
* Read Anthropic cache-creation input tokens from a provider-metadata record.
* Returns 0 when the field is absent so callers can sum without null checks.
*/
function getAnthropicCacheCreateTokens(metadata: Record<string, unknown> | undefined): number {
return (
(metadata?.anthropic as { cacheCreationInputTokens?: number } | undefined)
?.cacheCreationInputTokens ?? 0
);
}

/**
* Accumulate provider metadata across steps, specifically for cache creation tokens.
*
Expand All @@ -118,12 +129,8 @@ export function accumulateProviderMetadata(
if (!existing) return step;

// Extract cache creation tokens from both
const existingCacheCreate =
(existing.anthropic as { cacheCreationInputTokens?: number } | undefined)
?.cacheCreationInputTokens ?? 0;
const stepCacheCreate =
(step.anthropic as { cacheCreationInputTokens?: number } | undefined)
?.cacheCreationInputTokens ?? 0;
const existingCacheCreate = getAnthropicCacheCreateTokens(existing);
const stepCacheCreate = getAnthropicCacheCreateTokens(step);

const totalCacheCreate = existingCacheCreate + stepCacheCreate;

Expand Down
37 changes: 16 additions & 21 deletions src/common/utils/tools/toolCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@
* aiService or streamText.
*/

import type {
AssistantModelMessage,
Tool,
ToolCallPart,
ToolModelMessage,
ToolResultPart,
} from "ai";
import type { AssistantModelMessage, Tool, ToolModelMessage } from "ai";
import type { ModelMessage, MuxMessage } from "@/common/types/message";
import { buildRequiredToolPatterns, type ToolPolicy } from "@/common/utils/tools/toolPolicy";

Expand Down Expand Up @@ -154,6 +148,12 @@ export function buildToolCatalog(inputs: ToolCatalogInputs): ToolCatalogClassifi
return { catalog, deferredToolNames, allToolNames };
}

/** Return the tool record with the built-in `tool_search` entry removed. */
function withoutToolSearch(tools: Record<string, Tool>): Record<string, Tool> {
const { [TOOL_SEARCH_TOOL_NAME]: _removed, ...rest } = tools;
return rest;
}

/**
* Post-policy gate: decides whether tool-search deferral is active for this
* stream and returns the (possibly adjusted) tool record plus the seed state.
Expand Down Expand Up @@ -198,13 +198,11 @@ export function prepareToolSearch(inputs: ToolCatalogInputs): {
// Gated on the actual PTC flag, not record presence: a `code_execution`
// record entry may be a same-named MCP tool (classified as normal deferred).
if (inputs.ptcEnabled === true) {
const { [TOOL_SEARCH_TOOL_NAME]: _removed, ...rest } = inputs.tools;
return { tools: rest };
return { tools: withoutToolSearch(inputs.tools) };
}
const classification = buildToolCatalog(inputs);
if (classification.deferredToolNames.size === 0) {
const { [TOOL_SEARCH_TOOL_NAME]: _removed, ...rest } = inputs.tools;
return { tools: rest };
return { tools: withoutToolSearch(inputs.tools) };
}
return {
tools: inputs.tools,
Expand Down Expand Up @@ -354,13 +352,10 @@ function isMuxToolSearchOutput(output: unknown): boolean {
);
}

function renameLegacyToolSearchCallPart(part: ToolCallPart): ToolCallPart {
return part.toolName === LEGACY_TOOL_SEARCH_TOOL_NAME
? { ...part, toolName: TOOL_SEARCH_TOOL_NAME }
: part;
}

function renameLegacyToolSearchResultPart(part: ToolResultPart): ToolResultPart {
// Generic over the part shape so the assistant tool-call and tool-result paths
// share one implementation: both parts carry a `toolName`, and the rename is
// identical regardless of which kind of part is being rewritten.
function renameLegacyToolSearchPart<T extends { toolName: string }>(part: T): T {
return part.toolName === LEGACY_TOOL_SEARCH_TOOL_NAME
? { ...part, toolName: TOOL_SEARCH_TOOL_NAME }
: part;
Expand Down Expand Up @@ -440,10 +435,10 @@ export function normalizeLegacyToolSearchMessages(messages: ModelMessage[]): Mod
return part;
}
if (part.type === "tool-call") {
return renameLegacyToolSearchCallPart(part);
return renameLegacyToolSearchPart(part);
}
if (part.type === "tool-result") {
return renameLegacyToolSearchResultPart(part);
return renameLegacyToolSearchPart(part);
}
return part;
}
Expand All @@ -455,7 +450,7 @@ export function normalizeLegacyToolSearchMessages(messages: ModelMessage[]): Mod
if (message.role === "tool") {
const content: ToolModelMessage["content"] = message.content.map((part) => {
if (part.type === "tool-result" && legacyCallIds.has(part.toolCallId)) {
return renameLegacyToolSearchResultPart(part);
return renameLegacyToolSearchPart(part);
}
return part;
});
Expand Down
15 changes: 11 additions & 4 deletions src/common/utils/workflowRunMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ function isRecordValue(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

/**
* Tool outputs may be wrapped in a { type: "json", value } container (UI parts and SDK
* ToolResultPart outputs share this shape). Both the terminal-status probe and the
* record-stripping pass unwrap this the same way before inspecting the inner value.
*/
function isJsonWrappedOutput(output: Record<string, unknown>): boolean {
return output.type === "json" && "value" in output;
}

/**
* workflow_run / workflow_resume outputs embed the full run record (script source, event
* log, step snapshots) solely for the UI run card. The model only needs status/runId/result —
Expand All @@ -41,7 +50,7 @@ export function isTerminalWorkflowRunToolOutput(
if (!isWorkflowRunEmittingToolName(toolName) || !isRecordValue(output)) {
return false;
}
if (output.type === "json" && "value" in output) {
if (isJsonWrappedOutput(output)) {
return isTerminalWorkflowRunToolOutput(toolName, output.value, runId);
}
const status = output.status;
Expand All @@ -55,9 +64,7 @@ export function stripWorkflowRunRecordForModel(toolName: string, output: unknown
if (!isWorkflowRunEmittingToolName(toolName) || !isRecordValue(output)) {
return output;
}
// Tool outputs may be wrapped in a { type: "json", value } container (UI parts and
// SDK ToolResultPart outputs share this shape).
if (output.type === "json" && "value" in output) {
if (isJsonWrappedOutput(output)) {
const strippedValue = stripWorkflowRunRecordForModel(toolName, output.value);
return strippedValue === output.value ? output : { ...output, value: strippedValue };
}
Expand Down
14 changes: 2 additions & 12 deletions src/node/services/agentSkills/agentSkillsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import {
AgentSkillDescriptorSchema,
AgentSkillPackageSchema,
SkillNameSchema,
resolveSkillAdvertise,
resolveSkillUserInvocable,
resolveSkillWhenToUse,
buildSkillDescriptor,
} from "@/common/orpc/schemas";
import type {
AgentSkillDescriptor,
Expand Down Expand Up @@ -269,15 +267,7 @@ async function readSkillDescriptorFromDir(
directoryName,
});

const descriptor: AgentSkillDescriptor = {
name: parsed.frontmatter.name,
description: parsed.frontmatter.description,
scope,
advertise: resolveSkillAdvertise(parsed.frontmatter),
userInvocable: resolveSkillUserInvocable(parsed.frontmatter),
argumentHint: parsed.frontmatter["argument-hint"],
whenToUse: resolveSkillWhenToUse(parsed.frontmatter),
};
const descriptor = buildSkillDescriptor(parsed.frontmatter, scope);

const validated = AgentSkillDescriptorSchema.safeParse(descriptor);
if (!validated.success) {
Expand Down
Loading
Loading