mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
Compare commits
3 Commits
v1.10.8
...
codex/memo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379928fa10 | ||
|
|
ee0d788e58 | ||
|
|
f353eb056a |
@@ -55,8 +55,6 @@ ccb update # 更新到最新版本
|
|||||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
||||||
```
|
```
|
||||||
|
|
||||||
> **安装/更新失败?** 先 `npm rm -g claude-code-best` 清理旧版本,再 `npm i -g claude-code-best@latest`。仍失败则指定版本号:`npm i -g claude-code-best@<版本号>`
|
|
||||||
|
|
||||||
## ⚡ 快速开始(源码版)
|
## ⚡ 快速开始(源码版)
|
||||||
|
|
||||||
### ⚙️ 环境要求
|
### ⚙️ 环境要求
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.10.8",
|
"version": "1.10.2",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -106,84 +106,6 @@ describe("findActualString", () => {
|
|||||||
const result = findActualString("hello", "");
|
const result = findActualString("hello", "");
|
||||||
expect(result).toBe("");
|
expect(result).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Tab/space normalization (Bug #2 reproduction) ──
|
|
||||||
|
|
||||||
test("finds match when search uses spaces but file uses tabs", () => {
|
|
||||||
// File content uses Tab indentation
|
|
||||||
const fileContent = "\tif (x) {\n\t\treturn 1;\n\t}";
|
|
||||||
// User copies from Read output which renders tabs as spaces
|
|
||||||
const searchWithSpaces = " if (x) {\n return 1;\n }";
|
|
||||||
const result = findActualString(fileContent, searchWithSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result).toBe(fileContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("finds match when search mixes tabs and spaces inconsistently", () => {
|
|
||||||
const fileContent = "\tconst x = 1; // comment";
|
|
||||||
const searchMixed = " const x = 1; // comment";
|
|
||||||
const result = findActualString(fileContent, searchMixed);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("finds match for single-line tab-to-space mismatch", () => {
|
|
||||||
const fileContent = "\t\torder_price = NormalizeDouble(ask, digits);";
|
|
||||||
const searchSpaces = " order_price = NormalizeDouble(ask, digits);";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── CJK / UTF-8 characters (Bug #1 reproduction) ──
|
|
||||||
|
|
||||||
test("finds match with CJK characters in content", () => {
|
|
||||||
const fileContent = "input int x = 620; // 止盈点数(点) — 32个pip=320点";
|
|
||||||
const result = findActualString(fileContent, fileContent);
|
|
||||||
expect(result).toBe(fileContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("finds match with CJK characters when tab/space differs", () => {
|
|
||||||
const fileContent = "\t// 向上突破 → Sell Limit (逆方向做空)";
|
|
||||||
const searchSpaces = " // 向上突破 → Sell Limit (逆方向做空)";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result).toBe(fileContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Multiline with tabs + CJK (combined Bug #1 + #2) ──
|
|
||||||
|
|
||||||
test("finds multiline match with tabs and CJK characters", () => {
|
|
||||||
const fileContent = "\tif(effective_dir == BREAKOUT_UP)\n\t\t{\n\t\t\t// 向上突破\n\t\t}";
|
|
||||||
const searchSpaces = " if(effective_dir == BREAKOUT_UP)\n {\n // 向上突破\n }";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result).toBe(fileContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Returned string must be a valid substring of fileContent ──
|
|
||||||
|
|
||||||
test("returned string from tab match is a real substring of fileContent", () => {
|
|
||||||
const fileContent = "prefix\n\t\tindented code\nsuffix";
|
|
||||||
const searchSpaces = "prefix\n indented code\nsuffix";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(fileContent.includes(result!)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returned string from partial tab match is a real substring", () => {
|
|
||||||
const fileContent = "line1\n\tif (x) {\n\t\tdoStuff();\n\t}\nline5";
|
|
||||||
const searchSpaces = " if (x) {\n doStuff();\n }";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(fileContent.includes(result!)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("tab match with mixed indentation levels", () => {
|
|
||||||
const fileContent = "class Foo {\n\t\tmethod1() {\n\t\t\treturn 42;\n\t\t}\n}";
|
|
||||||
const searchSpaces = "class Foo {\n method1() {\n return 42;\n }\n}";
|
|
||||||
const result = findActualString(fileContent, searchSpaces);
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(fileContent.includes(result!)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── preserveQuoteStyle ─────────────────────────────────────────────────
|
// ─── preserveQuoteStyle ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -63,26 +63,9 @@ export function stripTrailingWhitespace(str: string): string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes whitespace for fuzzy matching by converting tabs to spaces
|
|
||||||
* and collapsing leading whitespace on each line to a canonical form.
|
|
||||||
* This handles the case where Read tool output renders tabs as spaces,
|
|
||||||
* so users copy spaces from the output but the file actually has tabs.
|
|
||||||
*/
|
|
||||||
function normalizeWhitespace(str: string): string {
|
|
||||||
return str.replace(/\t/g, ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the actual string in the file content that matches the search string,
|
* Finds the actual string in the file content that matches the search string,
|
||||||
* accounting for quote normalization and tab/space differences.
|
* accounting for quote normalization
|
||||||
*
|
|
||||||
* Matching cascade:
|
|
||||||
* 1. Exact match
|
|
||||||
* 2. Quote normalization (curly → straight quotes)
|
|
||||||
* 3. Tab/space normalization (tabs ↔ spaces in leading whitespace)
|
|
||||||
* 4. Quote + tab/space normalization combined
|
|
||||||
*
|
|
||||||
* @param fileContent The file content to search in
|
* @param fileContent The file content to search in
|
||||||
* @param searchString The string to search for
|
* @param searchString The string to search for
|
||||||
* @returns The actual string found in the file, or null if not found
|
* @returns The actual string found in the file, or null if not found
|
||||||
@@ -106,92 +89,9 @@ export function findActualString(
|
|||||||
return fileContent.substring(searchIndex, searchIndex + searchString.length)
|
return fileContent.substring(searchIndex, searchIndex + searchString.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try with tab/space normalization — handles the case where Read output
|
|
||||||
// renders tabs as spaces and the user copies the rendered version
|
|
||||||
const wsNormalizedFile = normalizeWhitespace(fileContent)
|
|
||||||
const wsNormalizedSearch = normalizeWhitespace(searchString)
|
|
||||||
|
|
||||||
const wsSearchIndex = wsNormalizedFile.indexOf(wsNormalizedSearch)
|
|
||||||
if (wsSearchIndex !== -1) {
|
|
||||||
// Map the match position back to the original file content.
|
|
||||||
// We need to find the corresponding range in the original string.
|
|
||||||
return mapNormalizedMatchBackToFile(fileContent, wsNormalizedFile, wsSearchIndex, wsNormalizedSearch.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try combined: quote normalization + tab/space normalization
|
|
||||||
const combinedFile = normalizeWhitespace(normalizedFile)
|
|
||||||
const combinedSearch = normalizeWhitespace(normalizedSearch)
|
|
||||||
|
|
||||||
const combinedIndex = combinedFile.indexOf(combinedSearch)
|
|
||||||
if (combinedIndex !== -1) {
|
|
||||||
return mapNormalizedMatchBackToFile(fileContent, combinedFile, combinedIndex, combinedSearch.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a match found in a normalized version of fileContent, map the match
|
|
||||||
* position back to the original fileContent and extract the corresponding
|
|
||||||
* substring.
|
|
||||||
*
|
|
||||||
* Strategy: walk through both strings character by character, building a
|
|
||||||
* mapping from normalized offset to original offset. When a tab is expanded
|
|
||||||
* to 4 spaces in the normalized version, the normalized offset advances by 4
|
|
||||||
* while the original offset advances by 1.
|
|
||||||
*/
|
|
||||||
function mapNormalizedMatchBackToFile(
|
|
||||||
fileContent: string,
|
|
||||||
normalizedFile: string,
|
|
||||||
normalizedStart: number,
|
|
||||||
normalizedLength: number,
|
|
||||||
): string {
|
|
||||||
// Build a sparse mapping from normalized position → original position.
|
|
||||||
// We only need to map the range [normalizedStart, normalizedStart + normalizedLength].
|
|
||||||
let normPos = 0
|
|
||||||
let origPos = 0
|
|
||||||
let origStart = -1
|
|
||||||
let origEnd = -1
|
|
||||||
|
|
||||||
while (origPos < fileContent.length && normPos <= normalizedStart + normalizedLength) {
|
|
||||||
if (normPos === normalizedStart) {
|
|
||||||
origStart = origPos
|
|
||||||
}
|
|
||||||
if (normPos === normalizedStart + normalizedLength) {
|
|
||||||
origEnd = origPos
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const origChar = fileContent[origPos]!
|
|
||||||
if (origChar === '\t') {
|
|
||||||
// Tab expands to 4 spaces in normalized version
|
|
||||||
const nextNormPos = normPos + 4
|
|
||||||
// If normalizedStart falls within this expanded tab, snap to origPos
|
|
||||||
if (normPos < normalizedStart && nextNormPos > normalizedStart && origStart === -1) {
|
|
||||||
origStart = origPos
|
|
||||||
}
|
|
||||||
if (normPos < normalizedStart + normalizedLength && nextNormPos > normalizedStart + normalizedLength && origEnd === -1) {
|
|
||||||
origEnd = origPos + 1
|
|
||||||
}
|
|
||||||
normPos = nextNormPos
|
|
||||||
origPos++
|
|
||||||
} else {
|
|
||||||
normPos++
|
|
||||||
origPos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: if we couldn't map precisely, use character-count heuristic
|
|
||||||
if (origStart === -1) origStart = 0
|
|
||||||
if (origEnd === -1) {
|
|
||||||
// Approximate: use the ratio of original to normalized length
|
|
||||||
const ratio = fileContent.length / normalizedFile.length
|
|
||||||
origEnd = Math.round(origStart + normalizedLength * ratio)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileContent.substring(origStart, origEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When old_string matched via quote normalization (curly quotes in file,
|
* When old_string matched via quote normalization (curly quotes in file,
|
||||||
* straight quotes from model), apply the same curly quote style to new_string
|
* straight quotes from model), apply the same curly quote style to new_string
|
||||||
|
|||||||
@@ -616,7 +616,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
case 'shutdown_response':
|
case 'shutdown_response':
|
||||||
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
|
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
|
||||||
case 'plan_approval_response':
|
case 'plan_approval_response':
|
||||||
return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${recipient}`
|
const planApprovalDecision = input.message.approve
|
||||||
|
? 'approve'
|
||||||
|
: 'reject'
|
||||||
|
return `plan_approval ${planApprovalDecision} to ${recipient}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -834,10 +837,10 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
const { postInterClaudeMessage } =
|
const { postInterClaudeMessage } =
|
||||||
require('src/bridge/peerSessions.js') as typeof import('src/bridge/peerSessions.js')
|
require('src/bridge/peerSessions.js') as typeof import('src/bridge/peerSessions.js')
|
||||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||||
const result = (await postInterClaudeMessage(
|
const result = await postInterClaudeMessage(
|
||||||
addr.target,
|
addr.target,
|
||||||
input.message,
|
input.message,
|
||||||
)) as { ok: boolean; error?: string }
|
) as { ok: boolean; error?: string }
|
||||||
const preview = input.summary || truncate(input.message, 50)
|
const preview = input.summary || truncate(input.message, 50)
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -849,7 +852,6 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addr.scheme === 'uds') {
|
if (addr.scheme === 'uds') {
|
||||||
const recipient = recipientForDisplay(input.to)
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const { sendToUdsSocket } =
|
const { sendToUdsSocket } =
|
||||||
require('src/utils/udsClient.js') as typeof import('src/utils/udsClient.js')
|
require('src/utils/udsClient.js') as typeof import('src/utils/udsClient.js')
|
||||||
@@ -860,14 +862,14 @@ export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
|
|||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
message: `”${preview}” → ${recipient}`,
|
message: `”${preview}” → ${input.to}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to send to ${recipient}: ${errorMessage(e)}`,
|
message: `Failed to send to ${input.to}: ${errorMessage(e)}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const DEFAULT_BUILD_FEATURES = [
|
|||||||
'DIRECT_CONNECT', // 直连模式(claude server / claude open)
|
'DIRECT_CONNECT', // 直连模式(claude server / claude open)
|
||||||
// Skill search & learning
|
// Skill search & learning
|
||||||
'EXPERIMENTAL_SKILL_SEARCH', // 实验性技能搜索(DiscoverSkills)
|
'EXPERIMENTAL_SKILL_SEARCH', // 实验性技能搜索(DiscoverSkills)
|
||||||
// 'SKILL_LEARNING', // projectContext cache 无淘汰机制(非 GB 级主因)
|
'SKILL_LEARNING', // projectContext cache 无淘汰机制(非 GB 级主因)
|
||||||
// P3: poor mode
|
// P3: poor mode
|
||||||
'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
|
'POOR', // 穷鬼模式,跳过 extract_memories/prompt_suggestion 减少消耗
|
||||||
// Team Memory
|
// Team Memory
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import {
|
|||||||
getOriginalCwd,
|
getOriginalCwd,
|
||||||
getSessionId,
|
getSessionId,
|
||||||
regenerateSessionId,
|
regenerateSessionId,
|
||||||
resetCostState,
|
|
||||||
setLastAPIRequest,
|
|
||||||
setLastAPIRequestMessages,
|
|
||||||
setLastClassifierRequests,
|
|
||||||
} from '../../bootstrap/state.js'
|
} from '../../bootstrap/state.js'
|
||||||
import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js'
|
import type { SDKStatusMessage } from '../../entrypoints/sdk/coreTypes.js'
|
||||||
import {
|
import {
|
||||||
@@ -148,14 +144,6 @@ export async function clearConversation({
|
|||||||
// tracking) is retained so those agents keep functioning.
|
// tracking) is retained so those agents keep functioning.
|
||||||
clearSessionCaches(preservedAgentIds)
|
clearSessionCaches(preservedAgentIds)
|
||||||
|
|
||||||
// Clear large STATE-held data that outlives the message array.
|
|
||||||
// lastAPIRequestMessages can hold the full post-compaction conversation
|
|
||||||
// (hundreds of KB–MB) for /share; resetCostState clears modelUsage.
|
|
||||||
setLastAPIRequest(null)
|
|
||||||
setLastAPIRequestMessages(null)
|
|
||||||
setLastClassifierRequests(null)
|
|
||||||
resetCostState()
|
|
||||||
|
|
||||||
setCwd(getOriginalCwd())
|
setCwd(getOriginalCwd())
|
||||||
readFileState.clear()
|
readFileState.clear()
|
||||||
discoveredSkillNames?.clear()
|
discoveredSkillNames?.clear()
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ export type Props = {
|
|||||||
lastThinkingBlockId?: string | null
|
lastThinkingBlockId?: string | null
|
||||||
/** UUID of the latest user bash output message (for auto-expanding) */
|
/** UUID of the latest user bash output message (for auto-expanding) */
|
||||||
latestBashOutputUUID?: string | null
|
latestBashOutputUUID?: string | null
|
||||||
/** Whether to collapse diff display for this message */
|
|
||||||
shouldCollapseDiffs?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageImpl({
|
function MessageImpl({
|
||||||
@@ -101,7 +99,6 @@ function MessageImpl({
|
|||||||
isUserContinuation = false,
|
isUserContinuation = false,
|
||||||
lastThinkingBlockId,
|
lastThinkingBlockId,
|
||||||
latestBashOutputUUID,
|
latestBashOutputUUID,
|
||||||
shouldCollapseDiffs,
|
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'attachment':
|
case 'attachment':
|
||||||
@@ -184,7 +181,6 @@ function MessageImpl({
|
|||||||
isUserContinuation={isUserContinuation}
|
isUserContinuation={isUserContinuation}
|
||||||
lookups={lookups}
|
lookups={lookups}
|
||||||
isTranscriptMode={isTranscriptMode}
|
isTranscriptMode={isTranscriptMode}
|
||||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -297,7 +293,6 @@ function UserMessage({
|
|||||||
isUserContinuation,
|
isUserContinuation,
|
||||||
lookups,
|
lookups,
|
||||||
isTranscriptMode,
|
isTranscriptMode,
|
||||||
shouldCollapseDiffs,
|
|
||||||
}: {
|
}: {
|
||||||
message: NormalizedUserMessage
|
message: NormalizedUserMessage
|
||||||
addMargin: boolean
|
addMargin: boolean
|
||||||
@@ -314,7 +309,6 @@ function UserMessage({
|
|||||||
isUserContinuation: boolean
|
isUserContinuation: boolean
|
||||||
lookups: ReturnType<typeof buildMessageLookups>
|
lookups: ReturnType<typeof buildMessageLookups>
|
||||||
isTranscriptMode: boolean
|
isTranscriptMode: boolean
|
||||||
shouldCollapseDiffs?: boolean
|
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const { columns } = useTerminalSize()
|
const { columns } = useTerminalSize()
|
||||||
switch (param.type) {
|
switch (param.type) {
|
||||||
@@ -350,7 +344,6 @@ function UserMessage({
|
|||||||
verbose={verbose}
|
verbose={verbose}
|
||||||
width={columns - 5}
|
width={columns - 5}
|
||||||
isTranscriptMode={isTranscriptMode}
|
isTranscriptMode={isTranscriptMode}
|
||||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export type Props = {
|
|||||||
columns: number
|
columns: number
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
lookups: ReturnType<typeof buildMessageLookups>
|
lookups: ReturnType<typeof buildMessageLookups>
|
||||||
shouldCollapseDiffs?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +141,6 @@ function MessageRowImpl({
|
|||||||
columns,
|
columns,
|
||||||
isLoading,
|
isLoading,
|
||||||
lookups,
|
lookups,
|
||||||
shouldCollapseDiffs,
|
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
const isTranscriptMode = screen === 'transcript'
|
const isTranscriptMode = screen === 'transcript'
|
||||||
const isGrouped = msg.type === 'grouped_tool_use'
|
const isGrouped = msg.type === 'grouped_tool_use'
|
||||||
@@ -223,7 +221,6 @@ function MessageRowImpl({
|
|||||||
isUserContinuation={isUserContinuation}
|
isUserContinuation={isUserContinuation}
|
||||||
lastThinkingBlockId={lastThinkingBlockId}
|
lastThinkingBlockId={lastThinkingBlockId}
|
||||||
latestBashOutputUUID={latestBashOutputUUID}
|
latestBashOutputUUID={latestBashOutputUUID}
|
||||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
// OffscreenFreeze: the outer React.memo already bails for static messages,
|
// OffscreenFreeze: the outer React.memo already bails for static messages,
|
||||||
|
|||||||
@@ -814,12 +814,6 @@ const MessagesImpl = ({
|
|||||||
streamingToolUseIDs,
|
streamingToolUseIDs,
|
||||||
))
|
))
|
||||||
|
|
||||||
// Collapse diffs for messages beyond the latest N messages.
|
|
||||||
// verbose (ctrl+o) overrides and always shows full diffs.
|
|
||||||
const DIFF_COLLAPSE_DISTANCE = 0
|
|
||||||
const shouldCollapseDiffs =
|
|
||||||
renderableMessages.length - 1 - index > DIFF_COLLAPSE_DISTANCE
|
|
||||||
|
|
||||||
const k = messageKey(msg)
|
const k = messageKey(msg)
|
||||||
const row = (
|
const row = (
|
||||||
<MessageRow
|
<MessageRow
|
||||||
@@ -844,7 +838,6 @@ const MessagesImpl = ({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
lookups={lookups}
|
lookups={lookups}
|
||||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type Props = {
|
|||||||
verbose: boolean
|
verbose: boolean
|
||||||
width: number | string
|
width: number | string
|
||||||
isTranscriptMode?: boolean
|
isTranscriptMode?: boolean
|
||||||
shouldCollapseDiffs?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserToolResultMessage({
|
export function UserToolResultMessage({
|
||||||
@@ -40,7 +39,6 @@ export function UserToolResultMessage({
|
|||||||
verbose,
|
verbose,
|
||||||
width,
|
width,
|
||||||
isTranscriptMode,
|
isTranscriptMode,
|
||||||
shouldCollapseDiffs,
|
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups)
|
const toolUse = useGetToolFromMessages(param.tool_use_id, tools, lookups)
|
||||||
if (!toolUse) {
|
if (!toolUse) {
|
||||||
@@ -98,7 +96,6 @@ export function UserToolResultMessage({
|
|||||||
verbose={verbose}
|
verbose={verbose}
|
||||||
width={width}
|
width={width}
|
||||||
isTranscriptMode={isTranscriptMode}
|
isTranscriptMode={isTranscriptMode}
|
||||||
shouldCollapseDiffs={shouldCollapseDiffs}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type Props = {
|
|||||||
verbose: boolean
|
verbose: boolean
|
||||||
width: number | string
|
width: number | string
|
||||||
isTranscriptMode?: boolean
|
isTranscriptMode?: boolean
|
||||||
shouldCollapseDiffs?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserToolSuccessMessage({
|
export function UserToolSuccessMessage({
|
||||||
@@ -47,7 +46,6 @@ export function UserToolSuccessMessage({
|
|||||||
verbose,
|
verbose,
|
||||||
width,
|
width,
|
||||||
isTranscriptMode,
|
isTranscriptMode,
|
||||||
shouldCollapseDiffs,
|
|
||||||
}: Props): React.ReactNode {
|
}: Props): React.ReactNode {
|
||||||
const [theme] = useTheme()
|
const [theme] = useTheme()
|
||||||
// Hook stays inside feature() ternary so external builds don't pay a
|
// Hook stays inside feature() ternary so external builds don't pay a
|
||||||
@@ -85,16 +83,12 @@ export function UserToolSuccessMessage({
|
|||||||
}
|
}
|
||||||
const toolResult = parsedOutput?.data ?? message.toolUseResult
|
const toolResult = parsedOutput?.data ?? message.toolUseResult
|
||||||
|
|
||||||
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
|
||||||
const effectiveStyle =
|
|
||||||
shouldCollapseDiffs && !verbose ? 'condensed' : style
|
|
||||||
|
|
||||||
const renderedMessage =
|
const renderedMessage =
|
||||||
tool.renderToolResultMessage?.(
|
tool.renderToolResultMessage?.(
|
||||||
toolResult as never,
|
toolResult as never,
|
||||||
filterToolProgressMessages(progressMessagesForMessage),
|
filterToolProgressMessages(progressMessagesForMessage),
|
||||||
{
|
{
|
||||||
style: effectiveStyle,
|
style,
|
||||||
theme,
|
theme,
|
||||||
tools,
|
tools,
|
||||||
verbose,
|
verbose,
|
||||||
|
|||||||
@@ -6907,9 +6907,6 @@ async function logTenguInit({
|
|||||||
allowDangerouslySkipPermissionsPassed,
|
allowDangerouslySkipPermissionsPassed,
|
||||||
thinkingType:
|
thinkingType:
|
||||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
...(thinkingConfig.type === "enabled" && {
|
|
||||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
|
||||||
}),
|
|
||||||
...(systemPromptFlag && {
|
...(systemPromptFlag && {
|
||||||
systemPromptFlag:
|
systemPromptFlag:
|
||||||
systemPromptFlag as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
systemPromptFlag as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
|
|||||||
@@ -3051,22 +3051,12 @@ export function REPL({
|
|||||||
// are O(n) per render, so drop everything before the previous
|
// are O(n) per render, so drop everything before the previous
|
||||||
// boundary to keep n bounded across multi-day sessions.
|
// boundary to keep n bounded across multi-day sessions.
|
||||||
if (isFullscreenEnvEnabled()) {
|
if (isFullscreenEnvEnabled()) {
|
||||||
setMessages(old => {
|
setMessages(old => [
|
||||||
const postBoundary = getMessagesAfterCompactBoundary(old, {
|
...getMessagesAfterCompactBoundary(old, {
|
||||||
includeSnipped: true,
|
includeSnipped: true,
|
||||||
})
|
}),
|
||||||
// Hard cap: keep at most 500 messages in fullscreen scrollback
|
newMessage,
|
||||||
// to prevent unbounded memory growth in multi-day sessions.
|
]);
|
||||||
// normalizeMessages/applyGrouping are O(n), and Ink fiber
|
|
||||||
// trees cost ~250KB RSS per message. Without this cap,
|
|
||||||
// scrollback after several compactions can reach thousands
|
|
||||||
// of messages (observed: 13k+, 1GB+ heap).
|
|
||||||
const MAX_FULLSCREEN_SCROLLBACK = 500
|
|
||||||
const kept = postBoundary.length > MAX_FULLSCREEN_SCROLLBACK
|
|
||||||
? postBoundary.slice(-MAX_FULLSCREEN_SCROLLBACK)
|
|
||||||
: postBoundary
|
|
||||||
return [...kept, newMessage]
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setMessages(() => [newMessage]);
|
setMessages(() => [newMessage]);
|
||||||
}
|
}
|
||||||
@@ -3092,23 +3082,17 @@ export function REPL({
|
|||||||
// history). Replacing those leaves the AgentTool UI stuck at
|
// history). Replacing those leaves the AgentTool UI stuck at
|
||||||
// "Initializing…" because it renders the full progress trail.
|
// "Initializing…" because it renders the full progress trail.
|
||||||
setMessages(oldMessages => {
|
setMessages(oldMessages => {
|
||||||
|
const last = oldMessages.at(-1);
|
||||||
|
const lastData = last?.data as Record<string, unknown> | undefined;
|
||||||
const newData = newMessage.data as Record<string, unknown>;
|
const newData = newMessage.data as Record<string, unknown>;
|
||||||
// Scan backwards to find the last ephemeral progress with matching
|
if (
|
||||||
// parentToolUseID and type. Previously only checked the last message,
|
last?.type === 'progress' &&
|
||||||
// so interleaved non-ephemeral messages caused duplicate progress
|
last.parentToolUseID === newMessage.parentToolUseID &&
|
||||||
// entries to accumulate (observed 13k+ entries in sleep-heavy sessions).
|
lastData?.type === newData.type
|
||||||
for (let i = oldMessages.length - 1; i >= 0; i--) {
|
) {
|
||||||
const m = oldMessages[i]!
|
const copy = oldMessages.slice();
|
||||||
if (m.type !== 'progress') break
|
copy[copy.length - 1] = newMessage;
|
||||||
const mData = m.data as Record<string, unknown> | undefined
|
return copy;
|
||||||
if (
|
|
||||||
m.parentToolUseID === newMessage.parentToolUseID &&
|
|
||||||
mData?.type === newData.type
|
|
||||||
) {
|
|
||||||
const copy = oldMessages.slice();
|
|
||||||
copy[i] = newMessage;
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return [...oldMessages, newMessage];
|
return [...oldMessages, newMessage];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ describe('startAgentSummarization', () => {
|
|||||||
let debugLogs: string[]
|
let debugLogs: string[]
|
||||||
let loggedErrors: Error[]
|
let loggedErrors: Error[]
|
||||||
let clearedHandles: unknown[]
|
let clearedHandles: unknown[]
|
||||||
let scheduledCount: number
|
|
||||||
let lastTimerHandle: unknown
|
|
||||||
|
|
||||||
function startTestSummarization(
|
function startTestSummarization(
|
||||||
dependencies: AgentSummaryDependencies = {},
|
dependencies: AgentSummaryDependencies = {},
|
||||||
@@ -83,10 +81,8 @@ describe('startAgentSummarization', () => {
|
|||||||
if (typeof callback !== 'function') {
|
if (typeof callback !== 'function') {
|
||||||
throw new Error('Expected timer callback')
|
throw new Error('Expected timer callback')
|
||||||
}
|
}
|
||||||
scheduledCount += 1
|
|
||||||
scheduled = callback as () => void | Promise<void>
|
scheduled = callback as () => void | Promise<void>
|
||||||
lastTimerHandle = { id: scheduledCount }
|
return 1 as unknown as ReturnType<typeof setTimeout>
|
||||||
return lastTimerHandle as ReturnType<typeof setTimeout>
|
|
||||||
}) as unknown as typeof setTimeout,
|
}) as unknown as typeof setTimeout,
|
||||||
updateAgentSummary: (taskId: string, summary: string) => {
|
updateAgentSummary: (taskId: string, summary: string) => {
|
||||||
updateCalls.push({ taskId, summary })
|
updateCalls.push({ taskId, summary })
|
||||||
@@ -105,14 +101,8 @@ describe('startAgentSummarization', () => {
|
|||||||
debugLogs = []
|
debugLogs = []
|
||||||
loggedErrors = []
|
loggedErrors = []
|
||||||
clearedHandles = []
|
clearedHandles = []
|
||||||
scheduledCount = 0
|
|
||||||
lastTimerHandle = undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function expectDebugLogContaining(fragment: string): void {
|
|
||||||
expect(debugLogs.some(message => message.includes(fragment))).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
test('summarizes bounded transcript once and skips unchanged fingerprints', async () => {
|
||||||
handle = startTestSummarization()
|
handle = startTestSummarization()
|
||||||
|
|
||||||
@@ -138,7 +128,6 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toHaveLength(1)
|
expect(forkCalls).toHaveLength(1)
|
||||||
expect(updateCalls).toHaveLength(1)
|
expect(updateCalls).toHaveLength(1)
|
||||||
expect(loggedErrors).toEqual([])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('skips summarization when filtering leaves too little bounded context', async () => {
|
test('skips summarization when filtering leaves too little bounded context', async () => {
|
||||||
@@ -161,7 +150,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining(
|
expect(debugLogs).toContain(
|
||||||
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
'[AgentSummary] Skipping summary for task-1: no bounded context available',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -175,7 +164,7 @@ describe('startAgentSummarization', () => {
|
|||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining(
|
expect(debugLogs).toContain(
|
||||||
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
'[AgentSummary] Skipping summary for task-1: not enough messages (2)',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -186,18 +175,16 @@ describe('startAgentSummarization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(typeof scheduled).toBe('function')
|
expect(typeof scheduled).toBe('function')
|
||||||
const initialScheduledCount = scheduledCount
|
|
||||||
const initialTimerHandle = lastTimerHandle
|
|
||||||
await scheduled!()
|
await scheduled!()
|
||||||
|
|
||||||
expect(forkCalls).toEqual([])
|
expect(forkCalls).toEqual([])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expectDebugLogContaining('[AgentSummary] Skipping summary — poor mode active')
|
expect(debugLogs).toContain(
|
||||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
'[AgentSummary] Skipping summary — poor mode active',
|
||||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('logs summary errors and schedules the next timer', async () => {
|
test('logs summary errors and keeps the next timer owned by the summarizer', async () => {
|
||||||
const error = new Error('fork failed')
|
const error = new Error('fork failed')
|
||||||
handle = startTestSummarization({
|
handle = startTestSummarization({
|
||||||
runForkedAgent: async () => {
|
runForkedAgent: async () => {
|
||||||
@@ -206,23 +193,20 @@ describe('startAgentSummarization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(typeof scheduled).toBe('function')
|
expect(typeof scheduled).toBe('function')
|
||||||
const initialScheduledCount = scheduledCount
|
|
||||||
const initialTimerHandle = lastTimerHandle
|
|
||||||
await scheduled!()
|
await scheduled!()
|
||||||
|
|
||||||
expect(loggedErrors).toEqual([error])
|
expect(loggedErrors).toEqual([error])
|
||||||
expect(updateCalls).toEqual([])
|
expect(updateCalls).toEqual([])
|
||||||
expect(scheduledCount).toBe(initialScheduledCount + 1)
|
|
||||||
expect(lastTimerHandle).not.toBe(initialTimerHandle)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('stop clears the pending summary timer', () => {
|
test('stop clears the pending summary timer', () => {
|
||||||
handle = startTestSummarization()
|
handle = startTestSummarization()
|
||||||
const pendingHandle = lastTimerHandle
|
|
||||||
|
|
||||||
handle.stop()
|
handle.stop()
|
||||||
|
|
||||||
expectDebugLogContaining('[AgentSummary] Stopping summarization for task-1')
|
expect(debugLogs).toContain(
|
||||||
expect(clearedHandles).toEqual([pendingHandle])
|
'[AgentSummary] Stopping summarization for task-1',
|
||||||
|
)
|
||||||
|
expect(clearedHandles).toEqual([1])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1776,10 +1776,6 @@ async function* queryModel(
|
|||||||
// captures only primitives instead of paramsFromContext's full closure scope
|
// captures only primitives instead of paramsFromContext's full closure scope
|
||||||
// (messagesForAPI, system, allTools, betas — the entire request-building
|
// (messagesForAPI, system, allTools, betas — the entire request-building
|
||||||
// context), which would otherwise be pinned until the promise resolves.
|
// context), which would otherwise be pinned until the promise resolves.
|
||||||
// Also capture thinking params for Langfuse observability.
|
|
||||||
// Pass the entire thinking config object so all fields (type, budget_tokens,
|
|
||||||
// and any future additions) flow through without cherry-picking.
|
|
||||||
let langfuseThinking: BetaMessageStreamParams['thinking'] | undefined
|
|
||||||
{
|
{
|
||||||
const queryParams = paramsFromContext({
|
const queryParams = paramsFromContext({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
@@ -1787,10 +1783,8 @@ async function* queryModel(
|
|||||||
})
|
})
|
||||||
const logMessagesLength = queryParams.messages.length
|
const logMessagesLength = queryParams.messages.length
|
||||||
const logBetas = useBetas ? (queryParams.betas ?? []) : []
|
const logBetas = useBetas ? (queryParams.betas ?? []) : []
|
||||||
|
const logThinkingType = queryParams.thinking?.type ?? 'disabled'
|
||||||
const logEffortValue = queryParams.output_config?.effort
|
const logEffortValue = queryParams.output_config?.effort
|
||||||
if (queryParams.thinking && queryParams.thinking.type !== 'disabled') {
|
|
||||||
langfuseThinking = queryParams.thinking
|
|
||||||
}
|
|
||||||
void options.getToolPermissionContext().then(permissionContext => {
|
void options.getToolPermissionContext().then(permissionContext => {
|
||||||
logAPIQuery({
|
logAPIQuery({
|
||||||
model: options.model,
|
model: options.model,
|
||||||
@@ -1800,7 +1794,7 @@ async function* queryModel(
|
|||||||
permissionMode: permissionContext.mode,
|
permissionMode: permissionContext.mode,
|
||||||
querySource: options.querySource,
|
querySource: options.querySource,
|
||||||
queryTracking: options.queryTracking,
|
queryTracking: options.queryTracking,
|
||||||
thinkingConfig,
|
thinkingType: logThinkingType,
|
||||||
effortValue: logEffortValue,
|
effortValue: logEffortValue,
|
||||||
fastMode: isFastMode,
|
fastMode: isFastMode,
|
||||||
previousRequestId,
|
previousRequestId,
|
||||||
@@ -2551,9 +2545,6 @@ async function* queryModel(
|
|||||||
maxOutputTokens,
|
maxOutputTokens,
|
||||||
thinkingType:
|
thinkingType:
|
||||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
...(thinkingConfig.type === 'enabled' && {
|
|
||||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
|
||||||
}),
|
|
||||||
fallback_disabled: true,
|
fallback_disabled: true,
|
||||||
request_id: (streamRequestId ??
|
request_id: (streamRequestId ??
|
||||||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
@@ -2586,9 +2577,6 @@ async function* queryModel(
|
|||||||
maxOutputTokens,
|
maxOutputTokens,
|
||||||
thinkingType:
|
thinkingType:
|
||||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
...(thinkingConfig.type === 'enabled' && {
|
|
||||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
|
||||||
}),
|
|
||||||
fallback_disabled: false,
|
fallback_disabled: false,
|
||||||
request_id: (streamRequestId ??
|
request_id: (streamRequestId ??
|
||||||
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
@@ -2705,9 +2693,6 @@ async function* queryModel(
|
|||||||
maxOutputTokens,
|
maxOutputTokens,
|
||||||
thinkingType:
|
thinkingType:
|
||||||
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
thinkingConfig.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
...(thinkingConfig.type === 'enabled' && {
|
|
||||||
thinkingBudgetTokens: thinkingConfig.budgetTokens,
|
|
||||||
}),
|
|
||||||
request_id:
|
request_id:
|
||||||
failedRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
failedRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
fallback_cause:
|
fallback_cause:
|
||||||
@@ -2940,7 +2925,6 @@ async function* queryModel(
|
|||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||||
thinking: langfuseThinking,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
void options.getToolPermissionContext().then(permissionContext => {
|
void options.getToolPermissionContext().then(permissionContext => {
|
||||||
|
|||||||
@@ -193,15 +193,6 @@ export async function* queryModelGemini(
|
|||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||||
thinking:
|
|
||||||
thinkingConfig.type !== 'disabled'
|
|
||||||
? {
|
|
||||||
type: thinkingConfig.type,
|
|
||||||
...(thinkingConfig.type === 'enabled' && {
|
|
||||||
budgetTokens: thinkingConfig.budgetTokens,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { getAPIProviderForStatsig } from 'src/utils/model/providers.js'
|
|||||||
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
|
import type { PermissionMode } from 'src/utils/permissions/PermissionMode.js'
|
||||||
import { jsonStringify } from 'src/utils/slowOperations.js'
|
import { jsonStringify } from 'src/utils/slowOperations.js'
|
||||||
import { logOTelEvent } from 'src/utils/telemetry/events.js'
|
import { logOTelEvent } from 'src/utils/telemetry/events.js'
|
||||||
import type { ThinkingConfig } from 'src/utils/thinking.js'
|
|
||||||
import {
|
import {
|
||||||
endLLMRequestSpan,
|
endLLMRequestSpan,
|
||||||
isBetaTracingEnabled,
|
isBetaTracingEnabled,
|
||||||
@@ -177,7 +176,7 @@ export function logAPIQuery({
|
|||||||
permissionMode,
|
permissionMode,
|
||||||
querySource,
|
querySource,
|
||||||
queryTracking,
|
queryTracking,
|
||||||
thinkingConfig,
|
thinkingType,
|
||||||
effortValue,
|
effortValue,
|
||||||
fastMode,
|
fastMode,
|
||||||
previousRequestId,
|
previousRequestId,
|
||||||
@@ -189,13 +188,11 @@ export function logAPIQuery({
|
|||||||
permissionMode?: PermissionMode
|
permissionMode?: PermissionMode
|
||||||
querySource: string
|
querySource: string
|
||||||
queryTracking?: QueryChainTracking
|
queryTracking?: QueryChainTracking
|
||||||
thinkingConfig?: ThinkingConfig
|
thinkingType?: 'adaptive' | 'enabled' | 'disabled'
|
||||||
effortValue?: EffortLevel | null
|
effortValue?: EffortLevel | null
|
||||||
fastMode?: boolean
|
fastMode?: boolean
|
||||||
previousRequestId?: string | null
|
previousRequestId?: string | null
|
||||||
}): void {
|
}): void {
|
||||||
const thinkingType = thinkingConfig?.type ?? 'disabled'
|
|
||||||
const thinkingBudgetTokens = thinkingConfig?.type === 'enabled' ? thinkingConfig.budgetTokens : undefined
|
|
||||||
logEvent('tengu_api_query', {
|
logEvent('tengu_api_query', {
|
||||||
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
messagesLength,
|
messagesLength,
|
||||||
@@ -222,9 +219,6 @@ export function logAPIQuery({
|
|||||||
: {}),
|
: {}),
|
||||||
thinkingType:
|
thinkingType:
|
||||||
thinkingType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
thinkingType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
...(thinkingBudgetTokens !== undefined && {
|
|
||||||
thinkingBudgetTokens,
|
|
||||||
}),
|
|
||||||
effortValue:
|
effortValue:
|
||||||
effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||||
fastMode,
|
fastMode,
|
||||||
|
|||||||
@@ -418,7 +418,6 @@ export async function* queryModelOpenAI(
|
|||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
completionStartTime: ttftMs > 0 ? new Date(start + ttftMs) : undefined,
|
||||||
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
tools: convertToolsToLangfuse(toolSchemas as unknown[]),
|
||||||
...(enableThinking && { thinking: { type: 'enabled' } }),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Safety: if stream ended without message_stop, assemble and yield whatever we have
|
// Safety: if stream ended without message_stop, assemble and yield whatever we have
|
||||||
|
|||||||
@@ -78,16 +78,6 @@ export function recordLLMObservation(
|
|||||||
endTime?: Date
|
endTime?: Date
|
||||||
completionStartTime?: Date
|
completionStartTime?: Date
|
||||||
tools?: unknown
|
tools?: unknown
|
||||||
/** Thinking depth configuration used for this request.
|
|
||||||
* Accepts the full API thinking config object. Fields:
|
|
||||||
* - type: thinking mode ("enabled", "adaptive", "disabled")
|
|
||||||
* - budget_tokens (snake_case, from Anthropic API) or budgetTokens (camelCase)
|
|
||||||
*/
|
|
||||||
thinking?: {
|
|
||||||
type: string
|
|
||||||
budget_tokens?: number
|
|
||||||
budgetTokens?: number
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
if (!rootSpan || !isLangfuseEnabled()) return
|
if (!rootSpan || !isLangfuseEnabled()) return
|
||||||
@@ -107,7 +97,6 @@ export function recordLLMObservation(
|
|||||||
metadata: {
|
metadata: {
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
...(params.thinking && { thinking: params.thinking }),
|
|
||||||
},
|
},
|
||||||
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
|
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ function buildAgentContent(params: {
|
|||||||
'',
|
'',
|
||||||
instincts
|
instincts
|
||||||
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
||||||
.slice(0, 20)
|
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
@@ -35,18 +35,15 @@ export function createInstinct(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_EVIDENCE_ENTRIES = 10
|
|
||||||
|
|
||||||
export function normalizeInstinct(instinct: StoredInstinct): StoredInstinct {
|
export function normalizeInstinct(instinct: StoredInstinct): StoredInstinct {
|
||||||
const uniqueEvidence = Array.from(new Set(instinct.evidence.filter(Boolean)))
|
|
||||||
return {
|
return {
|
||||||
...instinct,
|
...instinct,
|
||||||
id: instinct.id || buildInstinctId(instinct.trigger, instinct.action),
|
id: instinct.id || buildInstinctId(instinct.trigger, instinct.action),
|
||||||
confidence: clampConfidence(instinct.confidence),
|
confidence: clampConfidence(instinct.confidence),
|
||||||
evidence: uniqueEvidence.slice(-MAX_EVIDENCE_ENTRIES),
|
evidence: Array.from(new Set(instinct.evidence.filter(Boolean))),
|
||||||
evidenceOutcome: instinct.evidenceOutcome,
|
evidenceOutcome: instinct.evidenceOutcome,
|
||||||
observationIds: instinct.observationIds
|
observationIds: instinct.observationIds
|
||||||
? Array.from(new Set(instinct.observationIds)).slice(-20)
|
? Array.from(new Set(instinct.observationIds))
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ import {
|
|||||||
import type { LearnedSkillDraft, SkillLearningScope } from './types.js'
|
import type { LearnedSkillDraft, SkillLearningScope } from './types.js'
|
||||||
|
|
||||||
export const DUPLICATE_SKILL_OVERLAP_THRESHOLD = 0.8
|
export const DUPLICATE_SKILL_OVERLAP_THRESHOLD = 0.8
|
||||||
const MAX_EVIDENCE_LINES_PER_APPEND = 20
|
|
||||||
const MAX_EVIDENCE_LINES_IN_SKILL = 20
|
|
||||||
const MAX_SKILL_FILE_BYTES = 50_000
|
|
||||||
|
|
||||||
export type SkillGeneratorOptions = {
|
export type SkillGeneratorOptions = {
|
||||||
cwd?: string
|
cwd?: string
|
||||||
@@ -104,41 +101,20 @@ export async function appendInstinctEvidenceToSkill(
|
|||||||
const existing = await readFile(target.path, 'utf8').catch(
|
const existing = await readFile(target.path, 'utf8').catch(
|
||||||
() => target.content,
|
() => target.content,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Skip if the file already exceeds the size cap
|
|
||||||
if (Buffer.byteLength(existing, 'utf8') >= MAX_SKILL_FILE_BYTES) {
|
|
||||||
return target.path
|
|
||||||
}
|
|
||||||
|
|
||||||
const allEvidence = instincts.flatMap(instinct =>
|
|
||||||
instinct.evidence.map(evidence => `- ${evidence}`),
|
|
||||||
)
|
|
||||||
const evidenceLines = allEvidence.slice(0, MAX_EVIDENCE_LINES_PER_APPEND)
|
|
||||||
if (evidenceLines.length < allEvidence.length) {
|
|
||||||
evidenceLines.push(
|
|
||||||
`- [... ${allEvidence.length - evidenceLines.length} more evidence entries omitted]`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const block = [
|
const block = [
|
||||||
'',
|
'',
|
||||||
`## Learned evidence (${now})`,
|
`## Learned evidence (${now})`,
|
||||||
'',
|
'',
|
||||||
...evidenceLines,
|
...instincts.flatMap(instinct =>
|
||||||
|
instinct.evidence.map(evidence => `- ${evidence}`),
|
||||||
|
),
|
||||||
'',
|
'',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const merged = existing.endsWith('\n')
|
const merged = existing.endsWith('\n')
|
||||||
? existing + block
|
? existing + block
|
||||||
: `${existing}\n${block}`
|
: `${existing}\n${block}`
|
||||||
|
await writeFile(target.path, merged, 'utf8')
|
||||||
// Final guard: truncate if merged exceeds size cap
|
|
||||||
const finalContent =
|
|
||||||
Buffer.byteLength(merged, 'utf8') > MAX_SKILL_FILE_BYTES
|
|
||||||
? merged.slice(0, MAX_SKILL_FILE_BYTES)
|
|
||||||
: merged
|
|
||||||
|
|
||||||
await writeFile(target.path, finalContent, 'utf8')
|
|
||||||
clearSkillIndexCache()
|
clearSkillIndexCache()
|
||||||
return target.path
|
return target.path
|
||||||
}
|
}
|
||||||
@@ -215,7 +191,6 @@ function buildSkillContent(params: {
|
|||||||
'',
|
'',
|
||||||
instincts
|
instincts
|
||||||
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
.flatMap(instinct => instinct.evidence.map(evidence => `- ${evidence}`))
|
||||||
.slice(0, MAX_EVIDENCE_LINES_IN_SKILL)
|
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
'',
|
'',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -354,7 +354,6 @@ export async function countTokensViaHaikuFallback(
|
|||||||
},
|
},
|
||||||
startTime: new Date(apiStart),
|
startTime: new Date(apiStart),
|
||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
...(containsThinking && { thinking: { type: 'enabled', budgetTokens: TOKEN_COUNT_THINKING_BUDGET } }),
|
|
||||||
})
|
})
|
||||||
endTrace(langfuseTrace)
|
endTrace(langfuseTrace)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||||
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
import { mkdtempSync } from 'node:fs'
|
import { mkdtempSync } from 'node:fs'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import type { Message } from 'src/types/message.js'
|
import type { Message } from 'src/types/message.js'
|
||||||
import { getErrnoCode } from 'src/utils/errors.js'
|
|
||||||
import {
|
import {
|
||||||
compactMailboxMessages,
|
compactMailboxMessages,
|
||||||
getLastPeerDmSummary,
|
getLastPeerDmSummary,
|
||||||
@@ -347,30 +346,17 @@ describe('teammate mailbox retention', () => {
|
|||||||
const inboxPath = getInboxPath('worker', 'alpha')
|
const inboxPath = getInboxPath('worker', 'alpha')
|
||||||
await mkdir(inboxPath, { recursive: true })
|
await mkdir(inboxPath, { recursive: true })
|
||||||
|
|
||||||
const error = await writeToMailbox(
|
await expect(
|
||||||
'worker',
|
writeToMailbox(
|
||||||
{
|
'worker',
|
||||||
from: 'team-lead',
|
{
|
||||||
text: 'new',
|
from: 'team-lead',
|
||||||
timestamp: new Date(5).toISOString(),
|
text: 'new',
|
||||||
},
|
timestamp: new Date(5).toISOString(),
|
||||||
'alpha',
|
},
|
||||||
).then(
|
'alpha',
|
||||||
() => undefined,
|
),
|
||||||
err => err,
|
).rejects.toThrow()
|
||||||
)
|
|
||||||
|
|
||||||
const code = getErrnoCode(error)
|
|
||||||
expect(code).toBeDefined()
|
|
||||||
if (code === undefined) {
|
|
||||||
throw new Error('Expected filesystem errno code')
|
|
||||||
}
|
|
||||||
const expectedCodes =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? ['EISDIR', 'EPERM', 'EACCES']
|
|
||||||
: ['EISDIR']
|
|
||||||
expect(expectedCodes).toContain(code)
|
|
||||||
expect((await stat(inboxPath)).isDirectory()).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
test('readMailbox fails closed on corrupt mailbox content', async () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
writeFile,
|
writeFile,
|
||||||
} from 'node:fs/promises'
|
} from 'node:fs/promises'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { createConnection, createServer, type Socket } from 'node:net'
|
import { createConnection, createServer } from 'node:net'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import {
|
import {
|
||||||
@@ -227,147 +227,11 @@ describe('UDS inbox retention', () => {
|
|||||||
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
)
|
)
|
||||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
const { sendToUdsSocket } = await import('../udsClient.js')
|
||||||
'../udsClient.js'
|
|
||||||
|
await expect(sendToUdsSocket(path, 'hello')).rejects.toThrow(
|
||||||
|
'Failed to connect to peer',
|
||||||
)
|
)
|
||||||
|
|
||||||
const error = await sendToUdsSocket(path, 'hello').then(
|
|
||||||
() => undefined,
|
|
||||||
err => err,
|
|
||||||
)
|
|
||||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
|
||||||
if (!(error instanceof UdsPeerConnectionError)) {
|
|
||||||
throw new Error('Expected UDS peer connection error')
|
|
||||||
}
|
|
||||||
expect(error.socketPath).toBe(path)
|
|
||||||
expect(error.message).not.toContain('test-token')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('udsClient send reports response timeouts as peer connection errors', async () => {
|
|
||||||
const path = socketPath('uds-client-timeout')
|
|
||||||
const capabilityDir = join(tempConfigDir, 'messaging-capabilities')
|
|
||||||
const capabilityName = `${createHash('sha256').update(path).digest('hex')}.json`
|
|
||||||
await mkdir(capabilityDir, { recursive: true, mode: 0o700 })
|
|
||||||
await writeFile(
|
|
||||||
join(capabilityDir, capabilityName),
|
|
||||||
JSON.stringify({ socketPath: path, authToken: 'test-token' }),
|
|
||||||
'utf-8',
|
|
||||||
)
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await mkdir(dirname(path), { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const sockets = new Set<Socket>()
|
|
||||||
const receiver = createServer(socket => {
|
|
||||||
sockets.add(socket)
|
|
||||||
socket.on('close', () => {
|
|
||||||
sockets.delete(socket)
|
|
||||||
})
|
|
||||||
socket.on('data', () => undefined)
|
|
||||||
})
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
receiver.on('error', reject)
|
|
||||||
receiver.listen(path, () => resolve())
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { sendToUdsSocket, UdsPeerConnectionError } = await import(
|
|
||||||
'../udsClient.js'
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = await sendToUdsSocket(path, 'hello', 200).then(
|
|
||||||
() => undefined,
|
|
||||||
err => err,
|
|
||||||
)
|
|
||||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
|
||||||
if (!(error instanceof UdsPeerConnectionError)) {
|
|
||||||
throw new Error('Expected UDS peer connection timeout error')
|
|
||||||
}
|
|
||||||
expect(error.socketPath).toBe(path)
|
|
||||||
expect(error.cause).toBeInstanceOf(Error)
|
|
||||||
if (!(error.cause instanceof Error)) {
|
|
||||||
throw new Error('Expected timeout cause')
|
|
||||||
}
|
|
||||||
expect(error.cause.message).toBe('Connection timed out')
|
|
||||||
expect(error.message).not.toContain('test-token')
|
|
||||||
} finally {
|
|
||||||
for (const socket of sockets) {
|
|
||||||
socket.destroy()
|
|
||||||
}
|
|
||||||
await closeServer(receiver)
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await unlink(path).catch(() => undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('connectToPeer reports connection failures as peer connection errors', async () => {
|
|
||||||
const path = socketPath('uds-connect-error')
|
|
||||||
const { connectToPeer, UdsPeerConnectionError } = await import(
|
|
||||||
'../udsClient.js'
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = await connectToPeer(path, () => {
|
|
||||||
throw new Error('Unexpected post-connect socket error')
|
|
||||||
}).then(
|
|
||||||
() => undefined,
|
|
||||||
err => err,
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(UdsPeerConnectionError)
|
|
||||||
if (!(error instanceof UdsPeerConnectionError)) {
|
|
||||||
throw new Error('Expected UDS peer connection error')
|
|
||||||
}
|
|
||||||
expect(error.socketPath).toBe(path)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('connectToPeer leaves connected socket lifecycle to the caller', async () => {
|
|
||||||
const path = socketPath('uds-connect-lifecycle')
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await mkdir(dirname(path), { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const sockets = new Set<Socket>()
|
|
||||||
const receiver = createServer(socket => {
|
|
||||||
sockets.add(socket)
|
|
||||||
socket.on('close', () => {
|
|
||||||
sockets.delete(socket)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
receiver.on('error', reject)
|
|
||||||
receiver.listen(path, () => resolve())
|
|
||||||
})
|
|
||||||
|
|
||||||
let client: Socket | undefined
|
|
||||||
const socketErrors: Error[] = []
|
|
||||||
try {
|
|
||||||
const { connectToPeer } = await import('../udsClient.js')
|
|
||||||
client = await connectToPeer(
|
|
||||||
path,
|
|
||||||
error => {
|
|
||||||
socketErrors.push(error)
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
expect(client.destroyed).toBe(false)
|
|
||||||
expect(client.listenerCount('error')).toBe(1)
|
|
||||||
|
|
||||||
const socketError = new Error('post-connect failure')
|
|
||||||
client.emit('error', socketError)
|
|
||||||
expect(socketErrors).toEqual([socketError])
|
|
||||||
} finally {
|
|
||||||
client?.destroy()
|
|
||||||
for (const socket of sockets) {
|
|
||||||
socket.destroy()
|
|
||||||
}
|
|
||||||
await closeServer(receiver)
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
await unlink(path).catch(() => undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
test('sendUdsMessage fails closed before connecting without an auth token', async () => {
|
||||||
|
|||||||
@@ -294,12 +294,6 @@ export async function sideQuery(opts: SideQueryOptions): Promise<BetaMessage> {
|
|||||||
startTime: new Date(start),
|
startTime: new Date(start),
|
||||||
endTime: new Date(),
|
endTime: new Date(),
|
||||||
...(tools && { tools: convertToolsToLangfuse(tools as unknown[]) }),
|
...(tools && { tools: convertToolsToLangfuse(tools as unknown[]) }),
|
||||||
...(thinkingConfig && thinkingConfig.type !== 'disabled' && {
|
|
||||||
thinking: {
|
|
||||||
type: thinkingConfig.type,
|
|
||||||
...(thinkingConfig.type === 'enabled' && { budgetTokens: thinkingConfig.budget_tokens }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
endTrace(langfuseTrace)
|
endTrace(langfuseTrace)
|
||||||
|
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ export type PeerSession = {
|
|||||||
alive: boolean
|
alive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UdsPeerConnectionError extends Error {
|
|
||||||
readonly socketPath: string
|
|
||||||
|
|
||||||
constructor(socketPath: string, cause: unknown) {
|
|
||||||
super(
|
|
||||||
`Failed to connect to peer at ${socketPath}: ${errorMessage(cause)}`,
|
|
||||||
{ cause },
|
|
||||||
)
|
|
||||||
this.name = 'UdsPeerConnectionError'
|
|
||||||
this.socketPath = socketPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Session directory
|
// Session directory
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -206,7 +193,6 @@ export async function isPeerAlive(
|
|||||||
export async function sendToUdsSocket(
|
export async function sendToUdsSocket(
|
||||||
targetSocketPath: string,
|
targetSocketPath: string,
|
||||||
message: string | Record<string, unknown>,
|
message: string | Record<string, unknown>,
|
||||||
timeoutMs = 5000,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { parseUdsTarget } = await import('./udsMessaging.js')
|
const { parseUdsTarget } = await import('./udsMessaging.js')
|
||||||
const target = parseUdsTarget(targetSocketPath)
|
const target = parseUdsTarget(targetSocketPath)
|
||||||
@@ -251,63 +237,29 @@ export async function sendToUdsSocket(
|
|||||||
maxFrameBytes: MAX_UDS_FRAME_BYTES,
|
maxFrameBytes: MAX_UDS_FRAME_BYTES,
|
||||||
onSettled: finish,
|
onSettled: finish,
|
||||||
formatSocketError: err =>
|
formatSocketError: err =>
|
||||||
new UdsPeerConnectionError(target.socketPath, err),
|
new Error(
|
||||||
})
|
`Failed to connect to peer at ${target.socketPath}: ${errorMessage(err)}`,
|
||||||
conn.setTimeout(timeoutMs, () => {
|
|
||||||
finish(
|
|
||||||
new UdsPeerConnectionError(
|
|
||||||
target.socketPath,
|
|
||||||
new Error('Connection timed out'),
|
|
||||||
),
|
),
|
||||||
)
|
})
|
||||||
|
conn.setTimeout(5000, () => {
|
||||||
|
finish(new Error('Connection timed out'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a peer and return the raw socket for bidirectional communication.
|
* Connect to a peer and return the raw socket for bidirectional communication.
|
||||||
* The caller owns the post-connect lifecycle through onSocketError, which is
|
* The caller is responsible for managing the connection lifecycle.
|
||||||
* attached before the Promise resolves so peer socket errors cannot be
|
|
||||||
* swallowed or surface through a listener handoff window.
|
|
||||||
* Pre-connect failures reject with UdsPeerConnectionError.
|
|
||||||
* This only opens the transport; callers still own any capability handshake.
|
|
||||||
*/
|
*/
|
||||||
export function connectToPeer(
|
export function connectToPeer(socketPath: string): Promise<Socket> {
|
||||||
socketPath: string,
|
|
||||||
onSocketError: (error: Error) => void,
|
|
||||||
timeoutMs = 5000,
|
|
||||||
): Promise<Socket> {
|
|
||||||
return new Promise<Socket>((resolve, reject) => {
|
return new Promise<Socket>((resolve, reject) => {
|
||||||
const conn = createConnection(socketPath)
|
const conn = createConnection(socketPath, () => {
|
||||||
let settled = false
|
|
||||||
const timeout = setTimeout(
|
|
||||||
fail,
|
|
||||||
timeoutMs,
|
|
||||||
new Error('Connection timed out'),
|
|
||||||
)
|
|
||||||
function cleanupListeners(): void {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
conn.off('error', fail)
|
|
||||||
}
|
|
||||||
function fail(cause: unknown): void {
|
|
||||||
if (settled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
settled = true
|
|
||||||
cleanupListeners()
|
|
||||||
conn.destroy()
|
|
||||||
reject(new UdsPeerConnectionError(socketPath, cause))
|
|
||||||
}
|
|
||||||
conn.once('connect', () => {
|
|
||||||
if (settled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
settled = true
|
|
||||||
cleanupListeners()
|
|
||||||
conn.on('error', onSocketError)
|
|
||||||
resolve(conn)
|
resolve(conn)
|
||||||
})
|
})
|
||||||
conn.on('error', fail)
|
conn.on('error', reject)
|
||||||
|
conn.setTimeout(5000, () => {
|
||||||
|
conn.destroy(new Error('Connection timed out'))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -557,26 +557,7 @@ export async function startUdsMessaging(
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
// Restrict socket permissions to owner-only. On macOS with
|
await chmod(path, 0o600)
|
||||||
// Node.js v22, the listen callback may fire before the socket
|
|
||||||
// file is visible on disk (observed with nested tmpdir paths).
|
|
||||||
// The parent directory is already 0o700, so skipping chmod when
|
|
||||||
// the file is not yet visible is safe.
|
|
||||||
try {
|
|
||||||
await chmod(path, 0o600)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (
|
|
||||||
!(
|
|
||||||
err instanceof Error &&
|
|
||||||
(err as NodeJS.ErrnoException).code === 'ENOENT'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
logForDebugging(
|
|
||||||
`[udsMessaging] chmod skipped: socket file not yet visible at ${path}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
srv.off('error', rejectBeforeListen)
|
srv.off('error', rejectBeforeListen)
|
||||||
srv.on('error', logRuntimeError)
|
srv.on('error', logRuntimeError)
|
||||||
|
|||||||
Reference in New Issue
Block a user