From eb21261a4f1ff050bc9bc30c933a0f7d7136f75f Mon Sep 17 00:00:00 2001 From: yunpeng7 Date: Mon, 6 Jul 2026 17:36:38 +0800 Subject: [PATCH 1/5] fix(executor): extend Claude Code timeout to 24h --- docs/en/user-guide/ai-devices/local-device-support.md | 4 ++-- docs/zh/user-guide/ai-devices/local-device-support.md | 4 ++-- executor/src/agents/mod.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/user-guide/ai-devices/local-device-support.md b/docs/en/user-guide/ai-devices/local-device-support.md index 2d1c294230..d9227abc05 100644 --- a/docs/en/user-guide/ai-devices/local-device-support.md +++ b/docs/en/user-guide/ai-devices/local-device-support.md @@ -214,10 +214,10 @@ The installer and first startup create `~/.wegent-executor/device-config.json`. #### Claude Code Execution Timeout -When the local executor starts a Claude Code child process, it waits up to 1 hour by default. Long-running code generation, dependency installation, or file processing tasks can continue within that window. To tune the limit for a specific environment, set `WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS` before starting the executor. This setting only affects Claude Code child processes, not the native Codex app-server path; Codex RPC timeouts are controlled by `WEGENT_CODEX_RPC_TIMEOUT_SECONDS`. +When the local executor starts a Claude Code child process, it waits up to 24 hours by default. Long-running code generation, dependency installation, or file processing tasks can continue within that window. To tune the limit for a specific environment, set `WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS` before starting the executor. This setting only affects Claude Code child processes, not the native Codex app-server path; Codex RPC timeouts are controlled by `WEGENT_CODEX_RPC_TIMEOUT_SECONDS`. ```bash -export WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS=7200 +export WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS=172800 wegent-executor ``` diff --git a/docs/zh/user-guide/ai-devices/local-device-support.md b/docs/zh/user-guide/ai-devices/local-device-support.md index 4205140236..e6243ba2eb 100644 --- a/docs/zh/user-guide/ai-devices/local-device-support.md +++ b/docs/zh/user-guide/ai-devices/local-device-support.md @@ -216,10 +216,10 @@ wegent-executor #### Claude Code 执行超时 -本地 executor 启动 Claude Code 子进程时,默认最长等待 1 小时。长时间代码生成、依赖安装或文件处理任务可以在这个时间内继续运行。若需要为特定环境调整该限制,可在启动 executor 前设置 `WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS`。该配置只影响 Claude Code 子进程,不影响 native Codex app-server;Codex RPC 超时由 `WEGENT_CODEX_RPC_TIMEOUT_SECONDS` 控制。 +本地 executor 启动 Claude Code 子进程时,默认最长等待 24 小时。长时间代码生成、依赖安装或文件处理任务可以在这个时间内继续运行。若需要为特定环境调整该限制,可在启动 executor 前设置 `WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS`。该配置只影响 Claude Code 子进程,不影响 native Codex app-server;Codex RPC 超时由 `WEGENT_CODEX_RPC_TIMEOUT_SECONDS` 控制。 ```bash -export WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS=7200 +export WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS=172800 wegent-executor ``` diff --git a/executor/src/agents/mod.rs b/executor/src/agents/mod.rs index 3f8fe53a2c..4d18deca05 100644 --- a/executor/src/agents/mod.rs +++ b/executor/src/agents/mod.rs @@ -43,7 +43,7 @@ pub use codex::{ pub use dify::{build_dify_config, saved_dify_task_id, DifyEngine}; pub use image_validator::ImageValidatorEngine; -const DEFAULT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS: u64 = 3600; +const DEFAULT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS: u64 = 24 * 60 * 60; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentCommandPlanner { @@ -353,6 +353,6 @@ mod tests { let _old_timeout = EnvGuard::set("WEGENT_EXECUTOR_PROCESS_TIMEOUT_SECONDS", "1"); let _timeout = EnvGuard::remove("WEGENT_CLAUDE_CODE_PROCESS_TIMEOUT_SECONDS"); - assert_eq!(claude_code_process_timeout_seconds(), 3600); + assert_eq!(claude_code_process_timeout_seconds(), 86_400); } } From 646b39c842ef91e36cfd6409e1ab39113c1ca8fb Mon Sep 17 00:00:00 2001 From: yunpeng7 Date: Mon, 6 Jul 2026 20:54:33 +0800 Subject: [PATCH 2/5] fix(wework): improve runtime search responsiveness --- executor/src/runtime_work/handler.rs | 89 +++++++++++++++++- .../layout/WorkbenchSearchDialog.test.tsx | 94 ++++++++++++++++++- .../layout/WorkbenchSearchDialog.tsx | 43 ++++++++- 3 files changed, 219 insertions(+), 7 deletions(-) diff --git a/executor/src/runtime_work/handler.rs b/executor/src/runtime_work/handler.rs index 5a6c8909f5..a89f01a696 100644 --- a/executor/src/runtime_work/handler.rs +++ b/executor/src/runtime_work/handler.rs @@ -66,6 +66,8 @@ const PENDING_THREAD_EVENT_ROUTE_PREFIX: &str = "pending:"; const ACTIVE_CODEX_TURN_WAIT_ATTEMPTS: usize = 20; const ACTIVE_CODEX_TURN_WAIT_MS: u64 = 50; const TRANSCRIPT_NAVIGATION_PREVIEW_CHARS: usize = 96; +const SEARCH_SNIPPET_CONTEXT_CHARS: usize = 80; +const SEARCH_SNIPPET_MAX_CHARS: usize = 240; const CODEX_OFFICIAL_PROVIDER_ID: &str = "openai"; const CODEX_OFFICIAL_PROVIDER_NAME: &str = "CodeX"; @@ -3220,13 +3222,14 @@ fn first_message_search_result( let Some((match_start, match_end)) = text_match(&content, query) else { continue; }; + let snippet = bounded_search_snippet(&content, match_start, match_end); return Some(search_result_item( link, device_id, SearchResultMatch { - snippet: content, - match_start, - match_end, + snippet: snippet.text, + match_start: snippet.match_start, + match_end: snippet.match_end, message_id: string_field(&message, "id").unwrap_or_default(), message_role: string_field(&message, "role") .unwrap_or_else(|| "message".to_owned()), @@ -3237,6 +3240,56 @@ fn first_message_search_result( None } +struct SearchSnippet { + text: String, + match_start: usize, + match_end: usize, +} + +fn bounded_search_snippet(text: &str, match_start: usize, match_end: usize) -> SearchSnippet { + let total_chars = text.chars().count(); + if total_chars <= SEARCH_SNIPPET_MAX_CHARS { + return SearchSnippet { + text: text.to_owned(), + match_start, + match_end, + }; + } + + let match_start_char = text[..match_start].chars().count(); + let match_end_char = text[..match_end].chars().count(); + let match_chars = match_end_char.saturating_sub(match_start_char); + let context_budget = SEARCH_SNIPPET_MAX_CHARS.saturating_sub(match_chars); + let before_budget = context_budget.min(SEARCH_SNIPPET_CONTEXT_CHARS); + let after_budget = context_budget.saturating_sub(before_budget); + let before_chars = before_budget.min(match_start_char); + let mut after_chars = after_budget.min(total_chars.saturating_sub(match_end_char)); + + let unused_before_budget = before_budget.saturating_sub(before_chars); + if unused_before_budget > 0 { + after_chars = + (after_chars + unused_before_budget).min(total_chars.saturating_sub(match_end_char)); + } + + let snippet_start_char = match_start_char.saturating_sub(before_chars); + let snippet_end_char = (match_end_char + after_chars).min(total_chars); + let snippet_start_byte = byte_index_for_char(text, snippet_start_char); + let snippet_end_byte = byte_index_for_char(text, snippet_end_char); + + SearchSnippet { + text: text[snippet_start_byte..snippet_end_byte].to_owned(), + match_start: match_start.saturating_sub(snippet_start_byte), + match_end: match_end.saturating_sub(snippet_start_byte), + } +} + +fn byte_index_for_char(text: &str, char_index: usize) -> usize { + text.char_indices() + .nth(char_index) + .map(|(byte_index, _)| byte_index) + .unwrap_or(text.len()) +} + fn cached_transcript_response( link: &RuntimeTaskLink, messages: Vec, @@ -3968,6 +4021,36 @@ mod tests { assert_eq!(result["messages"].as_array().unwrap().len(), 0); } + #[test] + fn first_message_search_result_returns_bounded_snippet() { + let link = RuntimeTaskLink::new_pending( + "local-task-1".to_owned(), + "/tmp/project".to_owned(), + "Long message task".to_owned(), + ); + let content = format!("{}needle{}", "a".repeat(300), "b".repeat(300)); + + let result = first_message_search_result( + &link, + "device-1", + vec![json!({ + "id": "message-1", + "role": "user", + "content": content, + "createdAt": 1780000000, + })], + "needle", + ) + .expect("long matching message should produce a result"); + let snippet = result["snippet"].as_str().unwrap(); + let match_start = result["matchStart"].as_u64().unwrap() as usize; + let match_end = result["matchEnd"].as_u64().unwrap() as usize; + + assert!(snippet.len() < 300); + assert!(snippet.contains("needle")); + assert_eq!(&snippet[match_start..match_end], "needle"); + } + #[test] fn pending_thread_event_route_promotes_on_thread_started() { let index_path = temp_runtime_work_index_path("pending-thread-event-route"); diff --git a/wework/src/components/layout/WorkbenchSearchDialog.test.tsx b/wework/src/components/layout/WorkbenchSearchDialog.test.tsx index 90b80309a2..322bd8143c 100644 --- a/wework/src/components/layout/WorkbenchSearchDialog.test.tsx +++ b/wework/src/components/layout/WorkbenchSearchDialog.test.tsx @@ -1,9 +1,27 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, describe, expect, test, vi } from 'vitest' import '@/i18n' import { WorkbenchSearchDialog } from './WorkbenchSearchDialog' +function searchItem(title: string, snippet: string) { + return { + address: { + deviceId: 'device-1', + workspacePath: '/repo/Wegent', + taskId: title, + }, + runtime: 'codex', + title, + snippet, + matchStart: 0, + matchEnd: snippet.length, + updatedAt: '2026-06-21T12:00:01Z', + deviceName: 'MacBook', + workspacePath: '/repo/Wegent', + } +} + describe('WorkbenchSearchDialog', () => { afterEach(() => { vi.useRealTimers() @@ -91,4 +109,78 @@ describe('WorkbenchSearchDialog', () => { await vi.advanceTimersByTimeAsync(1) expect(onSearchRuntimeWork).toHaveBeenCalledWith({ query: '沙箱', limit: 20 }) }) + + test('does not show loading state before debounce starts the request', async () => { + vi.useFakeTimers() + let resolveSearch: ((value: { items: [] }) => void) | undefined + const onSearchRuntimeWork = vi.fn( + () => + new Promise<{ items: [] }>(resolve => { + resolveSearch = resolve + }) + ) + + render( + + ) + + fireEvent.change(screen.getByTestId('workbench-search-input'), { + target: { value: 'pwd' }, + }) + + expect(screen.queryByText(/正在搜索|Searching/)).not.toBeInTheDocument() + + await act(async () => { + await vi.advanceTimersByTimeAsync(180) + }) + expect(screen.getByText(/正在搜索|Searching/)).toBeInTheDocument() + + await act(async () => { + resolveSearch?.({ items: [] }) + }) + }) + + test('reuses cached results when a query repeats', async () => { + vi.useFakeTimers() + const onSearchRuntimeWork = vi + .fn() + .mockResolvedValue({ items: [] }) + .mockResolvedValueOnce({ items: [searchItem('执行 pwd', 'pwd')] }) + .mockResolvedValueOnce({ items: [searchItem('执行 ls', 'ls')] }) + + render( + + ) + + const input = screen.getByTestId('workbench-search-input') + fireEvent.change(input, { target: { value: 'pwd' } }) + await act(async () => { + await vi.advanceTimersByTimeAsync(180) + }) + expect(screen.getByText('执行 pwd')).toBeInTheDocument() + + fireEvent.change(input, { target: { value: 'ls' } }) + await act(async () => { + await vi.advanceTimersByTimeAsync(180) + }) + expect(screen.getByText('执行 ls')).toBeInTheDocument() + + fireEvent.change(input, { target: { value: 'pwd' } }) + await act(async () => { + await vi.advanceTimersByTimeAsync(180) + }) + + expect(onSearchRuntimeWork).toHaveBeenCalledTimes(2) + expect(screen.getByText('执行 pwd')).toBeInTheDocument() + }) }) diff --git a/wework/src/components/layout/WorkbenchSearchDialog.tsx b/wework/src/components/layout/WorkbenchSearchDialog.tsx index 4faccb11bb..4890b4443d 100644 --- a/wework/src/components/layout/WorkbenchSearchDialog.tsx +++ b/wework/src/components/layout/WorkbenchSearchDialog.tsx @@ -11,6 +11,7 @@ import type { const SEARCH_DEBOUNCE_MS = 180 const SEARCH_LIMIT = 20 +const SEARCH_CACHE_LIMIT = 20 interface WorkbenchSearchDialogProps { open: boolean @@ -43,11 +44,13 @@ function WorkbenchSearchDialogPanel({ }: Omit) { const { t } = useTranslation('common') const inputRef = useRef(null) + const searchCacheRef = useRef(new Map()) const [query, setQuery] = useState('') const [items, setItems] = useState([]) const [selectedIndex, setSelectedIndex] = useState(0) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [searchedQuery, setSearchedQuery] = useState('') const trimmedQuery = query.trim() useEffect(() => { @@ -57,19 +60,33 @@ function WorkbenchSearchDialogPanel({ useEffect(() => { if (!trimmedQuery) return + if (searchCacheRef.current.has(trimmedQuery)) { + const cachedItems = searchCacheRef.current.get(trimmedQuery) ?? [] + setItems(cachedItems) + setSelectedIndex(0) + setError(null) + setLoading(false) + setSearchedQuery(trimmedQuery) + return + } + let cancelled = false const timer = window.setTimeout(() => { + setLoading(true) onSearchRuntimeWork({ query: trimmedQuery, limit: SEARCH_LIMIT }) .then(response => { if (cancelled) return + rememberSearchResults(searchCacheRef.current, trimmedQuery, response.items) setItems(response.items) setSelectedIndex(0) setError(null) + setSearchedQuery(trimmedQuery) }) .catch(() => { if (cancelled) return setItems([]) setError(t('workbench.search_failed')) + setSearchedQuery(trimmedQuery) }) .finally(() => { if (!cancelled) setLoading(false) @@ -87,9 +104,11 @@ function WorkbenchSearchDialogPanel({ const statusLabel = useMemo(() => { if (loading) return t('workbench.search_loading') if (error) return error - if (hasQuery && items.length === 0) return t('workbench.search_no_results') + if (hasQuery && searchedQuery === trimmedQuery && items.length === 0) { + return t('workbench.search_no_results') + } return null - }, [error, hasQuery, items.length, loading, t]) + }, [error, hasQuery, items.length, loading, searchedQuery, t, trimmedQuery]) const openItem = (item: RuntimeWorkSearchItem | null) => { if (!item) return @@ -117,10 +136,12 @@ function WorkbenchSearchDialogPanel({ setSelectedIndex(0) setError(null) if (nextQuery.trim()) { - setLoading(true) + setLoading(false) + setSearchedQuery('') } else { setItems([]) setLoading(false) + setSearchedQuery('') } }} onKeyDown={event => { @@ -202,6 +223,22 @@ function WorkbenchSearchDialogPanel({ ) } +function rememberSearchResults( + cache: Map, + query: string, + items: RuntimeWorkSearchItem[] +) { + if (cache.has(query)) { + cache.delete(query) + } + cache.set(query, items) + if (cache.size <= SEARCH_CACHE_LIMIT) return + const oldestKey = cache.keys().next().value + if (oldestKey) { + cache.delete(oldestKey) + } +} + function renderHighlightedSnippet(snippet: string, query: string) { if (!query) return snippet const index = snippet.toLowerCase().indexOf(query.toLowerCase()) From d39ec861e1ab7b869509f58f4b28d90f202b9087 Mon Sep 17 00:00:00 2001 From: yunpeng7 Date: Mon, 6 Jul 2026 21:34:23 +0800 Subject: [PATCH 3/5] fix(wework): open search results with transcripts --- executor/src/runtime_work/response.rs | 50 ++++++++++++------- executor/tests/app_runtime_work_contract.rs | 18 +++++++ .../layout/WorkbenchSearchDialog.test.tsx | 1 + .../layout/WorkbenchSearchDialog.tsx | 3 -- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/executor/src/runtime_work/response.rs b/executor/src/runtime_work/response.rs index 651f78aad8..dd007bf0fb 100644 --- a/executor/src/runtime_work/response.rs +++ b/executor/src/runtime_work/response.rs @@ -397,19 +397,7 @@ pub(crate) fn search_result_item( } fn local_task_json(link: RuntimeTaskLink) -> Value { - let mut runtime_handle = link - .runtime_handle - .as_object() - .cloned() - .unwrap_or_else(Map::new); - runtime_handle.insert( - "threadId".to_owned(), - link.thread_id - .as_ref() - .map(|thread_id| Value::String(thread_id.clone())) - .unwrap_or(Value::Null), - ); - + let runtime_handle = runtime_handle_with_thread_id(&link); let mut task = Map::new(); task.insert("taskId".to_owned(), Value::String(link.local_task_id)); task.insert( @@ -461,11 +449,37 @@ fn archived_conversation_item(link: &RuntimeTaskLink, device_id: &str) -> Value } fn runtime_task_address(link: &RuntimeTaskLink, device_id: &str) -> Value { - json!({ - "deviceId": device_id, - "workspacePath": link.workspace_path, - "taskId": link.local_task_id, - }) + let mut address = Map::new(); + address.insert("deviceId".to_owned(), Value::String(device_id.to_owned())); + address.insert( + "workspacePath".to_owned(), + Value::String(link.workspace_path.clone()), + ); + address.insert( + "taskId".to_owned(), + Value::String(link.local_task_id.clone()), + ); + address.insert( + "runtimeHandle".to_owned(), + Value::Object(runtime_handle_with_thread_id(link)), + ); + Value::Object(address) +} + +fn runtime_handle_with_thread_id(link: &RuntimeTaskLink) -> Map { + let mut runtime_handle = link + .runtime_handle + .as_object() + .cloned() + .unwrap_or_else(Map::new); + runtime_handle.insert( + "threadId".to_owned(), + link.thread_id + .as_ref() + .map(|thread_id| Value::String(thread_id.clone())) + .unwrap_or(Value::Null), + ); + runtime_handle } fn thread_status(thread: &Value) -> String { diff --git a/executor/tests/app_runtime_work_contract.rs b/executor/tests/app_runtime_work_contract.rs index 4b78c339ff..7dec17e794 100644 --- a/executor/tests/app_runtime_work_contract.rs +++ b/executor/tests/app_runtime_work_contract.rs @@ -869,10 +869,28 @@ async fn app_runtime_searches_codex_titles_and_transcripts() { assert_eq!(title_result["success"], true); assert_eq!(title_result["items"][0]["address"]["taskId"], "thread-1"); assert_eq!(title_result["items"][0]["address"]["deviceId"], "device-1"); + assert_eq!( + title_result["items"][0]["address"]["runtimeHandle"]["threadId"], + "thread-1" + ); assert_eq!(title_result["items"][0]["title"], "Fix CI"); assert_eq!(title_result["items"][0]["snippet"], "Fix CI"); assert_eq!(title_result["items"][0]["messageRole"], "title"); + let transcript_from_search_result = handler + .handle_runtime_rpc(json!({ + "method": "runtime.tasks.transcript", + "payload": title_result["items"][0]["address"].clone() + })) + .await + .expect("search result address should load transcript"); + + assert_eq!(transcript_from_search_result["success"], true); + assert_eq!( + transcript_from_search_result["messages"][0]["content"], + "please fix ci" + ); + let transcript_result = handler .handle_runtime_rpc(json!({ "method": "runtime.tasks.search", diff --git a/wework/src/components/layout/WorkbenchSearchDialog.test.tsx b/wework/src/components/layout/WorkbenchSearchDialog.test.tsx index 322bd8143c..8163e230f1 100644 --- a/wework/src/components/layout/WorkbenchSearchDialog.test.tsx +++ b/wework/src/components/layout/WorkbenchSearchDialog.test.tsx @@ -70,6 +70,7 @@ describe('WorkbenchSearchDialog', () => { expect(screen.getByTestId('workbench-search-result-0')).toHaveTextContent( '请执行 pwd 并返回结果' ) + expect(screen.getByTestId('workbench-search-result-0')).not.toHaveTextContent('⌘1') expect(onSearchRuntimeWork).toHaveBeenCalledWith({ query: 'pwd', limit: 20 }) await user.click(screen.getByTestId('workbench-search-result-0')) diff --git a/wework/src/components/layout/WorkbenchSearchDialog.tsx b/wework/src/components/layout/WorkbenchSearchDialog.tsx index 4890b4443d..0e6220764e 100644 --- a/wework/src/components/layout/WorkbenchSearchDialog.tsx +++ b/wework/src/components/layout/WorkbenchSearchDialog.tsx @@ -206,9 +206,6 @@ function WorkbenchSearchDialogPanel({
{item.project?.name || item.deviceName} - - {`⌘${index + 1}`} -
))} From 343af4e4cb2499ee0bd29edf1e3434124bbfe2d8 Mon Sep 17 00:00:00 2001 From: yunpeng7 Date: Mon, 6 Jul 2026 21:40:57 +0800 Subject: [PATCH 4/5] test(wework): stabilize runtime terminal cache test --- wework/src/components/layout/DesktopWorkbenchLayout.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx b/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx index 7f64836c3a..2647139f6f 100644 --- a/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx +++ b/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx @@ -5785,7 +5785,7 @@ describe('DesktopWorkbenchLayout', () => { await waitFor(() => { expect(closeLocalTerminalMock).toHaveBeenCalledWith('local-terminal-a') }) - }) + }, 10000) test('opens the bottom workspace add menu without replacing the terminal', async () => { renderWorkspacePanelLayout() From dad2c793b54c9579ae2e8968964cfeec9f9fb359 Mon Sep 17 00:00:00 2001 From: yunpeng7 Date: Mon, 6 Jul 2026 21:44:34 +0800 Subject: [PATCH 5/5] docs(wework): document runtime search addresses --- docs/en/developer-guide/runtime-local-work.md | 4 ++-- docs/zh/developer-guide/runtime-local-work.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/developer-guide/runtime-local-work.md b/docs/en/developer-guide/runtime-local-work.md index ab3afebeb9..5090e5cef7 100644 --- a/docs/en/developer-guide/runtime-local-work.md +++ b/docs/en/developer-guide/runtime-local-work.md @@ -58,11 +58,11 @@ Wework searches device-local work with: POST /api/runtime-work/search ``` -Backend fans out `runtime.tasks.search` only to the current user's online or busy devices. It does not read central `TaskResource`, `Subtask`, or cached history rows. The executor searches local task titles and transcripts, then returns snippets, message metadata, update time, device name, workspace path, and the transient task address. +Backend fans out `runtime.tasks.search` only to the current user's online or busy devices. It does not read central `TaskResource`, `Subtask`, or cached history rows. The executor searches local task titles and transcripts, then returns snippets, message metadata, update time, device name, workspace path, and the transient task address. For Codex history threads, the transient task address must include `runtimeHandle.threadId` so opening a search result can load the original conversation through the transcript RPC without depending on a matching link already existing in the local task index. Search results are merged by `updatedAt` descending and capped by the request `limit`. `includeArchived` is passed to the executor so it can decide whether archived LocalTasks are included. When the request includes `projectId`, Backend derives the Project from each workspace path and only returns results under that Project; `workspaceKind: chat` Conversation results have no Project owner. -The frontend search dialog opens only the `deviceId + localTaskId` address from the result, then restores workspace context from the latest runtime work list. +The frontend search dialog opens the runtime address from the result, then restores workspace context from the latest runtime work list. The dialog only keeps recent query results in memory to avoid repeating the same RPC while the dialog is open; cached results are not written to Backend and do not replace executor-side transcript reads. ## Open And Continue diff --git a/docs/zh/developer-guide/runtime-local-work.md b/docs/zh/developer-guide/runtime-local-work.md index c48a8bfe12..e1c1109584 100644 --- a/docs/zh/developer-guide/runtime-local-work.md +++ b/docs/zh/developer-guide/runtime-local-work.md @@ -58,11 +58,11 @@ Wework 使用运行时搜索能力查找设备上的本地任务: POST /api/runtime-work/search ``` -Backend 只向当前用户在线或 busy 的设备 fan-out `runtime.tasks.search` RPC,不读取中心库 `TaskResource`、`Subtask` 或历史缓存。executor 在本机任务标题和 transcript 中搜索,并返回匹配片段、消息元数据、更新时间、设备名、工作区路径和临时任务地址。 +Backend 只向当前用户在线或 busy 的设备 fan-out `runtime.tasks.search` RPC,不读取中心库 `TaskResource`、`Subtask` 或历史缓存。executor 在本机任务标题和 transcript 中搜索,并返回匹配片段、消息元数据、更新时间、设备名、工作区路径和临时任务地址。对于 Codex 历史线程,临时任务地址必须携带 `runtimeHandle.threadId`,这样点击搜索结果后可以直接通过 transcript RPC 读取原始会话,而不依赖本地任务索引中已经存在对应 link。 搜索结果按 `updatedAt` 倒序聚合,并受请求 `limit` 限制。`includeArchived` 传给 executor 决定是否包含已归档 LocalTask。请求携带 `projectId` 时,Backend 会根据工作区路径推导 Project,并只返回该 Project 下的搜索结果;`workspaceKind: chat` 的 Conversation 结果没有 Project 归属。 -前端搜索框只打开结果里的 `deviceId + localTaskId` 地址,随后仍通过最新 runtime work 列表恢复工作区上下文。 +前端搜索框打开结果里的运行时地址,随后仍通过最新 runtime work 列表恢复工作区上下文。搜索框只在内存中保留最近查询结果,用于避免同一会话内重复输入触发相同 RPC;缓存结果不写入 Backend,也不替代 executor 侧的 transcript 读取。 ## 打开和继续任务