newmark-agent 0.5.12 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -111,12 +111,12 @@ let CORE_SYSTEM_PROMPT = `You are Newmark Agent, a powerful AI coding assistant
111
111
  - delete_file: Delete ONE file at a time under Agent supervision (absolute path; refuses directories and wildcards)
112
112
  - glob: Find files by pattern
113
113
  - grep: Search file contents with regex
114
- - web_search: Search the web via DuckDuckGo
114
+ - web_search: Search through the configured search-only MCP pool, then fall back to Bing HTTP and finally DuckDuckGo HTTP
115
115
  - web_fetch: Fetch and extract content from URLs
116
116
  - browser_use: Preferred native built-in-browser workflow. Observe first, then use the returned page generation, observation id, and opaque refs for click/type/select/scroll/key/navigation/wait/extraction. Recognition order is enforced as rendered DOM text first, then an ephemeral screenshot sent to a validated vision model when text is unavailable, and only then local OCR. A successful action receipt is enough to continue the Build; do not wait for the browser session or window to close. Every receipt is bound to the current workspace/conversation runtime and actor. Stale page capabilities are rejected; observe again to recover.
117
117
  - browser_open/browser_snapshot/browser_click/browser_type/browser_eval/browser_back/browser_forward/browser_reload/browser_cdp: Legacy and expert Chromium controls. Prefer browser_use for normal interactive work; raw eval/CDP remain advanced escape hatches.
118
118
  - computer_use: Native desktop Computer Use control for full desktop or app-scoped observe/move/click/scroll/type/key/wait against Windows desktop applications. A successful takeover_start receipt means the persistent control surface started and the Build may continue immediately; do not wait for takeover_stop or closure before taking the next step. Use takeover_stop when control is no longer needed. Use app_list/app_observe/app_activate/app_click/app_scroll/app_type/app_key when the task can be scoped to a visible taskbar application by title, process name, PID, or window handle; this narrows screenshots and actions to that application. Use observe/app_observe first, reason over returned screenshot plus UI Automation objects. If the model supports vision, Newmark sends the screenshot image and UI object tree together in the same tool-result context; use both for stable decisions. Prefer target_id from perception.scene_summary.high_priority_objects or perception.objects for move/click/scroll when available; fall back to exact coordinates only when necessary.
119
- - image_inspect: For durable user-submitted visual attachments, query source_info by stable attachment_id (or latest-message image_index) and actively crop/magnify a precise pixel region when text or geometry is too small to inspect reliably. Original user images remain revisitable; derived crops are current-turn-only and never saved to disk.
119
+ - image_inspect: Inspect durable user-submitted visual attachments, or use action=inspect with a workspace-relative PNG/JPEG path to send that file to the current validated vision model. Query source_info before cropping submitted images when dimensions are unknown. Workspace observations and derived crops are current-turn-only; image bytes are never saved in tool text or durable history.
120
120
  - image_display: Present one workspace PNG/JPEG to the user. Use it for diagrams or other visual evidence that materially helps explain the current Build; pass a workspace-relative path and optional caption hint. When validated vision is active, Newmark inspects the actual image and generates the displayed descriptive title; otherwise the caption or filename is used as fallback.
121
121
  - ocr_read: LAST-RESORT, approximate Simplified-Chinese/English OCR only. Never call it before normal text extraction and validated vision input. When it returns text, use its Agent repair prompt to conservatively correct likely substitutions, spacing, and line breaks from surrounding context; never invent unsupported content.
122
122
  - task: Create a subagent for parallel work
@@ -226,6 +226,13 @@ class Agent {
226
226
  branchMailbox = [];
227
227
  nextBranchMessageSequence = 1;
228
228
  branchCommunicationEnabled = false;
229
+ /**
230
+ * Title/model availability is a hard gate for the first formal response.
231
+ * These values mirror the current conversation's persisted gate state so a
232
+ * failed first attempt cannot be bypassed by a second ordinary user send.
233
+ */
234
+ titleRequestMessageId = '';
235
+ firstAgentResponseStarted = false;
229
236
  compressionArchiveCountCache = null;
230
237
  nextCompressionCacheId = 1;
231
238
  compressionHistoryArchive;
@@ -1663,6 +1670,48 @@ class Agent {
1663
1670
  return { runs: normalized, changed: false };
1664
1671
  return { runs: normalized, changed };
1665
1672
  }
1673
+ /** Close tool calls left without results by a hard worker exit. */
1674
+ repairDanglingToolCalls(messages) {
1675
+ const repaired = [];
1676
+ const pending = new Map();
1677
+ let changed = false;
1678
+ const settlePending = () => {
1679
+ for (const [id, name] of pending) {
1680
+ repaired.push({
1681
+ role: 'tool', tool_call_id: id, name,
1682
+ content: JSON.stringify({
1683
+ ok: false, code: 'runtime_interrupted', recoverable: true,
1684
+ error: 'The runtime exited before this tool returned. Retry the tool if it is still needed.',
1685
+ }),
1686
+ });
1687
+ changed = true;
1688
+ }
1689
+ pending.clear();
1690
+ };
1691
+ for (const message of messages) {
1692
+ const role = String(message.role || '');
1693
+ const toolCallId = String(message.tool_call_id || '');
1694
+ if (pending.size && role !== 'tool')
1695
+ settlePending();
1696
+ repaired.push({ ...message });
1697
+ if (role === 'assistant' && Array.isArray(message.tool_calls)) {
1698
+ for (const call of message.tool_calls) {
1699
+ const id = String(call.id || '');
1700
+ const fn = call.function && typeof call.function === 'object' ? call.function : {};
1701
+ if (id)
1702
+ pending.set(id, String(fn.name || 'tool'));
1703
+ }
1704
+ }
1705
+ else if (role === 'tool' && toolCallId)
1706
+ pending.delete(toolCallId);
1707
+ }
1708
+ if (pending.size)
1709
+ settlePending();
1710
+ return { messages: repaired, changed };
1711
+ }
1712
+ repairDanglingToolCallsForTest(messages) {
1713
+ return this.repairDanglingToolCalls(messages);
1714
+ }
1666
1715
  pauseFlowAfterUnexpectedExit() {
1667
1716
  if (this.mode !== 'flow' || !this.flow?.name)
1668
1717
  return false;
@@ -3575,10 +3624,10 @@ class Agent {
3575
3624
  return { id, title: resolvedTitle };
3576
3625
  }
3577
3626
  /**
3578
- * Build 命名判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
3579
- * (2) 其持久化 title 仍是自动生成(含为空)时返回 true。dev-0.4.3 起不再
3580
- * 用该判定注入首轮 tool-call 指令,而是在首个完成 Build 的最终响应处自动
3581
- * 命名(见 maybeAutoRenameConversationFromRun)。判定本身只读存储、无副作用。
3627
+ * Compatibility query for callers that still expose the rename affordance.
3628
+ * Automatic naming itself is a hard pre-response gate: until the persisted
3629
+ * firstAgentResponseStarted flag is true, every ordinary send must finish a
3630
+ * title probe for the first persisted user message before formal execution.
3582
3631
  */
3583
3632
  shouldPromptConversationRename() {
3584
3633
  if (this.conversationBuildHistory(1).length > 0)
@@ -3588,14 +3637,16 @@ class Agent {
3588
3637
  if (!stateKey)
3589
3638
  return false;
3590
3639
  const entry = this.readStoredConversationState().conversations?.[stateKey];
3640
+ if (this.restoredFirstAgentResponseStarted(entry))
3641
+ return false;
3591
3642
  const priorTitle = entry?.title;
3592
3643
  const messages = entry?.chatMessages || this.chatMessages;
3593
3644
  return this.isGeneratedConversationTitle(priorTitle, conversationId, messages);
3594
3645
  }
3595
3646
  /**
3596
- * 从首个 Build 的最终响应中提取一个简短对话标题。跳过 Markdown 标题/列表
3597
- * 符号与固定 section 标题,取第一条有意义的摘要句并做保守清洗。
3598
- * dev-0.4.5 起仅作为独立 rename API 不可用时的本地回退。
3647
+ * Legacy explicit-rename sanitizer retained for compatibility tests only.
3648
+ * Automatic naming must never use an Agent/Build summary as a fallback;
3649
+ * the independent first-input title probe is the sole automatic source.
3599
3650
  */
3600
3651
  deriveConversationTitleFromSummary(summary) {
3601
3652
  const clean = this.sanitizeAssistantOutput(summary || '').replace(/\r/g, '');
@@ -3638,27 +3689,28 @@ class Agent {
3638
3689
  return title.length >= 2 ? title : '';
3639
3690
  }
3640
3691
  /**
3641
- * 用独立的 provider API 请求为对话生成标题(与主响应并行、不共享主前缀缓存)。
3642
- * system 约束模型只按格式返回一个简短名词短语标题;失败、超时或无 provider
3643
- * 返回空字符串,由调用方回退到本地 deriveConversationTitleFromSummary。
3692
+ * Run the independent, tool-free title/model-availability probe. It shares
3693
+ * the frozen deployment selected for this send, but no main Agent history or
3694
+ * tool schema. An empty result keeps the formal response hard-blocked.
3644
3695
  */
3645
- async deriveConversationTitleFromProvider(summary) {
3646
- const provider = this.engineModel();
3647
- const modelName = this.activeModelName();
3648
- if (!provider || !modelName)
3649
- return '';
3696
+ async deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal) {
3650
3697
  const system = [
3651
3698
  'You are a conversation title generator.',
3652
- 'Return ONLY a short, concrete noun-phrase title for the conversation, a few words at most.',
3699
+ 'Summarize the user intent and return ONLY a short, concrete noun-phrase title for the conversation.',
3700
+ 'Do not quote, repeat, or truncate the user input as the title.',
3653
3701
  'No preamble, no explanation, no Markdown, no quotes, no trailing punctuation.',
3654
3702
  ].join('\n');
3655
- const prompt = `Task summary:\n${String(summary || '').slice(0, 2000)}\n\nConversation title (a few words):`;
3703
+ const prompt = `First user input:\n${String(firstUserInput || '').slice(0, 4000)}\n\nConversation title (a few words):`;
3656
3704
  const controller = new AbortController();
3657
3705
  const timer = setTimeout(() => controller.abort(new Error('conversation rename timed out')), 15000);
3658
3706
  try {
3659
- const { temperature } = provider.intelligenceConfig('low');
3660
- const generated = await provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, 64, controller.signal);
3661
- return this.normalizeConversationRenameTitle(generated);
3707
+ if (signal?.aborted)
3708
+ return '';
3709
+ const { temperature, reasoningEffort } = provider.intelligenceConfig(intelligence);
3710
+ const generated = await provider.chat(modelName, [{ role: 'user', content: prompt }], system, temperature, 64, controller.signal, reasoningEffort);
3711
+ const title = this.normalizeConversationRenameTitle(generated);
3712
+ const source = String(firstUserInput || '').replace(/\s+/g, ' ').trim();
3713
+ return title && title !== source ? title : '';
3662
3714
  }
3663
3715
  catch {
3664
3716
  return '';
@@ -3667,34 +3719,134 @@ class Agent {
3667
3719
  clearTimeout(timer);
3668
3720
  }
3669
3721
  }
3722
+ /** Legacy completion hook retained as a no-op; naming is pre-response only. */
3723
+ maybeAutoRenameConversationFromRun(_run) {
3724
+ // Compatibility no-op. Automatic naming is a hard gate on the first
3725
+ // persisted user input and never depends on Agent output or completion.
3726
+ }
3727
+ async startFirstInputConversationTitle(messageId, firstUserInput, provider, modelName, intelligence, signal) {
3728
+ const conversationId = this.activeConversationId || 'default';
3729
+ const workspace = this.workspace.current;
3730
+ const stateKey = this.workspaceConversationStateKeyFor(conversationId, workspace);
3731
+ // 标题探测失败(空响应、响应重复用户输入、临时传输错误)使用
3732
+ // 0s → 1s → 2s → 4s → 8s 的 5 级退避,全部失败才阻断正式首轮。
3733
+ const maxAttempts = 5;
3734
+ const retryDelaysMs = [0, 1000, 2000, 4000, 8000];
3735
+ if (!messageId || !firstUserInput.trim())
3736
+ return false;
3737
+ if (!stateKey) {
3738
+ // Pure Agent/CLI mode has no workspace conversation file to rename, but
3739
+ // still uses the title request as the required first model-availability
3740
+ // probe before starting the formal response.
3741
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3742
+ if (await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal))
3743
+ return true;
3744
+ if (signal?.aborted)
3745
+ return false;
3746
+ if (attempt < maxAttempts - 1)
3747
+ await new Promise(resolve => setTimeout(resolve, retryDelaysMs[attempt]));
3748
+ }
3749
+ return false;
3750
+ }
3751
+ const stored = this.readStoredConversationState(workspace);
3752
+ const entry = stored.conversations?.[stateKey];
3753
+ if (!entry || (entry.titleRequestMessageId && entry.titleRequestMessageId !== messageId))
3754
+ return false;
3755
+ const firstUser = (entry.chatMessages || []).find(message => message.role === 'user');
3756
+ if (!firstUser || String(firstUser.messageId || '') !== messageId)
3757
+ return false;
3758
+ entry.titleRequestMessageId = messageId;
3759
+ this.writeStoredConversationState(stored, workspace);
3760
+ this.titleRequestMessageId = messageId;
3761
+ const memoryKey = this.workspaceConversationKey();
3762
+ const memory = memoryKey ? this.workspaceConversations.get(memoryKey) : undefined;
3763
+ if (memory)
3764
+ memory.titleRequestMessageId = messageId;
3765
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3766
+ const title = await this.deriveConversationTitleFromProvider(firstUserInput, provider, modelName, intelligence, signal);
3767
+ if (title) {
3768
+ const latest = this.readStoredConversationState(workspace);
3769
+ const current = latest.conversations?.[stateKey];
3770
+ const stillFirst = (current?.chatMessages || []).find(message => message.role === 'user');
3771
+ if (!current || current.titleRequestMessageId !== messageId || String(stillFirst?.messageId || '') !== messageId)
3772
+ return false;
3773
+ if (!this.isGeneratedConversationTitle(current.title, conversationId, current.chatMessages || []))
3774
+ return true;
3775
+ current.title = title;
3776
+ current.updatedAt = this.nowIso();
3777
+ this.writeStoredConversationState(latest, workspace);
3778
+ return true;
3779
+ }
3780
+ if (signal?.aborted)
3781
+ return false;
3782
+ if (attempt < maxAttempts - 1)
3783
+ await new Promise(resolve => setTimeout(resolve, retryDelaysMs[attempt]));
3784
+ }
3785
+ return false;
3786
+ }
3787
+ firstPersistedUserForTitleGate() {
3788
+ const firstUser = this.chatMessages.find(message => message.role === 'user');
3789
+ const messageId = String(firstUser?.messageId || '').trim();
3790
+ if (!firstUser || !messageId)
3791
+ return null;
3792
+ const attachmentCount = firstUser.attachments?.length || 0;
3793
+ const input = String(firstUser.content || '').trim()
3794
+ || (attachmentCount ? `The user submitted ${attachmentCount} image attachment${attachmentCount === 1 ? '' : 's'}.` : '');
3795
+ return input ? { messageId, input } : null;
3796
+ }
3670
3797
  /**
3671
- * dev-0.4.5:conversation_rename 流程独立为「并行响应 API」。首个完成 Build
3672
- * 最终响应到达时,fire-and-forget 发起一个独立 provider 请求生成标题并按格式
3673
- * rename,不阻塞主流程、不污染主前缀缓存;无 provider / 失败时回退本地启发式。
3674
- * conversation_rename 工具与 shouldPromptConversationRename 判定均保留,且不再
3675
- * 在首轮 prompt 注入一次性 tool-call 指令。
3798
+ * Conversations saved before the durable title gate existed have no explicit
3799
+ * flag. Prior assistant history or any persisted WorkRun proves that their
3800
+ * first formal response already started. An explicit false is authoritative:
3801
+ * it represents a failed title probe that must remain blocked across reloads.
3676
3802
  */
3677
- maybeAutoRenameConversationFromRun(run) {
3678
- if (run.status !== 'completed')
3679
- return;
3680
- if (!this.shouldPromptConversationRename())
3681
- return;
3682
- const finalEvent = [...run.events].reverse().find(event => event.type === 'final_response');
3683
- const finalMessage = [...this.chatMessages].reverse().find(message => message.role === 'assistant' && message.runId === run.runId);
3684
- const raw = finalEvent?.content || finalMessage?.content || '';
3685
- const summary = this.sanitizePublicWorkContent(raw).slice(0, 2000);
3686
- const conversationId = this.activeConversationId || 'default';
3687
- void this.deriveConversationTitleFromProvider(summary)
3688
- .then(title => {
3689
- const resolved = title || this.deriveConversationTitleFromSummary(summary);
3690
- if (resolved)
3691
- this.renameConversation(conversationId, resolved);
3692
- })
3693
- .catch(() => {
3694
- const fallback = this.deriveConversationTitleFromSummary(summary);
3695
- if (fallback)
3696
- this.renameConversation(conversationId, fallback);
3697
- });
3803
+ restoredFirstAgentResponseStarted(entry) {
3804
+ if (!entry)
3805
+ return false;
3806
+ if (typeof entry.firstAgentResponseStarted === 'boolean')
3807
+ return entry.firstAgentResponseStarted;
3808
+ return (entry.workRuns || []).length > 0
3809
+ || (entry.chatMessages || []).some(message => message.role === 'assistant')
3810
+ || (entry.history || []).some(message => String(message.role || '') === 'assistant');
3811
+ }
3812
+ getConversationTitleGateState() {
3813
+ return {
3814
+ titleRequestMessageId: this.titleRequestMessageId,
3815
+ firstAgentResponseStarted: this.firstAgentResponseStarted,
3816
+ };
3817
+ }
3818
+ markFirstAgentResponseStarted(messageId) {
3819
+ const normalizedMessageId = String(messageId || '').trim();
3820
+ if (!normalizedMessageId)
3821
+ return false;
3822
+ const workspace = this.workspace.current;
3823
+ const stateKey = this.workspaceConversationStateKeyFor(this.activeConversationId || 'default', workspace);
3824
+ if (!stateKey) {
3825
+ this.titleRequestMessageId = normalizedMessageId;
3826
+ this.firstAgentResponseStarted = true;
3827
+ return true;
3828
+ }
3829
+ const marked = this.mutateStoredConversationState(workspace, latest => {
3830
+ const current = latest.conversations?.[stateKey];
3831
+ const firstUser = (current?.chatMessages || []).find(message => message.role === 'user');
3832
+ if (!current || current.titleRequestMessageId !== normalizedMessageId
3833
+ || String(firstUser?.messageId || '') !== normalizedMessageId)
3834
+ return latest;
3835
+ current.firstAgentResponseStarted = true;
3836
+ current.updatedAt = this.nowIso();
3837
+ return latest;
3838
+ }, latest => latest.conversations?.[stateKey]?.firstAgentResponseStarted === true);
3839
+ if (!marked)
3840
+ return false;
3841
+ this.titleRequestMessageId = normalizedMessageId;
3842
+ this.firstAgentResponseStarted = true;
3843
+ const key = this.workspaceConversationKey();
3844
+ const memory = key ? this.workspaceConversations.get(key) : undefined;
3845
+ if (memory) {
3846
+ memory.titleRequestMessageId = normalizedMessageId;
3847
+ memory.firstAgentResponseStarted = true;
3848
+ }
3849
+ return true;
3698
3850
  }
3699
3851
  reorderConversations(ids) {
3700
3852
  const prefix = this.workspaceConversationPrefix() || '';
@@ -3764,6 +3916,8 @@ class Agent {
3764
3916
  return true;
3765
3917
  if (cleanTitle === 'Default conversation')
3766
3918
  return true;
3919
+ if (/^(?:New (?:chat|conversation)|新对话)(?:\s+\d+)?$/i.test(cleanTitle))
3920
+ return true;
3767
3921
  return false;
3768
3922
  }
3769
3923
  sanitizeAssistantStreamingOutput(text) {
@@ -3845,6 +3999,8 @@ class Agent {
3845
3999
  inputMode: this.inputMode,
3846
4000
  mode: this.mode,
3847
4001
  goal: this.serializeGoal(),
4002
+ titleRequestMessageId: this.titleRequestMessageId || undefined,
4003
+ firstAgentResponseStarted: this.firstAgentResponseStarted,
3848
4004
  runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3849
4005
  runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3850
4006
  runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
@@ -3859,9 +4015,7 @@ class Agent {
3859
4015
  const priorTitle = stored.conversations[stateKey]?.title;
3860
4016
  const conversationId = this.activeConversationId || 'default';
3861
4017
  const derivedTitle = this.titleFromMessages(this.chatMessages, conversationId);
3862
- const title = this.hasUserConversationTitle(this.chatMessages) && this.isGeneratedConversationTitle(priorTitle, conversationId, stored.conversations[stateKey]?.chatMessages || [])
3863
- ? derivedTitle
3864
- : (priorTitle || derivedTitle);
4018
+ const title = priorTitle || derivedTitle;
3865
4019
  const nextEntry = {
3866
4020
  ...(stored.conversations[stateKey] || {}),
3867
4021
  title,
@@ -3880,6 +4034,8 @@ class Agent {
3880
4034
  inputMode: this.inputMode,
3881
4035
  mode: this.mode,
3882
4036
  goal: this.serializeGoal(),
4037
+ titleRequestMessageId: this.titleRequestMessageId || undefined,
4038
+ firstAgentResponseStarted: this.firstAgentResponseStarted,
3883
4039
  runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3884
4040
  runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3885
4041
  runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
@@ -3910,6 +4066,8 @@ class Agent {
3910
4066
  this.continuations = [];
3911
4067
  this.mode = 'build';
3912
4068
  this.goal = null;
4069
+ this.titleRequestMessageId = '';
4070
+ this.firstAgentResponseStarted = false;
3913
4071
  this.flow = null;
3914
4072
  this.flowPc = 0;
3915
4073
  this.status = 'idle';
@@ -3936,6 +4094,8 @@ class Agent {
3936
4094
  this.inputMode = this.defaultInputMode();
3937
4095
  this.mode = saved.mode || 'build';
3938
4096
  this.goal = this.restoreGoal(saved.goal);
4097
+ this.titleRequestMessageId = String(saved.titleRequestMessageId || '');
4098
+ this.firstAgentResponseStarted = this.restoredFirstAgentResponseStarted(saved);
3939
4099
  this.status = this.restoreStatusFromWorkRuns(saved.goal);
3940
4100
  this.recoverMissingTerminalBuildOverviews();
3941
4101
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
@@ -3944,7 +4104,8 @@ class Agent {
3944
4104
  const stored = this.readStoredConversationState();
3945
4105
  const stateKey = this.workspaceConversationStateKey();
3946
4106
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
3947
- this.history = persisted?.history ? [...persisted.history] : [];
4107
+ const repairedHistory = this.repairDanglingToolCalls(persisted?.history ? [...persisted.history] : []);
4108
+ this.history = repairedHistory.messages;
3948
4109
  this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
3949
4110
  this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
3950
4111
  this.branchMailbox = (persisted?.branchMailbox || []).map(message => ({ ...message }));
@@ -3961,6 +4122,8 @@ class Agent {
3961
4122
  this.inputMode = this.defaultInputMode();
3962
4123
  this.mode = persisted?.mode || 'build';
3963
4124
  this.goal = this.restoreGoal(persisted?.goal);
4125
+ this.titleRequestMessageId = String(persisted?.titleRequestMessageId || '');
4126
+ this.firstAgentResponseStarted = this.restoredFirstAgentResponseStarted(persisted);
3964
4127
  const persistedRuntimeOwnerPid = Math.floor(Number(persisted?.runtimeOwnerPid) || 0);
3965
4128
  const persistedRuntimeOwnerKnown = persistedRuntimeOwnerPid > 0;
3966
4129
  const persistedRuntimeOwnerAlive = persistedRuntimeOwnerKnown && (0, runtimeLifecycle_1.isRuntimeProcessAlive)(persistedRuntimeOwnerPid);
@@ -3982,7 +4145,7 @@ class Agent {
3982
4145
  }
3983
4146
  }
3984
4147
  this.recoverMissingTerminalBuildOverviews();
3985
- const recoveryApplied = recoveredWorkRuns.changed || runtimeOwnerLost || goalPausedByRecovery || flowPausedByRecovery;
4148
+ const recoveryApplied = repairedHistory.changed || recoveredWorkRuns.changed || runtimeOwnerLost || goalPausedByRecovery || flowPausedByRecovery;
3986
4149
  const recoveryAt = recoveryApplied ? this.nowIso() : persisted?.updatedAt;
3987
4150
  this.workspaceConversations.set(key, {
3988
4151
  chatMessages: [...this.chatMessages],
@@ -4000,6 +4163,8 @@ class Agent {
4000
4163
  inputMode: this.defaultInputMode(),
4001
4164
  mode: this.mode,
4002
4165
  goal: this.serializeGoal(),
4166
+ titleRequestMessageId: this.titleRequestMessageId || undefined,
4167
+ firstAgentResponseStarted: this.firstAgentResponseStarted,
4003
4168
  runtimeOwnerId: this.activeConversationRuntimeOwner().runtimeOwnerId,
4004
4169
  runtimeOwnerPid: this.activeConversationRuntimeOwner().runtimeOwnerPid,
4005
4170
  runtimeLifecycleRole: this.activeConversationRuntimeOwner().runtimeLifecycleRole,
@@ -4010,6 +4175,7 @@ class Agent {
4010
4175
  recoveredStored.conversations = recoveredStored.conversations || {};
4011
4176
  recoveredStored.conversations[stateKey] = {
4012
4177
  ...(recoveredStored.conversations[stateKey] || persisted),
4178
+ history: [...this.history],
4013
4179
  workRuns: this.normalizeWorkRuns(this.workRuns),
4014
4180
  flowSelection: this.currentConversationFlowSelection(),
4015
4181
  mode: this.mode,
@@ -5221,6 +5387,17 @@ class Agent {
5221
5387
  const continuations = this.normalizeContinuations(source.continuations || this.getConversationSnapshot(clean).continuations);
5222
5388
  const normalizedChatMessages = this.normalizeConversationChatMessages(source.chatMessages, source.history);
5223
5389
  const updatedAt = new Date().toISOString();
5390
+ const stateKey = this.workspaceConversationStateKey(clean);
5391
+ const stored = this.readStoredConversationState(ws);
5392
+ stored.conversations = stored.conversations || {};
5393
+ const previous = stateKey ? stored.conversations[stateKey] : undefined;
5394
+ const sourceTitleGate = source.getConversationTitleGateState?.() || source.titleGateState;
5395
+ const titleRequestMessageId = sourceTitleGate?.titleRequestMessageId === undefined
5396
+ ? String(previous?.titleRequestMessageId || '')
5397
+ : String(sourceTitleGate.titleRequestMessageId || '');
5398
+ const firstAgentResponseStarted = sourceTitleGate?.firstAgentResponseStarted === undefined
5399
+ ? this.restoredFirstAgentResponseStarted(previous)
5400
+ : sourceTitleGate.firstAgentResponseStarted === true;
5224
5401
  if (key) {
5225
5402
  this.workspaceConversations.set(key, {
5226
5403
  chatMessages: normalizedChatMessages,
@@ -5234,15 +5411,13 @@ class Agent {
5234
5411
  inputMode: this.inputMode,
5235
5412
  mode: source.mode || this.mode,
5236
5413
  goal: source.goal === undefined ? this.serializeGoal() : source.goal,
5414
+ titleRequestMessageId: titleRequestMessageId || undefined,
5415
+ firstAgentResponseStarted,
5237
5416
  updatedAt,
5238
5417
  });
5239
5418
  }
5240
- const stateKey = this.workspaceConversationStateKey(clean);
5241
5419
  if (!stateKey)
5242
5420
  return;
5243
- const stored = this.readStoredConversationState(ws);
5244
- stored.conversations = stored.conversations || {};
5245
- const previous = stored.conversations[stateKey];
5246
5421
  const derivedTitle = this.titleFromMessages(normalizedChatMessages, clean);
5247
5422
  const title = this.hasUserConversationTitle(normalizedChatMessages) && this.isGeneratedConversationTitle(previous?.title, clean, previous?.chatMessages || [])
5248
5423
  ? derivedTitle
@@ -5261,6 +5436,8 @@ class Agent {
5261
5436
  inputMode: this.inputMode,
5262
5437
  mode: source.mode || previous?.mode || this.mode,
5263
5438
  goal: source.goal === undefined ? (previous?.goal || this.serializeGoal()) : source.goal,
5439
+ titleRequestMessageId: titleRequestMessageId || undefined,
5440
+ firstAgentResponseStarted,
5264
5441
  updatedAt,
5265
5442
  };
5266
5443
  if (nextEntry.tree || nextEntry.branches?.length) {
@@ -5281,6 +5458,8 @@ class Agent {
5281
5458
  this.mode = source.mode || this.mode;
5282
5459
  if (source.goal !== undefined)
5283
5460
  this.goal = this.restoreGoal(source.goal);
5461
+ this.titleRequestMessageId = titleRequestMessageId;
5462
+ this.firstAgentResponseStarted = firstAgentResponseStarted;
5284
5463
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
5285
5464
  }
5286
5465
  }
@@ -7498,8 +7677,9 @@ class Agent {
7498
7677
  }
7499
7678
  }
7500
7679
  else if (!hiddenUserInput) {
7680
+ const messageId = crypto.randomUUID();
7501
7681
  this.chatMessages.push({
7502
- messageId: crypto.randomUUID(),
7682
+ messageId,
7503
7683
  branchNodeId: this.currentBranchNodeId(),
7504
7684
  role: 'user',
7505
7685
  content: displayText,
@@ -7520,13 +7700,21 @@ class Agent {
7520
7700
  run_id: inputRunId || undefined,
7521
7701
  });
7522
7702
  }
7523
- // Agent.process seeds the kernel from history, so its initial prompt does
7524
- // not otherwise emit a kernel message_start event.
7525
- this.notifyAgentKernelUserMessageStart(text, clientMessageId || undefined);
7526
7703
  if (!hiddenUserInput)
7527
7704
  this.recordWorkRunPrimaryPrompt(displayText);
7528
7705
  this.saveWorkspaceConversationState(true);
7529
- this.emitWorkEvent({ type: 'start', content: 'Preparing request.' });
7706
+ let firstResponseGateMessageId = '';
7707
+ let firstResponseTitleInput = null;
7708
+ if (!hiddenUserInput && !clientMessageId && !this.firstAgentResponseStarted) {
7709
+ const firstInput = this.firstPersistedUserForTitleGate();
7710
+ if (!firstInput)
7711
+ throw new Error('Conversation title generation failed; the first persisted user input is unavailable.');
7712
+ if (this.model === 'auto' && !autoRouteEvaluated) {
7713
+ await this.evaluateAndSwitch(firstInput.input, inputEnvelope?.routePolicy);
7714
+ autoRouteEvaluated = true;
7715
+ }
7716
+ firstResponseTitleInput = firstInput;
7717
+ }
7530
7718
  if (this.model === 'auto' && !autoRouteEvaluated) {
7531
7719
  await this.evaluateAndSwitch(displayText, inputEnvelope?.routePolicy);
7532
7720
  }
@@ -7542,6 +7730,30 @@ class Agent {
7542
7730
  throw new Error(message);
7543
7731
  }
7544
7732
  }
7733
+ if (firstResponseTitleInput) {
7734
+ // ConversationKernel isolates each send in a runner. Capture the
7735
+ // resolved deployment only after routing/fallback, then use that same
7736
+ // runner-owned model and intelligence for the title availability gate.
7737
+ const titleProvider = this.engineModel();
7738
+ const titleModelName = this.activeModelName();
7739
+ const titleIntelligence = this.intelligence;
7740
+ if (!titleProvider || !titleModelName) {
7741
+ throw new Error('Conversation title generation failed; no resolved model deployment is available. No LLM configured. Add provider in Settings > Models.');
7742
+ }
7743
+ const titled = await this.startFirstInputConversationTitle(firstResponseTitleInput.messageId, firstResponseTitleInput.input, titleProvider, titleModelName, titleIntelligence, processSignal);
7744
+ if (!titled)
7745
+ throw new Error('Conversation title generation failed; the first Agent request was not started. Retry the first input.');
7746
+ firstResponseGateMessageId = firstResponseTitleInput.messageId;
7747
+ }
7748
+ if (firstResponseGateMessageId && !this.markFirstAgentResponseStarted(firstResponseGateMessageId)) {
7749
+ throw new Error('Conversation title generation succeeded, but the first Agent response gate could not be persisted. Retry the first input.');
7750
+ }
7751
+ // Agent.process seeds the kernel from history, so its initial prompt does
7752
+ // not otherwise emit a kernel message_start event. Keep this notification
7753
+ // after the title/model hard gate so a failed probe starts no formal
7754
+ // Agent provider request.
7755
+ this.notifyAgentKernelUserMessageStart(text, clientMessageId || undefined);
7756
+ this.emitWorkEvent({ type: 'start', content: 'Preparing request.' });
7545
7757
  // Use external opencode CLI engine
7546
7758
  if (this.engine === 'opencode') {
7547
7759
  if (images.length)
@@ -8520,8 +8732,29 @@ class Agent {
8520
8732
  }
8521
8733
  catch { }
8522
8734
  const action = String(input.action || '').trim();
8523
- if (action !== 'source_info' && action !== 'crop')
8524
- return '[Image inspect error] action must be source_info or crop.';
8735
+ if (action !== 'source_info' && action !== 'crop' && action !== 'inspect')
8736
+ return '[Image inspect error] action must be source_info, crop, or inspect.';
8737
+ if (action === 'inspect') {
8738
+ try {
8739
+ const workspacePath = this.workspace.current?.path || this.rootPath;
8740
+ const source = (0, displayImages_1.readWorkspaceImageForVision)(workspacePath, String(input.path || ''));
8741
+ return JSON.stringify({
8742
+ ok: true,
8743
+ action,
8744
+ source: 'workspace',
8745
+ path: source.path,
8746
+ name: source.name,
8747
+ format: source.mimeType,
8748
+ byte_length: source.byteLength,
8749
+ width: source.width,
8750
+ height: source.height,
8751
+ image_data_url: source.dataUrl,
8752
+ }, null, 2);
8753
+ }
8754
+ catch (error) {
8755
+ return `[Image inspect error] ${error instanceof Error ? error.message : String(error)}`;
8756
+ }
8757
+ }
8525
8758
  const attachmentId = String(input.attachment_id || '').trim();
8526
8759
  const images = this.latestSubmittedImages(attachmentId);
8527
8760
  const imageIndex = Math.max(1, Math.floor(Number(input.image_index || 1)));
@@ -18,6 +18,13 @@ export declare function isPublicBrowserUseAttribute(input: string): boolean;
18
18
  export interface BrowserUseScope {
19
19
  owner: string;
20
20
  runtimeKey: string;
21
+ /**
22
+ * Selects the user-visible right-sidebar browser surface. `false` keeps the
23
+ * same Browser-Use protocol on a host-owned background page that is never
24
+ * attached to the renderer. Omission is normalized to `true` for backwards
25
+ * compatibility.
26
+ */
27
+ visible?: boolean;
21
28
  }
22
29
  export interface BrowserUseRequest extends BrowserUseScope {
23
30
  action: BrowserUseAction;