mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): flatten nested ternaries in server (no-nested-ternary)
This commit is contained in:
@@ -3782,12 +3782,14 @@ export class DaemonClient {
|
||||
if (frame) {
|
||||
const binaryStartMs = perfNow();
|
||||
this.handleBinaryFrame(frame);
|
||||
let frameKind: "output" | "snapshot" | "other" = "other";
|
||||
if (frame.opcode === TerminalStreamOpcode.Output) {
|
||||
frameKind = "output";
|
||||
} else if (frame.opcode === TerminalStreamOpcode.Snapshot) {
|
||||
frameKind = "snapshot";
|
||||
}
|
||||
this.runtimeMetrics?.recordBinaryFrame(
|
||||
frame.opcode === TerminalStreamOpcode.Output
|
||||
? "output"
|
||||
: frame.opcode === TerminalStreamOpcode.Snapshot
|
||||
? "snapshot"
|
||||
: "other",
|
||||
frameKind,
|
||||
rawBytes.byteLength,
|
||||
perfNow() - binaryStartMs,
|
||||
);
|
||||
|
||||
@@ -145,6 +145,17 @@ type AttentionState =
|
||||
attentionTimestamp: Date;
|
||||
};
|
||||
|
||||
function resolveInitialAttention(input: AttentionState | undefined): AttentionState {
|
||||
if (input == null || !input.requiresAttention) {
|
||||
return { requiresAttention: false };
|
||||
}
|
||||
return {
|
||||
requiresAttention: true,
|
||||
attentionReason: input.attentionReason,
|
||||
attentionTimestamp: new Date(input.attentionTimestamp),
|
||||
};
|
||||
}
|
||||
|
||||
interface ForegroundTurnWaiter {
|
||||
turnId: string;
|
||||
callback: (event: AgentStreamEvent) => void;
|
||||
@@ -293,14 +304,14 @@ function isTurnTerminalEvent(event: AgentStreamEvent): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function abortMessage(reason: unknown, fallbackMessage: string): string {
|
||||
if (typeof reason === "string") return reason;
|
||||
if (reason instanceof Error) return reason.message;
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
function createAbortError(signal: AbortSignal | undefined, fallbackMessage: string): Error {
|
||||
const reason = signal?.reason;
|
||||
const message =
|
||||
typeof reason === "string"
|
||||
? reason
|
||||
: reason instanceof Error
|
||||
? reason.message
|
||||
: fallbackMessage;
|
||||
const message = abortMessage(signal?.reason, fallbackMessage);
|
||||
return Object.assign(new Error(message), { name: "AbortError" });
|
||||
}
|
||||
|
||||
@@ -1311,11 +1322,15 @@ export class AgentManager {
|
||||
mutableAgent.activeForegroundTurnId = null;
|
||||
const terminalError = mutableAgent.lastError;
|
||||
const shouldHoldBusyForReplacement = mutableAgent.pendingReplacement && !terminalError;
|
||||
mutableAgent.lifecycle = shouldHoldBusyForReplacement
|
||||
? "running"
|
||||
: terminalError
|
||||
? "error"
|
||||
: "idle";
|
||||
let nextLifecycle: "running" | "error" | "idle";
|
||||
if (shouldHoldBusyForReplacement) {
|
||||
nextLifecycle = "running";
|
||||
} else if (terminalError) {
|
||||
nextLifecycle = "error";
|
||||
} else {
|
||||
nextLifecycle = "idle";
|
||||
}
|
||||
mutableAgent.lifecycle = nextLifecycle;
|
||||
const persistenceHandle =
|
||||
mutableAgent.session.describePersistence() ??
|
||||
(mutableAgent.runtimeInfo?.sessionId
|
||||
@@ -2004,16 +2019,7 @@ export class AgentManager {
|
||||
lastUserMessageAt: options?.lastUserMessageAt ?? null,
|
||||
lastUsage: options?.lastUsage,
|
||||
lastError: options?.lastError,
|
||||
attention:
|
||||
options?.attention != null
|
||||
? options.attention.requiresAttention
|
||||
? {
|
||||
requiresAttention: true,
|
||||
attentionReason: options.attention.attentionReason,
|
||||
attentionTimestamp: new Date(options.attention.attentionTimestamp),
|
||||
}
|
||||
: { requiresAttention: false }
|
||||
: { requiresAttention: false },
|
||||
attention: resolveInitialAttention(options?.attention),
|
||||
internal: config.internal ?? false,
|
||||
labels: options?.labels ?? {},
|
||||
} as ActiveManagedAgent;
|
||||
|
||||
@@ -340,6 +340,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
})
|
||||
: null;
|
||||
const resolvedProvider = resolvedProviderModel?.provider ?? callerAgent.provider;
|
||||
let resolvedModel: string | undefined;
|
||||
if (resolvedProviderModel?.model) {
|
||||
resolvedModel = resolvedProviderModel.model;
|
||||
} else if (!hasProviderOverride && callerAgent.config.model) {
|
||||
resolvedModel = callerAgent.config.model;
|
||||
}
|
||||
return {
|
||||
type: "new-agent" as const,
|
||||
config: {
|
||||
@@ -354,11 +360,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(resolvedProviderModel?.model
|
||||
? { model: resolvedProviderModel.model }
|
||||
: !hasProviderOverride && callerAgent.config.model
|
||||
? { model: callerAgent.config.model }
|
||||
: {}),
|
||||
...(resolvedModel ? { model: resolvedModel } : {}),
|
||||
...(callerAgent.config.thinkingOptionId
|
||||
? { thinkingOptionId: callerAgent.config.thinkingOptionId }
|
||||
: {}),
|
||||
|
||||
@@ -178,6 +178,41 @@ interface ClaudeAgentSessionOptions {
|
||||
|
||||
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
|
||||
function resolvePathEnvKey(): "Path" | "PATH" | null {
|
||||
if (process.env["Path"] !== undefined) return "Path";
|
||||
if (process.env["PATH"] !== undefined) return "PATH";
|
||||
return null;
|
||||
}
|
||||
|
||||
function errorToMessageString(error: unknown): string {
|
||||
if (typeof error === "string") return error;
|
||||
if (error instanceof Error) return error.message;
|
||||
return "";
|
||||
}
|
||||
|
||||
function firstStringField(
|
||||
input: Record<string, unknown>,
|
||||
primaryKey: string,
|
||||
secondaryKey: string,
|
||||
): string | undefined {
|
||||
const primary = input[primaryKey];
|
||||
if (typeof primary === "string") return primary;
|
||||
const secondary = input[secondaryKey];
|
||||
if (typeof secondary === "string") return secondary;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractSessionIdRaw(msg: {
|
||||
session_id?: unknown;
|
||||
sessionId?: unknown;
|
||||
session?: { id?: unknown } | null;
|
||||
}): string {
|
||||
if (typeof msg.session_id === "string") return msg.session_id;
|
||||
if (typeof msg.sessionId === "string") return msg.sessionId;
|
||||
if (typeof msg.session?.id === "string") return msg.session.id;
|
||||
return "";
|
||||
}
|
||||
|
||||
function resolveClaudeSpawnCommand(
|
||||
spawnOptions: SpawnOptions,
|
||||
runtimeSettings?: ProviderRuntimeSettings,
|
||||
@@ -536,6 +571,26 @@ function isMetadata(value: unknown): value is AgentMetadata {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function createDefaultToolUseCacheEntry(id: string, block: ClaudeContentChunk): ToolUseCacheEntry {
|
||||
const nameFromBlock =
|
||||
typeof block.name === "string" && block.name.length > 0 ? block.name : "tool";
|
||||
let server: string;
|
||||
if (typeof block.server === "string" && block.server.length > 0) {
|
||||
server = block.server;
|
||||
} else if (typeof block.name === "string" && block.name.length > 0) {
|
||||
server = block.name;
|
||||
} else {
|
||||
server = "tool";
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: nameFromBlock,
|
||||
server,
|
||||
classification: "generic",
|
||||
started: false,
|
||||
};
|
||||
}
|
||||
|
||||
function readTrimmedString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
@@ -2137,12 +2192,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.logger.debug(
|
||||
{
|
||||
claudeBinary,
|
||||
pathEnvKey:
|
||||
process.env["Path"] !== undefined
|
||||
? "Path"
|
||||
: process.env["PATH"] !== undefined
|
||||
? "PATH"
|
||||
: null,
|
||||
pathEnvKey: resolvePathEnvKey(),
|
||||
pathIncludesClaudeLocalBin: (process.env["Path"] ?? process.env["PATH"] ?? "")
|
||||
.toLowerCase()
|
||||
.includes("\\.local\\bin"),
|
||||
@@ -2333,7 +2383,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (this.getRecentStderrDiagnostic()) {
|
||||
return;
|
||||
}
|
||||
const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
|
||||
const message = errorToMessageString(error);
|
||||
if (
|
||||
!/\bprocess exited with code\b/i.test(message) &&
|
||||
!/\bterminated by signal\b/i.test(message)
|
||||
@@ -2414,12 +2464,14 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (consecutiveRecoveries >= 3) {
|
||||
return false;
|
||||
}
|
||||
const message =
|
||||
typeof error === "string"
|
||||
? error
|
||||
: error instanceof Error
|
||||
? `${error.message}\n${error.stack ?? ""}`
|
||||
: JSON.stringify(error);
|
||||
let message: string;
|
||||
if (typeof error === "string") {
|
||||
message = error;
|
||||
} else if (error instanceof Error) {
|
||||
message = `${error.message}\n${error.stack ?? ""}`;
|
||||
} else {
|
||||
message = JSON.stringify(error);
|
||||
}
|
||||
return message.toLowerCase().includes("request was aborted");
|
||||
}
|
||||
|
||||
@@ -2919,15 +2971,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
sessionId?: unknown;
|
||||
session?: { id?: unknown } | null;
|
||||
};
|
||||
const sessionIdRaw =
|
||||
typeof msg.session_id === "string"
|
||||
? msg.session_id
|
||||
: typeof msg.sessionId === "string"
|
||||
? msg.sessionId
|
||||
: typeof msg.session?.id === "string"
|
||||
? msg.session.id
|
||||
: "";
|
||||
const sessionId = sessionIdRaw.trim();
|
||||
const sessionId = extractSessionIdRaw(msg).trim();
|
||||
if (!sessionId) {
|
||||
return null;
|
||||
}
|
||||
@@ -2961,15 +3005,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
sessionId?: unknown;
|
||||
session?: { id?: unknown } | null;
|
||||
};
|
||||
const newSessionIdRaw =
|
||||
typeof msg.session_id === "string"
|
||||
? msg.session_id
|
||||
: typeof msg.sessionId === "string"
|
||||
? msg.sessionId
|
||||
: typeof msg.session?.id === "string"
|
||||
? msg.session.id
|
||||
: "";
|
||||
const newSessionId = newSessionIdRaw.trim();
|
||||
const newSessionId = extractSessionIdRaw(msg).trim();
|
||||
if (!newSessionId) {
|
||||
return null;
|
||||
}
|
||||
@@ -3600,27 +3636,13 @@ class ClaudeAgentSession implements AgentSession {
|
||||
) {
|
||||
if (input && typeof input.file_path === "string") {
|
||||
// Support both old_str/new_str and old_string/new_string parameter names
|
||||
const oldContent =
|
||||
typeof input.old_str === "string"
|
||||
? input.old_str
|
||||
: typeof input.old_string === "string"
|
||||
? input.old_string
|
||||
: undefined;
|
||||
const newContent =
|
||||
typeof input.new_str === "string"
|
||||
? input.new_str
|
||||
: typeof input.new_string === "string"
|
||||
? input.new_string
|
||||
: undefined;
|
||||
const oldContent = firstStringField(input, "old_str", "old_string");
|
||||
const newContent = firstStringField(input, "new_str", "new_string");
|
||||
const diff = firstStringField(input, "patch", "diff");
|
||||
return {
|
||||
type: "file_edit",
|
||||
filePath: input.file_path,
|
||||
diff:
|
||||
typeof input.patch === "string"
|
||||
? input.patch
|
||||
: typeof input.diff === "string"
|
||||
? input.diff
|
||||
: undefined,
|
||||
diff,
|
||||
oldContent,
|
||||
newContent,
|
||||
};
|
||||
@@ -3702,20 +3724,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
const existing =
|
||||
this.toolUseCache.get(id) ??
|
||||
({
|
||||
id,
|
||||
name: typeof block.name === "string" && block.name.length > 0 ? block.name : "tool",
|
||||
server:
|
||||
typeof block.server === "string" && block.server.length > 0
|
||||
? block.server
|
||||
: typeof block.name === "string" && block.name.length > 0
|
||||
? block.name
|
||||
: "tool",
|
||||
classification: "generic",
|
||||
started: false,
|
||||
} satisfies ToolUseCacheEntry);
|
||||
const existing = this.toolUseCache.get(id) ?? createDefaultToolUseCacheEntry(id, block);
|
||||
|
||||
if (typeof block.name === "string" && block.name.length > 0) {
|
||||
existing.name = block.name;
|
||||
|
||||
@@ -342,12 +342,14 @@ export function coerceTaskNotificationHistoryRecordToSystemMessage(
|
||||
}
|
||||
|
||||
const normalizedStatus = parsed.status?.toLowerCase() ?? null;
|
||||
const status =
|
||||
normalizedStatus === "failed" || normalizedStatus === "error"
|
||||
? "failed"
|
||||
: normalizedStatus === "canceled" || normalizedStatus === "cancelled"
|
||||
? "stopped"
|
||||
: "completed";
|
||||
let status: "failed" | "stopped" | "completed";
|
||||
if (normalizedStatus === "failed" || normalizedStatus === "error") {
|
||||
status = "failed";
|
||||
} else if (normalizedStatus === "canceled" || normalizedStatus === "cancelled") {
|
||||
status = "stopped";
|
||||
} else {
|
||||
status = "completed";
|
||||
}
|
||||
|
||||
return {
|
||||
type: "system",
|
||||
|
||||
@@ -203,22 +203,29 @@ const ClaudeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||
|
||||
type ClaudeToolCallPass2 = z.infer<typeof ClaudeToolCallPass2Schema>;
|
||||
|
||||
function resolveDetailName(normalized: ClaudeToolCallPass2): string {
|
||||
switch (normalized.toolKind) {
|
||||
case "shell":
|
||||
return "shell";
|
||||
case "read":
|
||||
return "read_file";
|
||||
case "write":
|
||||
return "write_file";
|
||||
case "edit":
|
||||
return "apply_patch";
|
||||
case "search":
|
||||
case "fetch":
|
||||
return normalized.name;
|
||||
case "speak":
|
||||
return "speak";
|
||||
default:
|
||||
return normalized.name;
|
||||
}
|
||||
}
|
||||
|
||||
function toToolCallTimelineItem(normalized: ClaudeToolCallPass2): ToolCallTimelineItem {
|
||||
const name = normalized.toolKind === "speak" ? ("speak" as const) : normalized.name;
|
||||
const detailName =
|
||||
normalized.toolKind === "shell"
|
||||
? "shell"
|
||||
: normalized.toolKind === "read"
|
||||
? "read_file"
|
||||
: normalized.toolKind === "write"
|
||||
? "write_file"
|
||||
: normalized.toolKind === "edit"
|
||||
? "apply_patch"
|
||||
: normalized.toolKind === "search" || normalized.toolKind === "fetch"
|
||||
? normalized.name
|
||||
: normalized.toolKind === "speak"
|
||||
? "speak"
|
||||
: normalized.name;
|
||||
const detailName = resolveDetailName(normalized);
|
||||
const detail = deriveClaudeToolDetail(detailName, normalized.input, normalized.output);
|
||||
if (normalized.status === "failed") {
|
||||
return {
|
||||
|
||||
@@ -253,6 +253,30 @@ function resolveCodexHomeDir(): string {
|
||||
return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||
}
|
||||
|
||||
function decodeEscapedChar(next: string): string {
|
||||
if (next === "n") return "\n";
|
||||
if (next === "t") return "\t";
|
||||
return next;
|
||||
}
|
||||
|
||||
function resolvePermissionDecision(
|
||||
response: AgentPermissionResponse,
|
||||
): "accept" | "cancel" | "decline" {
|
||||
if (response.behavior === "allow") return "accept";
|
||||
if (response.interrupt) return "cancel";
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function firstPositiveFiniteNumber(primary: unknown, secondary: unknown): number | undefined {
|
||||
if (typeof primary === "number" && Number.isFinite(primary) && primary > 0) {
|
||||
return primary;
|
||||
}
|
||||
if (typeof secondary === "number" && Number.isFinite(secondary) && secondary > 0) {
|
||||
return secondary;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function tokenizeCommandArgs(args: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
let current = "";
|
||||
@@ -268,7 +292,7 @@ function tokenizeCommandArgs(args: string): string[] {
|
||||
const next = args[i + 1]!;
|
||||
if (next === quote || next === "\\" || next === "n" || next === "t") {
|
||||
i += 1;
|
||||
current += next === "n" ? "\n" : next === "t" ? "\t" : next;
|
||||
current += decodeEscapedChar(next);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -789,26 +813,14 @@ function toAgentUsage(tokenUsage: unknown): AgentUsage | undefined {
|
||||
totalTokens?: number;
|
||||
};
|
||||
};
|
||||
const contextWindowMaxTokens =
|
||||
typeof usage.model_context_window === "number" &&
|
||||
Number.isFinite(usage.model_context_window) &&
|
||||
usage.model_context_window > 0
|
||||
? usage.model_context_window
|
||||
: typeof usage.modelContextWindow === "number" &&
|
||||
Number.isFinite(usage.modelContextWindow) &&
|
||||
usage.modelContextWindow > 0
|
||||
? usage.modelContextWindow
|
||||
: undefined;
|
||||
const contextWindowUsedTokens =
|
||||
typeof usage.last?.total_tokens === "number" &&
|
||||
Number.isFinite(usage.last.total_tokens) &&
|
||||
usage.last.total_tokens > 0
|
||||
? usage.last.total_tokens
|
||||
: typeof usage.last?.totalTokens === "number" &&
|
||||
Number.isFinite(usage.last.totalTokens) &&
|
||||
usage.last.totalTokens > 0
|
||||
? usage.last.totalTokens
|
||||
: undefined;
|
||||
const contextWindowMaxTokens = firstPositiveFiniteNumber(
|
||||
usage.model_context_window,
|
||||
usage.modelContextWindow,
|
||||
);
|
||||
const contextWindowUsedTokens = firstPositiveFiniteNumber(
|
||||
usage.last?.total_tokens,
|
||||
usage.last?.totalTokens,
|
||||
);
|
||||
return {
|
||||
inputTokens: usage.last?.inputTokens,
|
||||
cachedInputTokens: usage.last?.cachedInputTokens,
|
||||
@@ -3188,12 +3200,14 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.resolvedPermissionRequests.add(requestId);
|
||||
|
||||
if (response.behavior === "deny" && pendingRequest?.kind === "tool") {
|
||||
const fallbackName =
|
||||
pendingRequest.name === "CodexBash"
|
||||
? "shell"
|
||||
: pendingRequest.name === "CodexFileChange"
|
||||
? "apply_patch"
|
||||
: pendingRequest.name;
|
||||
let fallbackName: string;
|
||||
if (pendingRequest.name === "CodexBash") {
|
||||
fallbackName = "shell";
|
||||
} else if (pendingRequest.name === "CodexFileChange") {
|
||||
fallbackName = "apply_patch";
|
||||
} else {
|
||||
fallbackName = pendingRequest.name;
|
||||
}
|
||||
this.emitEvent({
|
||||
type: "timeline",
|
||||
provider: CODEX_PROVIDER,
|
||||
@@ -3224,16 +3238,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
});
|
||||
|
||||
if (pending.kind === "command") {
|
||||
const decision =
|
||||
response.behavior === "allow" ? "accept" : response.interrupt ? "cancel" : "decline";
|
||||
pending.resolve({ decision });
|
||||
pending.resolve({ decision: resolvePermissionDecision(response) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.kind === "file") {
|
||||
const decision =
|
||||
response.behavior === "allow" ? "accept" : response.interrupt ? "cancel" : "decline";
|
||||
pending.resolve({ decision });
|
||||
pending.resolve({ decision: resolvePermissionDecision(response) });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -549,36 +549,36 @@ export async function parseRolloutFile(filePath: string): Promise<AgentTimelineI
|
||||
.reduce((map, record) => map.set(record.callId, record.output), new Map<string, unknown>());
|
||||
const terminalCommandsBySessionId = buildTerminalCommandBySessionId(parsedRecords);
|
||||
|
||||
const timeline = parsedRecords.flatMap((record): AgentTimelineItem[] =>
|
||||
record.kind === "timeline"
|
||||
? [record.item]
|
||||
: record.kind === "call"
|
||||
? (() => {
|
||||
if (record.name === "write_stdin") {
|
||||
const input =
|
||||
record.input && typeof record.input === "object"
|
||||
? (record.input as { session_id?: unknown; sessionId?: unknown })
|
||||
: null;
|
||||
const sessionId =
|
||||
readTerminalSessionId(input?.session_id) ?? readTerminalSessionId(input?.sessionId);
|
||||
return [
|
||||
mapCodexTerminalInteractionToToolCall({
|
||||
processId: sessionId,
|
||||
fallbackCallId: record.callId,
|
||||
command: sessionId ? terminalCommandsBySessionId.get(sessionId) : undefined,
|
||||
}),
|
||||
];
|
||||
}
|
||||
const mapped = mapCodexRolloutToolCall({
|
||||
callId: record.callId ?? null,
|
||||
name: record.name,
|
||||
input: record.input ?? null,
|
||||
output: record.callId ? (outputsByCallId.get(record.callId) ?? null) : null,
|
||||
});
|
||||
return mapped ? [mapped] : [];
|
||||
})()
|
||||
: [],
|
||||
);
|
||||
const timeline = parsedRecords.flatMap((record): AgentTimelineItem[] => {
|
||||
if (record.kind === "timeline") {
|
||||
return [record.item];
|
||||
}
|
||||
if (record.kind !== "call") {
|
||||
return [];
|
||||
}
|
||||
if (record.name === "write_stdin") {
|
||||
const input =
|
||||
record.input && typeof record.input === "object"
|
||||
? (record.input as { session_id?: unknown; sessionId?: unknown })
|
||||
: null;
|
||||
const sessionId =
|
||||
readTerminalSessionId(input?.session_id) ?? readTerminalSessionId(input?.sessionId);
|
||||
return [
|
||||
mapCodexTerminalInteractionToToolCall({
|
||||
processId: sessionId,
|
||||
fallbackCallId: record.callId,
|
||||
command: sessionId ? terminalCommandsBySessionId.get(sessionId) : undefined,
|
||||
}),
|
||||
];
|
||||
}
|
||||
const mapped = mapCodexRolloutToolCall({
|
||||
callId: record.callId ?? null,
|
||||
name: record.name,
|
||||
input: record.input ?? null,
|
||||
output: record.callId ? (outputsByCallId.get(record.callId) ?? null) : null,
|
||||
});
|
||||
return mapped ? [mapped] : [];
|
||||
});
|
||||
return dedupeMirroredTextTimelineItems(timeline);
|
||||
}
|
||||
|
||||
|
||||
@@ -870,37 +870,20 @@ export class PiDirectAgentSession implements AgentSession {
|
||||
error: unknown,
|
||||
): void {
|
||||
const turnId = this.currentTurnIdForEvent();
|
||||
const detail = mapToolDetail(toolCall, result);
|
||||
const baseItem = {
|
||||
type: "tool_call" as const,
|
||||
callId: toolCallId,
|
||||
name: toolCall.toolName,
|
||||
detail,
|
||||
};
|
||||
const item =
|
||||
status === "failed" ? { ...baseItem, status, error } : { ...baseItem, status, error: null };
|
||||
this.emit({
|
||||
type: "timeline",
|
||||
provider: PI_PROVIDER,
|
||||
turnId,
|
||||
item:
|
||||
status === "running"
|
||||
? {
|
||||
type: "tool_call",
|
||||
callId: toolCallId,
|
||||
name: toolCall.toolName,
|
||||
status,
|
||||
detail: mapToolDetail(toolCall, result),
|
||||
error: null,
|
||||
}
|
||||
: status === "completed"
|
||||
? {
|
||||
type: "tool_call",
|
||||
callId: toolCallId,
|
||||
name: toolCall.toolName,
|
||||
status,
|
||||
detail: mapToolDetail(toolCall, result),
|
||||
error: null,
|
||||
}
|
||||
: {
|
||||
type: "tool_call",
|
||||
callId: toolCallId,
|
||||
name: toolCall.toolName,
|
||||
status,
|
||||
detail: mapToolDetail(toolCall, result),
|
||||
error,
|
||||
},
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,18 @@ export const ToolShellInputSchema = z
|
||||
const parsedCommand = CommandValueSchema.safeParse(
|
||||
"command" in value ? value.command : value.cmd,
|
||||
);
|
||||
const command = parsedCommand.success
|
||||
? typeof parsedCommand.data === "string"
|
||||
? nonEmptyString(parsedCommand.data)
|
||||
: parsedCommand.data
|
||||
let command: string | undefined;
|
||||
if (parsedCommand.success) {
|
||||
if (typeof parsedCommand.data === "string") {
|
||||
command = nonEmptyString(parsedCommand.data);
|
||||
} else {
|
||||
command =
|
||||
parsedCommand.data
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0)
|
||||
.join(" ") || undefined
|
||||
: undefined;
|
||||
.join(" ") || undefined;
|
||||
}
|
||||
}
|
||||
return {
|
||||
command,
|
||||
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
|
||||
@@ -702,14 +706,11 @@ export function toWriteToolDetail(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = input?.content ?? output?.content;
|
||||
return {
|
||||
type: "write",
|
||||
filePath,
|
||||
...(input?.content
|
||||
? { content: input.content }
|
||||
: output?.content
|
||||
? { content: output.content }
|
||||
: {}),
|
||||
...(content ? { content } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -723,20 +724,14 @@ export function toEditToolDetail(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newString = input?.newString ?? output?.newString;
|
||||
const unifiedDiff = input?.unifiedDiff ?? output?.unifiedDiff;
|
||||
return {
|
||||
type: "edit",
|
||||
filePath,
|
||||
...(input?.oldString ? { oldString: input.oldString } : {}),
|
||||
...(input?.newString
|
||||
? { newString: input.newString }
|
||||
: output?.newString
|
||||
? { newString: output.newString }
|
||||
: {}),
|
||||
...(input?.unifiedDiff
|
||||
? { unifiedDiff: input.unifiedDiff }
|
||||
: output?.unifiedDiff
|
||||
? { unifiedDiff: output.unifiedDiff }
|
||||
: {}),
|
||||
...(newString ? { newString } : {}),
|
||||
...(unifiedDiff ? { unifiedDiff } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -750,12 +745,10 @@ export function toSearchToolDetail(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filePaths =
|
||||
isParsedToolGrepOutput(output) || isParsedToolGlobOutput(output)
|
||||
? output.filenames.length > 0
|
||||
? output.filenames
|
||||
: undefined
|
||||
: undefined;
|
||||
let filePaths: string[] | undefined;
|
||||
if (isParsedToolGrepOutput(output) || isParsedToolGlobOutput(output)) {
|
||||
filePaths = output.filenames.length > 0 ? output.filenames : undefined;
|
||||
}
|
||||
const webResults = isParsedToolWebSearchOutput(output)
|
||||
? output.results.flatMap((entry) => (typeof entry === "string" ? [] : entry.content))
|
||||
: undefined;
|
||||
|
||||
@@ -241,12 +241,14 @@ export function selectTimelineWindowByProjectedLimit(input: {
|
||||
};
|
||||
}
|
||||
|
||||
const projectedEntries =
|
||||
limit === 0 || limit >= projectedAll.length
|
||||
? projectedAll
|
||||
: direction === "after"
|
||||
? projectedAll.slice(0, limit)
|
||||
: projectedAll.slice(projectedAll.length - limit);
|
||||
let projectedEntries: typeof projectedAll;
|
||||
if (limit === 0 || limit >= projectedAll.length) {
|
||||
projectedEntries = projectedAll;
|
||||
} else if (direction === "after") {
|
||||
projectedEntries = projectedAll.slice(0, limit);
|
||||
} else {
|
||||
projectedEntries = projectedAll.slice(projectedAll.length - limit);
|
||||
}
|
||||
|
||||
if (projectedEntries.length === 0) {
|
||||
return {
|
||||
|
||||
@@ -635,12 +635,12 @@ export async function createPaseoDaemon(
|
||||
return;
|
||||
}
|
||||
const callerAgentIdRaw = req.query.callerAgentId;
|
||||
const callerAgentId =
|
||||
typeof callerAgentIdRaw === "string"
|
||||
? callerAgentIdRaw
|
||||
: Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string"
|
||||
? callerAgentIdRaw[0]
|
||||
: undefined;
|
||||
let callerAgentId: string | undefined;
|
||||
if (typeof callerAgentIdRaw === "string") {
|
||||
callerAgentId = callerAgentIdRaw;
|
||||
} else if (Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string") {
|
||||
callerAgentId = callerAgentIdRaw[0];
|
||||
}
|
||||
transport = await createAgentMcpTransport(callerAgentId);
|
||||
}
|
||||
|
||||
|
||||
@@ -212,38 +212,43 @@ class NonPersistentReloadClient implements AgentClient {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSpeechConfig() {
|
||||
if (hasLocalSpeech) {
|
||||
return {
|
||||
providers: {
|
||||
dictationStt: { provider: "local" as const, explicit: true },
|
||||
voiceStt: { provider: "local" as const, explicit: true },
|
||||
voiceTts: { provider: "local" as const, explicit: true },
|
||||
},
|
||||
local: {
|
||||
modelsDir: localModelsDir,
|
||||
models: {
|
||||
dictationStt:
|
||||
process.env.PASEO_DICTATION_LOCAL_STT_MODEL ?? "zipformer-bilingual-zh-en-2023-02-20",
|
||||
voiceStt:
|
||||
process.env.PASEO_VOICE_LOCAL_STT_MODEL ?? "zipformer-bilingual-zh-en-2023-02-20",
|
||||
voiceTts: process.env.PASEO_VOICE_LOCAL_TTS_MODEL ?? "kitten-nano-en-v0_1-fp16",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (openaiApiKey) {
|
||||
return {
|
||||
providers: {
|
||||
dictationStt: { provider: "openai" as const, explicit: true },
|
||||
voiceStt: { provider: "openai" as const, explicit: true },
|
||||
voiceTts: { provider: "openai" as const, explicit: true },
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describe("daemon client E2E", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
const speechConfig = hasLocalSpeech
|
||||
? {
|
||||
providers: {
|
||||
dictationStt: { provider: "local" as const, explicit: true },
|
||||
voiceStt: { provider: "local" as const, explicit: true },
|
||||
voiceTts: { provider: "local" as const, explicit: true },
|
||||
},
|
||||
local: {
|
||||
modelsDir: localModelsDir,
|
||||
models: {
|
||||
dictationStt:
|
||||
process.env.PASEO_DICTATION_LOCAL_STT_MODEL ??
|
||||
"zipformer-bilingual-zh-en-2023-02-20",
|
||||
voiceStt:
|
||||
process.env.PASEO_VOICE_LOCAL_STT_MODEL ?? "zipformer-bilingual-zh-en-2023-02-20",
|
||||
voiceTts: process.env.PASEO_VOICE_LOCAL_TTS_MODEL ?? "kitten-nano-en-v0_1-fp16",
|
||||
},
|
||||
},
|
||||
}
|
||||
: openaiApiKey
|
||||
? {
|
||||
providers: {
|
||||
dictationStt: { provider: "openai" as const, explicit: true },
|
||||
voiceStt: { provider: "openai" as const, explicit: true },
|
||||
voiceTts: { provider: "openai" as const, explicit: true },
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
const speechConfig = resolveSpeechConfig();
|
||||
|
||||
ctx = await createDaemonTestContext({
|
||||
dictationFinalTimeoutMs: 5000,
|
||||
|
||||
@@ -42,10 +42,28 @@ const CONTROL_PING_INTERVAL_MS = 10_000;
|
||||
const CONTROL_STALE_TIMEOUT_MS = 30_000;
|
||||
const CONTROL_READY_TIMEOUT_MS = 8_000;
|
||||
|
||||
function normalizeRelaySendPayload(data: string | Uint8Array | ArrayBuffer): string | ArrayBuffer {
|
||||
if (typeof data === "string") return data;
|
||||
if (data instanceof ArrayBuffer) return data;
|
||||
if (ArrayBuffer.isView(data)) {
|
||||
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||
const out = new Uint8Array(view.byteLength);
|
||||
out.set(view);
|
||||
return out.buffer;
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
function tryParseControlMessage(raw: unknown): ControlMessage | null {
|
||||
try {
|
||||
const text =
|
||||
typeof raw === "string" ? raw : Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
||||
let text: string;
|
||||
if (typeof raw === "string") {
|
||||
text = raw;
|
||||
} else if (Buffer.isBuffer(raw)) {
|
||||
text = raw.toString("utf8");
|
||||
} else {
|
||||
text = String(raw);
|
||||
}
|
||||
const parsed = JSON.parse(text) as any;
|
||||
if (!parsed || typeof parsed !== "object") return null;
|
||||
if (parsed.type === "ping") return { type: "ping" };
|
||||
@@ -443,19 +461,7 @@ function createEncryptedSocket(channel: EncryptedChannel, emitter: EventEmitter)
|
||||
return readyState;
|
||||
},
|
||||
send: (data) => {
|
||||
const outbound =
|
||||
typeof data === "string"
|
||||
? data
|
||||
: data instanceof ArrayBuffer
|
||||
? data
|
||||
: ArrayBuffer.isView(data)
|
||||
? (() => {
|
||||
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
||||
const out = new Uint8Array(view.byteLength);
|
||||
out.set(view);
|
||||
return out.buffer;
|
||||
})()
|
||||
: String(data);
|
||||
const outbound = normalizeRelaySendPayload(data);
|
||||
void channel.send(outbound).catch((error) => {
|
||||
emitter.emit("error", error);
|
||||
});
|
||||
|
||||
@@ -238,6 +238,66 @@ const LEGACY_PROVIDER_IDS = new Set(["claude", "codex", "opencode"]);
|
||||
const MIN_VERSION_ALL_PROVIDERS = "0.1.45";
|
||||
const MIN_VERSION_FLEXIBLE_EDITOR_IDS = "0.1.50";
|
||||
|
||||
function errorToFriendlyMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
function resolveSubscriptionId(
|
||||
subscribe: unknown,
|
||||
requestedSubscriptionId: string | undefined,
|
||||
): string | null {
|
||||
if (!subscribe) return null;
|
||||
if (requestedSubscriptionId && requestedSubscriptionId.length > 0) {
|
||||
return requestedSubscriptionId;
|
||||
}
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
function diffChangeTypeFor(file: { isNew?: boolean; isDeleted?: boolean }): "A" | "D" | "M" {
|
||||
if (file.isNew) return "A";
|
||||
if (file.isDeleted) return "D";
|
||||
return "M";
|
||||
}
|
||||
|
||||
function buildWorkspaceCheckout(
|
||||
workspace: PersistedWorkspaceRecord,
|
||||
project: PersistedProjectRecord,
|
||||
): ProjectPlacementPayload["checkout"] {
|
||||
if (project.kind !== "git") {
|
||||
return {
|
||||
cwd: workspace.cwd,
|
||||
isGit: false,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
};
|
||||
}
|
||||
if (workspace.kind === "worktree") {
|
||||
return {
|
||||
cwd: workspace.cwd,
|
||||
isGit: true,
|
||||
currentBranch: workspace.displayName,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: workspace.cwd,
|
||||
isPaseoOwnedWorktree: true,
|
||||
mainRepoRoot: project.rootPath,
|
||||
};
|
||||
}
|
||||
return {
|
||||
cwd: workspace.cwd,
|
||||
isGit: true,
|
||||
currentBranch: workspace.displayName,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: workspace.cwd,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
};
|
||||
}
|
||||
|
||||
function isAppVersionAtLeast(appVersion: string | null, minVersion: string): boolean {
|
||||
if (!appVersion) return false;
|
||||
// Strip prerelease suffix: "0.1.45-beta.4" -> "0.1.45"
|
||||
@@ -1057,13 +1117,7 @@ export class Session {
|
||||
);
|
||||
} catch (error) {
|
||||
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: "Unknown error";
|
||||
return { ok: false, error: message };
|
||||
return { ok: false, error: errorToFriendlyMessage(error) };
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
@@ -1082,8 +1136,7 @@ export class Session {
|
||||
}
|
||||
|
||||
private handleAgentRunError(agentId: string, error: unknown, context: string): void {
|
||||
const message =
|
||||
error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
||||
const message = errorToFriendlyMessage(error);
|
||||
this.sessionLogger.error({ err: error, agentId, context }, `${context} for agent ${agentId}`);
|
||||
this.emit({
|
||||
type: "activity_log",
|
||||
@@ -1408,36 +1461,7 @@ export class Session {
|
||||
if (!project) {
|
||||
throw new Error(`Project not found for workspace ${workspace.workspaceId}`);
|
||||
}
|
||||
const checkout =
|
||||
project.kind !== "git"
|
||||
? {
|
||||
cwd: workspace.cwd,
|
||||
isGit: false as const,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: null,
|
||||
isPaseoOwnedWorktree: false as const,
|
||||
mainRepoRoot: null,
|
||||
}
|
||||
: workspace.kind === "worktree"
|
||||
? {
|
||||
cwd: workspace.cwd,
|
||||
isGit: true as const,
|
||||
currentBranch: workspace.displayName,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: workspace.cwd,
|
||||
isPaseoOwnedWorktree: true as const,
|
||||
mainRepoRoot: project.rootPath,
|
||||
}
|
||||
: {
|
||||
cwd: workspace.cwd,
|
||||
isGit: true as const,
|
||||
currentBranch: workspace.displayName,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: workspace.cwd,
|
||||
isPaseoOwnedWorktree: false as const,
|
||||
mainRepoRoot: null,
|
||||
};
|
||||
const checkout = buildWorkspaceCheckout(workspace, project);
|
||||
return {
|
||||
projectKey: project.projectId,
|
||||
projectName: project.displayName,
|
||||
@@ -3538,7 +3562,7 @@ export class Session {
|
||||
? [
|
||||
"Files changed:",
|
||||
...diff.structured.map((file) => {
|
||||
const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
|
||||
const changeType = diffChangeTypeFor(file);
|
||||
const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
|
||||
return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
|
||||
}),
|
||||
@@ -3604,7 +3628,7 @@ export class Session {
|
||||
? [
|
||||
"Files changed:",
|
||||
...diff.structured.map((file) => {
|
||||
const changeType = file.isNew ? "A" : file.isDeleted ? "D" : "M";
|
||||
const changeType = diffChangeTypeFor(file);
|
||||
const status = file.status && file.status !== "ok" ? ` [${file.status}]` : "";
|
||||
return `${changeType}\t${file.path}\t(+${file.additions} -${file.deletions})${status}`;
|
||||
}),
|
||||
@@ -6569,11 +6593,7 @@ export class Session {
|
||||
request: Extract<SessionInboundMessage, { type: "fetch_agents_request" }>,
|
||||
): Promise<void> {
|
||||
const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
|
||||
const subscriptionId = request.subscribe
|
||||
? requestedSubscriptionId && requestedSubscriptionId.length > 0
|
||||
? requestedSubscriptionId
|
||||
: uuidv4()
|
||||
: null;
|
||||
const subscriptionId = resolveSubscriptionId(request.subscribe, requestedSubscriptionId);
|
||||
|
||||
try {
|
||||
if (subscriptionId) {
|
||||
@@ -6657,11 +6677,7 @@ export class Session {
|
||||
request: Extract<SessionInboundMessage, { type: "fetch_workspaces_request" }>,
|
||||
): Promise<void> {
|
||||
const requestedSubscriptionId = request.subscribe?.subscriptionId?.trim();
|
||||
const subscriptionId = request.subscribe
|
||||
? requestedSubscriptionId && requestedSubscriptionId.length > 0
|
||||
? requestedSubscriptionId
|
||||
: uuidv4()
|
||||
: null;
|
||||
const subscriptionId = resolveSubscriptionId(request.subscribe, requestedSubscriptionId);
|
||||
|
||||
try {
|
||||
this.sessionLogger.debug(
|
||||
@@ -7297,19 +7313,13 @@ export class Session {
|
||||
try {
|
||||
await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: "Unknown error";
|
||||
this.emit({
|
||||
type: "send_agent_message_response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
agentId,
|
||||
accepted: false,
|
||||
error: message,
|
||||
error: errorToFriendlyMessage(error),
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -7327,19 +7337,13 @@ export class Session {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: "Unknown error";
|
||||
this.emit({
|
||||
type: "send_agent_message_response",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
agentId: resolved.agentId,
|
||||
accepted: false,
|
||||
error: message,
|
||||
error: errorToFriendlyMessage(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -7383,12 +7387,14 @@ export class Session {
|
||||
return;
|
||||
}
|
||||
const final = this.buildStoredAgentPayload(record);
|
||||
const status =
|
||||
record.attentionReason === "permission"
|
||||
? "permission"
|
||||
: record.lastStatus === "error"
|
||||
? "error"
|
||||
: "idle";
|
||||
let status: "permission" | "error" | "idle";
|
||||
if (record.attentionReason === "permission") {
|
||||
status = "permission";
|
||||
} else if (record.lastStatus === "error") {
|
||||
status = "error";
|
||||
} else {
|
||||
status = "idle";
|
||||
}
|
||||
const error = resolveWaitForFinishError({ status, final });
|
||||
this.emit({
|
||||
type: "wait_for_finish_response",
|
||||
@@ -7415,11 +7421,14 @@ export class Session {
|
||||
throw new Error(`Agent ${agentId} disappeared while waiting`);
|
||||
}
|
||||
|
||||
let status: "permission" | "error" | "idle" = result.permission
|
||||
? "permission"
|
||||
: result.status === "error"
|
||||
? "error"
|
||||
: "idle";
|
||||
let status: "permission" | "error" | "idle";
|
||||
if (result.permission) {
|
||||
status = "permission";
|
||||
} else if (result.status === "error") {
|
||||
status = "error";
|
||||
} else {
|
||||
status = "idle";
|
||||
}
|
||||
const error = resolveWaitForFinishError({ status, final });
|
||||
|
||||
this.emit({
|
||||
@@ -7431,12 +7440,7 @@ export class Session {
|
||||
error instanceof Error &&
|
||||
(error.name === "AbortError" || error.message.toLowerCase().includes("aborted"));
|
||||
if (!isAbort) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "string"
|
||||
? error
|
||||
: "Unknown error";
|
||||
const message = errorToFriendlyMessage(error);
|
||||
this.sessionLogger.error({ err: error, agentId }, "wait_for_finish_request failed");
|
||||
const final = await this.getAgentPayloadById(agentId);
|
||||
this.emit({
|
||||
|
||||
@@ -93,7 +93,7 @@ export function pcm16leToFloat32(pcm16le: Buffer, gain: number = 1): Float32Arra
|
||||
const out = new Float32Array(int16.length);
|
||||
for (let i = 0; i < int16.length; i += 1) {
|
||||
const v = (int16[i]! / 32768.0) * gain;
|
||||
out[i] = v > 1 ? 1 : v < -1 ? -1 : v;
|
||||
out[i] = Math.max(-1, Math.min(1, v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -102,24 +102,28 @@ export class SherpaOnnxTTS implements TextToSpeechProvider {
|
||||
// from sherpa itself instead of trying to clone after generate() returns.
|
||||
enableExternalBuffer: false,
|
||||
});
|
||||
const rawSamples: Float32Array | null =
|
||||
audio && audio.samples instanceof Float32Array
|
||||
? audio.samples
|
||||
: audio && Array.isArray(audio.samples)
|
||||
? Float32Array.from(audio.samples as number[])
|
||||
: null;
|
||||
let rawSamples: Float32Array | null = null;
|
||||
if (audio && audio.samples instanceof Float32Array) {
|
||||
rawSamples = audio.samples;
|
||||
} else if (audio && Array.isArray(audio.samples)) {
|
||||
rawSamples = Float32Array.from(audio.samples as number[]);
|
||||
}
|
||||
// Copy to avoid "External buffers are not allowed" when sherpa-onnx
|
||||
// returns a Float32Array backed by native memory.
|
||||
const samples = rawSamples ? Float32Array.from(rawSamples) : null;
|
||||
const sampleRate: number =
|
||||
let sampleRate: number;
|
||||
if (
|
||||
audio &&
|
||||
typeof audio.sampleRate === "number" &&
|
||||
Number.isFinite(audio.sampleRate) &&
|
||||
audio.sampleRate > 0
|
||||
? audio.sampleRate
|
||||
: typeof this.tts.sampleRate === "number"
|
||||
? this.tts.sampleRate
|
||||
: 24000;
|
||||
) {
|
||||
sampleRate = audio.sampleRate;
|
||||
} else if (typeof this.tts.sampleRate === "number") {
|
||||
sampleRate = this.tts.sampleRate;
|
||||
} else {
|
||||
sampleRate = 24000;
|
||||
}
|
||||
|
||||
if (!samples) {
|
||||
throw new Error("Unexpected sherpa TTS output: missing Float32 samples");
|
||||
|
||||
@@ -312,6 +312,15 @@ function describeRequestedProviders(providers: RequestedSpeechProviders): {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveVoiceTtsLabel(
|
||||
ttsService: TextToSpeechProvider | null,
|
||||
localVoiceTtsProvider: TextToSpeechProvider | null,
|
||||
): "unavailable" | "local" | "openai" {
|
||||
if (!ttsService) return "unavailable";
|
||||
if (ttsService === localVoiceTtsProvider) return "local";
|
||||
return "openai";
|
||||
}
|
||||
|
||||
function resolveEffectiveProviderIds(params: {
|
||||
turnDetectionService: TurnDetectionProvider | null;
|
||||
sttService: SpeechToTextProvider | null;
|
||||
@@ -328,11 +337,7 @@ function resolveEffectiveProviderIds(params: {
|
||||
dictationStt: params.dictationSttService?.id ?? "unavailable",
|
||||
voiceTurnDetection: params.turnDetectionService?.id ?? "unavailable",
|
||||
voiceStt: params.sttService?.id ?? "unavailable",
|
||||
voiceTts: !params.ttsService
|
||||
? "unavailable"
|
||||
: params.ttsService === params.localVoiceTtsProvider
|
||||
? "local"
|
||||
: "openai",
|
||||
voiceTts: resolveVoiceTtsLabel(params.ttsService, params.localVoiceTtsProvider),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -296,12 +296,14 @@ describe("voice roundtrip e2e", () => {
|
||||
})();
|
||||
|
||||
const outputRaw = Buffer.concat(outputAudio.chunks);
|
||||
const outputFormat =
|
||||
outputAudio.format === "pcm"
|
||||
? "audio/pcm;rate=24000;bits=16"
|
||||
: outputAudio.format.includes("wav")
|
||||
? "audio/wav"
|
||||
: `audio/${outputAudio.format}`;
|
||||
let outputFormat: string;
|
||||
if (outputAudio.format === "pcm") {
|
||||
outputFormat = "audio/pcm;rate=24000;bits=16";
|
||||
} else if (outputAudio.format.includes("wav")) {
|
||||
outputFormat = "audio/wav";
|
||||
} else {
|
||||
outputFormat = `audio/${outputAudio.format}`;
|
||||
}
|
||||
const transcription = await withTimeout(
|
||||
sttOutput.transcribe(outputRaw, outputFormat, {
|
||||
label: "voice-roundtrip-output",
|
||||
|
||||
@@ -1667,8 +1667,14 @@ function toCurrentPullRequestStatus(
|
||||
const repoIdentity = parseGitHubPullRequestRepo(item.url);
|
||||
const mergedAt =
|
||||
typeof item.mergedAt === "string" && item.mergedAt.trim().length > 0 ? item.mergedAt : null;
|
||||
const state =
|
||||
mergedAt !== null ? "merged" : item.state.trim().length > 0 ? item.state.toLowerCase() : "";
|
||||
let state: string;
|
||||
if (mergedAt !== null) {
|
||||
state = "merged";
|
||||
} else if (item.state.trim().length > 0) {
|
||||
state = item.state.toLowerCase();
|
||||
} else {
|
||||
state = "";
|
||||
}
|
||||
const checks = parseStatusCheckRollup(item.statusCheckRollup);
|
||||
return {
|
||||
...(typeof item.number === "number" ? { number: item.number } : {}),
|
||||
|
||||
@@ -166,30 +166,36 @@ export function findLatestPermissionRequest(
|
||||
return latest;
|
||||
}
|
||||
|
||||
function resolveAgentAttentionTitle(reason: AgentAttentionReason): string {
|
||||
if (reason === "permission") return "Agent needs permission";
|
||||
if (reason === "error") return "Agent needs attention";
|
||||
return "Agent finished";
|
||||
}
|
||||
|
||||
function resolveAgentAttentionPreview(
|
||||
input: BuildAgentAttentionNotificationPayloadInput,
|
||||
): string | null {
|
||||
if (input.reason === "finished") {
|
||||
return buildNotificationPreview(input.assistantMessage);
|
||||
}
|
||||
if (input.reason === "permission") {
|
||||
return buildNotificationPreview(buildPermissionDetails(input.permissionRequest));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveAgentAttentionFallbackBody(reason: AgentAttentionReason): string {
|
||||
if (reason === "permission") return "Permission requested.";
|
||||
if (reason === "error") return "Encountered an error.";
|
||||
return "Finished working.";
|
||||
}
|
||||
|
||||
export function buildAgentAttentionNotificationPayload(
|
||||
input: BuildAgentAttentionNotificationPayloadInput,
|
||||
): AgentAttentionNotificationPayload {
|
||||
const title =
|
||||
input.reason === "permission"
|
||||
? "Agent needs permission"
|
||||
: input.reason === "error"
|
||||
? "Agent needs attention"
|
||||
: "Agent finished";
|
||||
|
||||
const preview =
|
||||
input.reason === "finished"
|
||||
? buildNotificationPreview(input.assistantMessage)
|
||||
: input.reason === "permission"
|
||||
? buildNotificationPreview(buildPermissionDetails(input.permissionRequest))
|
||||
: null;
|
||||
|
||||
const body =
|
||||
preview ??
|
||||
(input.reason === "permission"
|
||||
? "Permission requested."
|
||||
: input.reason === "error"
|
||||
? "Encountered an error."
|
||||
: "Finished working.");
|
||||
const title = resolveAgentAttentionTitle(input.reason);
|
||||
const preview = resolveAgentAttentionPreview(input);
|
||||
const body = preview ?? resolveAgentAttentionFallbackBody(input.reason);
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@@ -17,8 +17,12 @@ export function normalizeRelayProtocolVersion(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof value === "string" ? value.trim() : typeof value === "number" ? String(value) : "";
|
||||
let normalized = "";
|
||||
if (typeof value === "string") {
|
||||
normalized = value.trim();
|
||||
} else if (typeof value === "number") {
|
||||
normalized = String(value);
|
||||
}
|
||||
if (!normalized) {
|
||||
return fallback;
|
||||
}
|
||||
@@ -134,7 +138,14 @@ export function extractHostPortFromWebSocketUrl(wsUrl: string): string {
|
||||
}
|
||||
|
||||
const host = parsed.hostname;
|
||||
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "wss:" ? 443 : 80;
|
||||
let port: number;
|
||||
if (parsed.port) {
|
||||
port = Number(parsed.port);
|
||||
} else if (parsed.protocol === "wss:") {
|
||||
port = 443;
|
||||
} else {
|
||||
port = 80;
|
||||
}
|
||||
if (!host) {
|
||||
throw new Error("Invalid WebSocket URL (missing hostname)");
|
||||
}
|
||||
|
||||
@@ -331,12 +331,14 @@ function normalizeProcessToken(token: string): string {
|
||||
return token;
|
||||
}
|
||||
|
||||
const quote =
|
||||
token.startsWith('"') && token.endsWith('"')
|
||||
? '"'
|
||||
: token.startsWith("'") && token.endsWith("'")
|
||||
? "'"
|
||||
: "";
|
||||
let quote: "'" | '"' | "";
|
||||
if (token.startsWith('"') && token.endsWith('"')) {
|
||||
quote = '"';
|
||||
} else if (token.startsWith("'") && token.endsWith("'")) {
|
||||
quote = "'";
|
||||
} else {
|
||||
quote = "";
|
||||
}
|
||||
const rawToken = quote ? token.slice(1, -1) : token;
|
||||
if (rawToken.length === 0) {
|
||||
return token;
|
||||
|
||||
@@ -536,12 +536,14 @@ async function assertPortAvailable(port: number): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", (error: NodeJS.ErrnoException) => {
|
||||
const message =
|
||||
error?.code === "EADDRINUSE"
|
||||
? `Persisted worktree port ${port} is already in use`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
let message: string;
|
||||
if (error?.code === "EADDRINUSE") {
|
||||
message = `Persisted worktree port ${port} is already in use`;
|
||||
} else if (error instanceof Error) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = String(error);
|
||||
}
|
||||
reject(new Error(message));
|
||||
});
|
||||
server.listen(port, () => {
|
||||
|
||||
Reference in New Issue
Block a user