import { randomUUID } from "node:crypto"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.js"; import { resolveAllowedModelRef, resolveConfiguredModelRef } from "../agents/model-selection.js"; import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; import { formatThinkingLevels, formatXHighModelHint, normalizeElevatedLevel, normalizeReasoningLevel, normalizeThinkLevel, normalizeUsageDisplay, supportsXHighThinking, } from "../auto-reply/thinking.js"; import type { ClawdbotConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { isSubagentSessionKey } from "../routing/session-key.js"; import { applyVerboseOverride, parseVerboseOverride } from "../sessions/level-overrides.js"; import { normalizeSendPolicy } from "../sessions/send-policy.js"; import { parseSessionLabel } from "../sessions/session-label.js"; import { ErrorCodes, type ErrorShape, errorShape, type SessionsPatchParams, } from "./protocol/index.js"; function invalid(message: string): { ok: false; error: ErrorShape } { return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) }; } function normalizeExecHost(raw: string): "sandbox" | "gateway" | "node" | undefined { const normalized = raw.trim().toLowerCase(); if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") { return normalized; } return undefined; } function normalizeExecSecurity(raw: string): "deny" | "allowlist" | "full" | undefined { const normalized = raw.trim().toLowerCase(); if (normalized === "deny" || normalized === "allowlist" || normalized === "full") { return normalized; } return undefined; } function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined { const normalized = raw.trim().toLowerCase(); if (normalized === "off" || normalized === "on-miss" || normalized === "always") { return normalized; } return undefined; } export async function applySessionsPatchToStore(params: { cfg: ClawdbotConfig; store: Record; storeKey: string; patch: SessionsPatchParams; loadGatewayModelCatalog?: () => Promise; }): Promise<{ ok: true; entry: SessionEntry } | { ok: false; error: ErrorShape }> { const { cfg, store, storeKey, patch } = params; const now = Date.now(); const existing = store[storeKey]; const next: SessionEntry = existing ? { ...existing, updatedAt: Math.max(existing.updatedAt ?? 0, now), } : { sessionId: randomUUID(), updatedAt: now }; if ("spawnedBy" in patch) { const raw = patch.spawnedBy; if (raw === null) { if (existing?.spawnedBy) return invalid("spawnedBy cannot be cleared once set"); } else if (raw !== undefined) { const trimmed = String(raw).trim(); if (!trimmed) return invalid("invalid spawnedBy: empty"); if (!isSubagentSessionKey(storeKey)) { return invalid("spawnedBy is only supported for subagent:* sessions"); } if (existing?.spawnedBy && existing.spawnedBy !== trimmed) { return invalid("spawnedBy cannot be changed once set"); } next.spawnedBy = trimmed; } } if ("label" in patch) { const raw = patch.label; if (raw === null) { delete next.label; } else if (raw !== undefined) { const parsed = parseSessionLabel(raw); if (!parsed.ok) return invalid(parsed.error); for (const [key, entry] of Object.entries(store)) { if (key === storeKey) continue; if (entry?.label === parsed.label) { return invalid(`label already in use: ${parsed.label}`); } } next.label = parsed.label; } } if ("thinkingLevel" in patch) { const raw = patch.thinkingLevel; if (raw === null) { delete next.thinkingLevel; } else if (raw !== undefined) { const normalized = normalizeThinkLevel(String(raw)); if (!normalized) { const resolvedDefault = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const hintProvider = existing?.providerOverride?.trim() || resolvedDefault.provider; const hintModel = existing?.modelOverride?.trim() || resolvedDefault.model; return invalid( `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`, ); } if (normalized === "off") delete next.thinkingLevel; else next.thinkingLevel = normalized; } } if ("verboseLevel" in patch) { const raw = patch.verboseLevel; const parsed = parseVerboseOverride(raw); if (!parsed.ok) return invalid(parsed.error); applyVerboseOverride(next, parsed.value); } if ("reasoningLevel" in patch) { const raw = patch.reasoningLevel; if (raw === null) { delete next.reasoningLevel; } else if (raw !== undefined) { const normalized = normalizeReasoningLevel(String(raw)); if (!normalized) { return invalid('invalid reasoningLevel (use "on"|"off"|"stream")'); } if (normalized === "off") delete next.reasoningLevel; else next.reasoningLevel = normalized; } } if ("responseUsage" in patch) { const raw = patch.responseUsage; if (raw === null) { delete next.responseUsage; } else if (raw !== undefined) { const normalized = normalizeUsageDisplay(String(raw)); if (!normalized) return invalid('invalid responseUsage (use "off"|"tokens"|"full")'); if (normalized === "off") delete next.responseUsage; else next.responseUsage = normalized; } } if ("elevatedLevel" in patch) { const raw = patch.elevatedLevel; if (raw === null) { delete next.elevatedLevel; } else if (raw !== undefined) { const normalized = normalizeElevatedLevel(String(raw)); if (!normalized) return invalid('invalid elevatedLevel (use "on"|"off")'); // Persist "off" explicitly so patches can override defaults. next.elevatedLevel = normalized; } } if ("execHost" in patch) { const raw = patch.execHost; if (raw === null) { delete next.execHost; } else if (raw !== undefined) { const normalized = normalizeExecHost(String(raw)); if (!normalized) return invalid('invalid execHost (use "sandbox"|"gateway"|"node")'); next.execHost = normalized; } } if ("execSecurity" in patch) { const raw = patch.execSecurity; if (raw === null) { delete next.execSecurity; } else if (raw !== undefined) { const normalized = normalizeExecSecurity(String(raw)); if (!normalized) return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")'); next.execSecurity = normalized; } } if ("execAsk" in patch) { const raw = patch.execAsk; if (raw === null) { delete next.execAsk; } else if (raw !== undefined) { const normalized = normalizeExecAsk(String(raw)); if (!normalized) return invalid('invalid execAsk (use "off"|"on-miss"|"always")'); next.execAsk = normalized; } } if ("execNode" in patch) { const raw = patch.execNode; if (raw === null) { delete next.execNode; } else if (raw !== undefined) { const trimmed = String(raw).trim(); if (!trimmed) return invalid("invalid execNode: empty"); next.execNode = trimmed; } } if ("model" in patch) { const raw = patch.model; if (raw === null) { delete next.providerOverride; delete next.modelOverride; } else if (raw !== undefined) { const trimmed = String(raw).trim(); if (!trimmed) return invalid("invalid model: empty"); const resolvedDefault = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); if (!params.loadGatewayModelCatalog) { return { ok: false, error: errorShape(ErrorCodes.UNAVAILABLE, "model catalog unavailable"), }; } const catalog = await params.loadGatewayModelCatalog(); const resolved = resolveAllowedModelRef({ cfg, catalog, raw: trimmed, defaultProvider: resolvedDefault.provider, defaultModel: resolvedDefault.model, }); if ("error" in resolved) { return invalid(resolved.error); } if ( resolved.ref.provider === resolvedDefault.provider && resolved.ref.model === resolvedDefault.model ) { delete next.providerOverride; delete next.modelOverride; } else { next.providerOverride = resolved.ref.provider; next.modelOverride = resolved.ref.model; } } } if (next.thinkingLevel === "xhigh") { const resolvedDefault = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const effectiveProvider = next.providerOverride ?? resolvedDefault.provider; const effectiveModel = next.modelOverride ?? resolvedDefault.model; if (!supportsXHighThinking(effectiveProvider, effectiveModel)) { if ("thinkingLevel" in patch) { return invalid(`thinkingLevel "xhigh" is only supported for ${formatXHighModelHint()}`); } next.thinkingLevel = "high"; } } if ("sendPolicy" in patch) { const raw = patch.sendPolicy; if (raw === null) { delete next.sendPolicy; } else if (raw !== undefined) { const normalized = normalizeSendPolicy(String(raw)); if (!normalized) return invalid('invalid sendPolicy (use "allow"|"deny")'); next.sendPolicy = normalized; } } if ("groupActivation" in patch) { const raw = patch.groupActivation; if (raw === null) { delete next.groupActivation; } else if (raw !== undefined) { const normalized = normalizeGroupActivation(String(raw)); if (!normalized) { return invalid('invalid groupActivation (use "mention"|"always")'); } next.groupActivation = normalized; } } store[storeKey] = next; return { ok: true, entry: next }; }