newmark-agent 0.4.9 → 0.5.0

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.
package/dist/server.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = void 0;
36
37
  exports.configureHostedServer = configureHostedServer;
37
38
  exports.stopHostedServer = stopHostedServer;
38
39
  exports.hostedServerStatus = hostedServerStatus;
@@ -43,6 +44,7 @@ const http = __importStar(require("http"));
43
44
  const fs = __importStar(require("fs"));
44
45
  const path = __importStar(require("path"));
45
46
  const os = __importStar(require("os"));
47
+ const crypto_1 = require("crypto");
46
48
  const uiPreferences_1 = require("./core/uiPreferences");
47
49
  const child_process_1 = require("child_process");
48
50
  const agent_1 = require("./core/agent");
@@ -53,6 +55,7 @@ const workspaceFileRouter_1 = require("./core/workspaceFileRouter");
53
55
  const nativeBash_1 = require("./core/nativeBash");
54
56
  const installUpdate_1 = require("./core/installUpdate");
55
57
  const mobilePairing_1 = require("./core/mobilePairing");
58
+ const workEventCoalescer_1 = require("./core/workEventCoalescer");
56
59
  const PORT = 47890;
57
60
  let agent = null;
58
61
  let automation = null;
@@ -86,9 +89,12 @@ function publishMobileWorkEvent(event) {
86
89
  }
87
90
  }
88
91
  function publishServerWorkEvent(event) {
92
+ serverWorkEventCoalescer.push(event);
93
+ }
94
+ const serverWorkEventCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => {
89
95
  publishMobileWorkEvent(event);
90
96
  hostedWorkEventSink?.(event);
91
- }
97
+ });
92
98
  function mobileAuthorized(req) {
93
99
  if (!mobileToken)
94
100
  return false;
@@ -124,6 +130,8 @@ const MOBILE_EDITABLE_EXTENSIONS = new Set([
124
130
  '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
125
131
  ]);
126
132
  const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
133
+ const MAX_API_BODY_BYTES = 30 * 1024 * 1024;
134
+ exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
127
135
  function resolveMobileWorkspacePath(ws, relativePath) {
128
136
  const root = path.resolve(ws.path);
129
137
  const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
@@ -142,6 +150,156 @@ function mobileEditableFile(target) {
142
150
  const base = path.basename(target).toLowerCase();
143
151
  return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
144
152
  }
153
+ function validMobileUploadDirectory(value) {
154
+ const clean = String(value || '').replace(/\\/g, '/');
155
+ return !path.isAbsolute(clean)
156
+ && !/^[A-Za-z]:\//.test(clean)
157
+ && !clean.split('/').some(segment => segment === '..' || segment.includes('\0'));
158
+ }
159
+ function validMobileUploadFileName(value) {
160
+ const clean = String(value || '');
161
+ return clean.length > 0
162
+ && clean.length <= 255
163
+ && clean !== '.'
164
+ && clean !== '..'
165
+ && !clean.includes('/')
166
+ && !clean.includes('\\')
167
+ && !clean.includes('\0')
168
+ && path.basename(clean) === clean;
169
+ }
170
+ function numberedUploadName(fileName, index) {
171
+ if (index <= 0)
172
+ return fileName;
173
+ const extension = path.extname(fileName);
174
+ const stem = fileName.slice(0, fileName.length - extension.length);
175
+ return `${stem} (${index})${extension}`;
176
+ }
177
+ async function handleMobileWorkspaceUpload(req, res) {
178
+ if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
179
+ mobileJson(res, { error: 'Remote touch disabled' }, 403);
180
+ req.resume();
181
+ return;
182
+ }
183
+ if (!mobileAuthorized(req)) {
184
+ mobileJson(res, { error: 'Unauthorized' }, 401);
185
+ req.resume();
186
+ return;
187
+ }
188
+ if (!agent) {
189
+ mobileJson(res, { error: 'Agent not initialized' }, 500);
190
+ req.resume();
191
+ return;
192
+ }
193
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
194
+ const workspaceId = url.searchParams.get('workspaceId') || '';
195
+ const directory = url.searchParams.get('directory') || '';
196
+ const fileName = url.searchParams.get('fileName') || '';
197
+ const ws = resolveMobileWorkspace(agent, workspaceId);
198
+ if (!ws) {
199
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
200
+ req.resume();
201
+ return;
202
+ }
203
+ if (!validMobileUploadDirectory(directory) || !validMobileUploadFileName(fileName)) {
204
+ mobileJson(res, { error: 'Invalid upload path or file name' }, 400);
205
+ req.resume();
206
+ return;
207
+ }
208
+ const declaredLength = Number(req.headers['content-length'] || -1);
209
+ if (Number.isFinite(declaredLength) && declaredLength > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES) {
210
+ mobileJson(res, { error: 'Upload exceeds the 64 MiB limit' }, 413);
211
+ req.resume();
212
+ return;
213
+ }
214
+ let temporaryPath = '';
215
+ try {
216
+ const resolved = resolveMobileWorkspacePath(ws, directory);
217
+ const realRoot = await fs.promises.realpath(resolved.root);
218
+ if (directory === 'Uploaded') {
219
+ await fs.promises.mkdir(resolved.target, { recursive: true });
220
+ }
221
+ const realDirectory = await fs.promises.realpath(resolved.target);
222
+ const canonicalRelative = path.relative(realRoot, realDirectory);
223
+ if (canonicalRelative === '..' || canonicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(canonicalRelative)) {
224
+ throw new Error('Path escapes workspace through a symbolic link');
225
+ }
226
+ const directoryStat = await fs.promises.stat(realDirectory);
227
+ if (!directoryStat.isDirectory()) {
228
+ mobileJson(res, { error: 'Upload destination is not a directory' }, 400);
229
+ req.resume();
230
+ return;
231
+ }
232
+ temporaryPath = path.join(realDirectory, `.newmark-upload-${(0, crypto_1.randomUUID)()}.tmp`);
233
+ const handle = await fs.promises.open(temporaryPath, 'wx', 0o600);
234
+ const hash = (0, crypto_1.createHash)('sha256');
235
+ let size = 0;
236
+ try {
237
+ for await (const part of req) {
238
+ const chunk = Buffer.isBuffer(part) ? part : Buffer.from(part);
239
+ size += chunk.length;
240
+ if (size > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES)
241
+ throw new RangeError('Upload exceeds the 64 MiB limit');
242
+ hash.update(chunk);
243
+ let offset = 0;
244
+ while (offset < chunk.length) {
245
+ const { bytesWritten } = await handle.write(chunk, offset, chunk.length - offset);
246
+ if (bytesWritten <= 0)
247
+ throw new Error('Unable to write the complete upload');
248
+ offset += bytesWritten;
249
+ }
250
+ }
251
+ await handle.sync();
252
+ }
253
+ finally {
254
+ await handle.close();
255
+ }
256
+ let finalPath = '';
257
+ let finalName = '';
258
+ let published = false;
259
+ for (let index = 0; index < 10_000; index++) {
260
+ finalName = numberedUploadName(fileName, index);
261
+ finalPath = path.join(realDirectory, finalName);
262
+ try {
263
+ // A hard link publishes the already-complete inode atomically and fails
264
+ // with EEXIST, so parallel uploads can never overwrite one another.
265
+ await fs.promises.link(temporaryPath, finalPath);
266
+ published = true;
267
+ break;
268
+ }
269
+ catch (error) {
270
+ if (error?.code === 'EEXIST')
271
+ continue;
272
+ throw error;
273
+ }
274
+ }
275
+ if (!published)
276
+ throw new Error('Unable to allocate a unique upload name');
277
+ await fs.promises.unlink(temporaryPath);
278
+ temporaryPath = '';
279
+ const relativePath = path.posix.join(resolved.relative, finalName).replace(/^\.\//, '');
280
+ const suppliedMime = String(req.headers['content-type'] || 'application/octet-stream').split(';')[0].trim();
281
+ const mimeType = /^[\w.+-]+\/[\w.+-]+$/.test(suppliedMime) ? suppliedMime : 'application/octet-stream';
282
+ mobileJson(res, {
283
+ ok: true,
284
+ file: { name: finalName, path: relativePath, size, mimeType, sha256: hash.digest('hex'), created: true },
285
+ }, 201);
286
+ }
287
+ catch (error) {
288
+ if (temporaryPath)
289
+ await fs.promises.unlink(temporaryPath).catch(() => { });
290
+ if (error instanceof RangeError)
291
+ mobileJson(res, { error: error.message }, 413);
292
+ else if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
293
+ mobileJson(res, { error: 'Upload destination does not exist' }, 400);
294
+ }
295
+ else if (String(error?.message || '').toLowerCase().includes('workspace')) {
296
+ mobileJson(res, { error: 'Invalid upload destination' }, 400);
297
+ }
298
+ else {
299
+ mobileJson(res, { error: 'Upload could not be stored' }, 500);
300
+ }
301
+ }
302
+ }
145
303
  function mobileRightSidebarState(current, ws, conversationId) {
146
304
  const scoped = mobileScopedAgent(ws, conversationId);
147
305
  return {
@@ -1483,10 +1641,30 @@ function startServer(root, options = {}) {
1483
1641
  handleMobileEvents(req, res);
1484
1642
  return;
1485
1643
  }
1644
+ if (req.method === 'POST' && url.pathname === '/api/mobile/workspace-file-upload') {
1645
+ await handleMobileWorkspaceUpload(req, res);
1646
+ return;
1647
+ }
1486
1648
  if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
1487
1649
  let body = '';
1488
- req.on('data', chunk => body += chunk);
1489
- req.on('end', () => handleApi(req, res, body));
1650
+ let bodyBytes = 0;
1651
+ let rejected = false;
1652
+ req.on('data', chunk => {
1653
+ if (rejected)
1654
+ return;
1655
+ bodyBytes += Buffer.byteLength(chunk);
1656
+ if (bodyBytes > MAX_API_BODY_BYTES) {
1657
+ rejected = true;
1658
+ if (url.pathname.startsWith('/api/mobile/'))
1659
+ mobileJson(res, { error: 'Request body too large' }, 413);
1660
+ else
1661
+ jsonResponse(res, { error: 'Request body too large' }, 413);
1662
+ return;
1663
+ }
1664
+ body += chunk;
1665
+ });
1666
+ req.on('end', () => { if (!rejected)
1667
+ handleApi(req, res, body); });
1490
1668
  return;
1491
1669
  }
1492
1670
  if (url.pathname.startsWith('/api/')) {
@@ -6156,6 +6156,8 @@ var state = {
6156
6156
  agentWorkUiByConversation: {},
6157
6157
  workRunsByTarget: {},
6158
6158
  workRunsByBranch: {},
6159
+ conversationMessagesByTarget: {},
6160
+ pendingConversationActivations: {},
6159
6161
  runtimeBranchPathsByTarget: {},
6160
6162
  workRunAnchorIndexesByTarget: {},
6161
6163
  guideMessagesByTarget: {},
@@ -9656,11 +9658,18 @@ function recordGuideUiMessage(input, target) {
9656
9658
  createdAt: String(receipt.createdAt || input.createdAt || (prior && prior.createdAt) || new Date().toISOString()),
9657
9659
  updatedAt: String(receipt.updatedAt || input.updatedAt || new Date().toISOString()),
9658
9660
  reason: explicitReason || (status === 'deferred' || status === 'rejected' ? String(prior && prior.reason || '') : ''),
9661
+ awaitingAck: input.awaitingAck === true || receipt.awaitingAck === true,
9659
9662
  attachments: normalizeConversationImageAttachments(input.attachments || receipt.attachments || (prior && prior.attachments) || [])
9660
9663
  };
9661
9664
  messages[clientMessageId] = record;
9662
9665
  if (isActiveConversationTarget(target)) {
9663
9666
  var element = findGuideMessageElement(clientMessageId);
9667
+ if (element) {
9668
+ if (prior && prior.awaitingAck && !record.awaitingAck && element.closest && element.closest('.work-run-pending-guides')) {
9669
+ element.remove();
9670
+ element = null;
9671
+ }
9672
+ }
9664
9673
  if (element) {
9665
9674
  applyGuideMessageMeta(element, record);
9666
9675
  if (record.runId && !element.classList.contains('work-run-guide-message')) element.remove();
@@ -9701,6 +9710,26 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
9701
9710
  });
9702
9711
  pending.forEach(function(item) {
9703
9712
  var element = findGuideMessageElement(item.clientMessageId);
9713
+ if (item.awaitingAck && item.runId) {
9714
+ var runElement = findWorkRunElement({ runId: item.runId });
9715
+ var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
9716
+ if (runBody) {
9717
+ var pendingStack = runBody.querySelector(':scope > .work-run-pending-guides');
9718
+ if (!pendingStack) {
9719
+ pendingStack = document.createElement('div');
9720
+ pendingStack.className = 'work-run-pending-guides';
9721
+ runBody.appendChild(pendingStack);
9722
+ }
9723
+ if (!element || !pendingStack.contains(element)) {
9724
+ var holder = document.createElement('div');
9725
+ holder.innerHTML = renderWorkRunGuideMessage(Object.assign({}, item, { status: 'accepted' }));
9726
+ element = holder.firstElementChild;
9727
+ if (element) pendingStack.appendChild(element);
9728
+ }
9729
+ if (element) applyGuideMessageMeta(element, item);
9730
+ return;
9731
+ }
9732
+ }
9704
9733
  if (element) applyGuideMessageMeta(element, item);
9705
9734
  else addMsg('user', item.content || 'Guide', 'guide', item.model || state.model, undefined, {
9706
9735
  clientMessageId: item.clientMessageId,
@@ -9964,7 +9993,7 @@ function normalizedWorkRun(run, target) {
9964
9993
  var status = String(run.status || 'running');
9965
9994
  var live = ['running', 'stopping', 'force_restarting'].indexOf(status) >= 0;
9966
9995
  var expanded = run.expanded === undefined
9967
- ? live && state.expandToolsDefault !== false
9996
+ ? (status === 'error' || (live && state.expandToolsDefault !== false))
9968
9997
  : !!run.expanded;
9969
9998
  return {
9970
9999
  runId: String(run.runId || run.id || ('run-' + Date.now() + '-' + Math.random().toString(16).slice(2))),
@@ -10632,6 +10661,7 @@ function updateConversationWorkRunElement(run, element) {
10632
10661
  '<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
10633
10662
  '<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
10634
10663
  '<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
10664
+ renderPendingGuideMessages(run.target, {});
10635
10665
  var wrapper = element.closest ? element.closest('.work-run-message') : null;
10636
10666
  if (wrapper) {
10637
10667
  wrapper.classList.toggle('flow-result-only', !!(run.flow && run.flow.activityVisibility === 'result-only'));
@@ -10778,14 +10808,14 @@ function applyAgentWorkEventToRun(event) {
10778
10808
  }
10779
10809
  run.sequence = Math.max(Number(run.sequence || 0), Number(event.sequence || 0));
10780
10810
  if (type === 'done') { run.status = 'completed'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
10781
- else if (type === 'error') { run.status = 'error'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
10811
+ else if (type === 'error') { run.status = 'error'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = true; }
10782
10812
  else if (type === 'interrupted') { run.status = 'interrupted'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
10783
10813
  else if (type === 'force_interrupted') { run.status = 'force_interrupted'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
10784
10814
  else if (event.status && type.indexOf('guide') !== 0) {
10785
10815
  run.status = String(event.status);
10786
10816
  if (['completed', 'interrupted', 'force_interrupted', 'error'].indexOf(run.status) >= 0) {
10787
10817
  run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString();
10788
- if (run.userToggled !== true) run.expanded = false;
10818
+ if (run.userToggled !== true) run.expanded = run.status === 'error';
10789
10819
  }
10790
10820
  }
10791
10821
  if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
@@ -11066,14 +11096,58 @@ function renderPersistedToolMessage(message) {
11066
11096
  }, resultPrefix.test(content) || /\bcompleted\.?$/i.test(content) ? 'completed' : '');
11067
11097
  }
11068
11098
 
11069
- function renderChatMessages(messages) {
11070
- state.renderedChatMessages = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
11099
+ function conversationMessageCache(target) {
11100
+ var scopedTarget = target || currentConversationTarget();
11101
+ var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
11102
+ if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
11103
+ return Array.isArray(state.conversationMessagesByTarget[key])
11104
+ ? state.conversationMessagesByTarget[key].map(function(message) { return Object.assign({}, message); })
11105
+ : [];
11106
+ }
11107
+
11108
+ function cacheConversationMessages(messages, target) {
11109
+ var scopedTarget = target || currentConversationTarget();
11110
+ var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
11111
+ var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
11112
+ if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
11113
+ state.conversationMessagesByTarget[key] = cloned;
11114
+ return cloned.map(function(message) { return Object.assign({}, message); });
11115
+ }
11116
+
11117
+ function conversationHasReadableHistory(target) {
11118
+ return conversationMessageCache(target).length > 0 || workRunsForTarget(target).length > 0;
11119
+ }
11120
+
11121
+ function snapshotHasReadableConversationHistory(snapshot) {
11122
+ return !!(snapshot && ((Array.isArray(snapshot.chatMessages) && snapshot.chatMessages.length > 0)
11123
+ || (Array.isArray(snapshot.workRuns) && snapshot.workRuns.length > 0)));
11124
+ }
11125
+
11126
+ function renderConversationHistoryFirst(target) {
11127
+ if (!isActiveConversationTarget(target)) return false;
11128
+ var cachedMessages = conversationMessageCache(target);
11129
+ if (cachedMessages.length > 0) {
11130
+ renderChatMessages(cachedMessages, target);
11131
+ renderLoadEarlierButton();
11132
+ return true;
11133
+ }
11134
+ if (workRunsForTarget(target).length > 0) {
11135
+ renderChatMessages([], target);
11136
+ renderLoadEarlierButton();
11137
+ return true;
11138
+ }
11139
+ return false;
11140
+ }
11141
+
11142
+ function renderChatMessages(messages, target) {
11143
+ var renderTarget = target || currentConversationTarget(activeConversationId());
11144
+ var normalizedMessages = cacheConversationMessages(messages, renderTarget);
11145
+ state.renderedChatMessages = normalizedMessages;
11071
11146
  if (!els['chat-area']) return;
11072
11147
  var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
11073
11148
  els['chat-area'].innerHTML = '';
11074
11149
  if (chatWasAtBottom) _chatShouldAutoScroll = true;
11075
- var renderConversationId = activeConversationId();
11076
- var renderTarget = currentConversationTarget(renderConversationId);
11150
+ var renderConversationId = renderTarget.conversationId;
11077
11151
  var guideMessages = guideMessagesForTarget(renderTarget);
11078
11152
  var persistedGuideIds = {};
11079
11153
  var persistedRuns = workRunsForTarget(renderTarget).slice();
@@ -11099,7 +11173,7 @@ function renderChatMessages(messages) {
11099
11173
  persistedRunIndexes[runId] = runIndex;
11100
11174
  }
11101
11175
  });
11102
- (Array.isArray(messages) ? messages : []).forEach(function(message, messageIndex) {
11176
+ normalizedMessages.forEach(function(message, messageIndex) {
11103
11177
  var runId = String(message && message.runId || '');
11104
11178
  var role = String(message && message.role || '');
11105
11179
  var indexedGuideId = String(message && message.clientMessageId || '');
@@ -11144,7 +11218,7 @@ function renderChatMessages(messages) {
11144
11218
  var pendingWorkReview = conversationWorkUiState(renderConversationId).pendingWorkReview;
11145
11219
  resetConversationWorkUi(renderConversationId, false);
11146
11220
  conversationWorkUiState(renderConversationId).pendingWorkReview = pendingWorkReview;
11147
- if (!messages || !messages.length) {
11221
+ if (!normalizedMessages.length) {
11148
11222
  persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
11149
11223
  var pendingGuideCount = renderPendingGuideMessages(renderTarget, persistedGuideIds);
11150
11224
  if (!persistedRuns.length && !pendingGuideCount) {
@@ -11582,8 +11656,19 @@ function loadActiveConversationMessages(conversationId) {
11582
11656
  requestedConversationId === String(activeConversationId() || 'default') &&
11583
11657
  responseConversationId === requestedConversationId;
11584
11658
  if (!responseIsCurrent) return;
11585
- applyConversationSnapshot(s, requestedConversationId);
11586
- }).catch(function(){});
11659
+ applyConversationSnapshot(s, requestedConversationId, { preserveReadableHistory: true, allowEmptyHistory: true });
11660
+ }).catch(function(error) {
11661
+ var stillCurrent = loadGeneration === state.conversationLoadGeneration &&
11662
+ requestedWorkspaceKey === currentWorkspaceKey() &&
11663
+ requestedConversationId === String(activeConversationId() || 'default');
11664
+ if (!stillCurrent) return;
11665
+ showUiNotice(
11666
+ currentLang() === 'zh' ? '历史记录增强加载失败,已保留当前内容。' : 'History enhancement failed; current content was preserved.',
11667
+ 'error',
11668
+ 'conversation-history-load-' + requestedWorkspaceKey + '-' + requestedConversationId
11669
+ );
11670
+ console.error('[Conversation] history enhancement failed:', error && error.message ? error.message : String(error));
11671
+ });
11587
11672
  }
11588
11673
 
11589
11674
  function hydrateConversationBranchState(snapshot) {
@@ -11600,7 +11685,7 @@ function hydrateConversationBranchState(snapshot) {
11600
11685
  rememberRuntimeConversationBranchPath(currentConversationTarget(s.conversationId || activeConversationId()));
11601
11686
  }
11602
11687
 
11603
- function applyConversationSnapshot(s, requestedConversationId) {
11688
+ function applyConversationSnapshot(s, requestedConversationId, options) {
11604
11689
  if (!s) return;
11605
11690
  if (s.computerUse) {
11606
11691
  var computerTarget = currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId());
@@ -11616,18 +11701,21 @@ function applyConversationSnapshot(s, requestedConversationId) {
11616
11701
  workspaceId: String((s && (s.workspaceId || (s.target && s.target.workspaceId))) || runtimeWorkspaceId('')),
11617
11702
  conversationId: String((s && (s.conversationId || (s.target && s.target.conversationId))) || requestedConversationId)
11618
11703
  };
11704
+ var preserveReadableHistory = !!(options && options.preserveReadableHistory
11705
+ && !snapshotHasReadableConversationHistory(s)
11706
+ && (!options.allowEmptyHistory || conversationHasReadableHistory(snapshotTarget)));
11619
11707
  if (s && s.runtimeKey) registerRuntimeKey(snapshotTarget, s.runtimeKey);
11620
11708
  hydrateConversationBranchState(s);
11621
11709
  rebindQueueToRuntimeBranch(snapshotTarget);
11622
- if (s && Array.isArray(s.workRuns)) {
11710
+ if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
11623
11711
  var viewedBranchIdForSync = Array.isArray(s.viewedBranchNodePath) && s.viewedBranchNodePath.length
11624
11712
  ? String(s.viewedBranchNodePath[s.viewedBranchNodePath.length - 1])
11625
11713
  : '';
11626
11714
  syncWorkRunsSnapshot(s.workRuns, snapshotTarget, viewedBranchIdForSync || undefined);
11627
11715
  }
11628
11716
  applyAutoRouteRatingState(s);
11629
- if (s && s.chatMessages) {
11630
- renderChatMessages(s.chatMessages);
11717
+ if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory) {
11718
+ renderChatMessages(s.chatMessages, snapshotTarget);
11631
11719
  renderLoadEarlierButton();
11632
11720
  }
11633
11721
  if (s && s.conversationId) {
@@ -13513,18 +13601,10 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13513
13601
  attachments: optimisticAttachments,
13514
13602
  model: state.model,
13515
13603
  createdAt: envelope.createdAt,
13604
+ awaitingAck: true,
13516
13605
  allowStatusReset: retryingUnacknowledgedGuide
13517
13606
  }, lockedTarget);
13518
- applyAgentWorkEventToRun({
13519
- id: 'guide-' + clientMessageId + '-accepted',
13520
- type: 'guide_accepted',
13521
- content: displayText,
13522
- workspaceId: lockedTarget.workspaceId,
13523
- conversationId: lockedTarget.conversationId,
13524
- runId: envelope.runId,
13525
- clientMessageId: clientMessageId,
13526
- guide: optimisticGuide
13527
- });
13607
+ renderPendingGuideMessages(lockedTarget, {});
13528
13608
  var enqueue = api.enqueueGuide ? api.enqueueGuide(envelope) : api.sendMessage(requestMessage, lockedConversationId);
13529
13609
  return Promise.resolve(enqueue).then(function(receipt) {
13530
13610
  if (!receipt || typeof receipt !== 'object' || !String(receipt.status || '').trim()) {
@@ -13680,6 +13760,8 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
13680
13760
  var responseMsg = null;
13681
13761
  var sendFailure = '';
13682
13762
  try {
13763
+ var activationBeforeSend = pendingConversationActivation(lockedTarget);
13764
+ if (activationBeforeSend) await activationBeforeSend;
13683
13765
  var sendPromise = api.sendMessage(requestMessage, lockedTarget);
13684
13766
  if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
13685
13767
  var r = await sendPromise;
@@ -19348,6 +19430,12 @@ function holdForegroundConversation(id, ms) {
19348
19430
  state.foregroundConversationHoldUntil = Date.now() + (ms || 4500);
19349
19431
  }
19350
19432
 
19433
+ function pendingConversationActivation(target) {
19434
+ var scopedTarget = target || currentConversationTarget();
19435
+ var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
19436
+ return state.pendingConversationActivations && state.pendingConversationActivations[key];
19437
+ }
19438
+
19351
19439
  function applyBackendConversations(items, activeId, workspaceId) {
19352
19440
  if (!state.currentWorkspace || !items || !items.length) return;
19353
19441
  var key = currentWorkspaceKey(workspaceId);
@@ -19409,14 +19497,24 @@ function syncBackendConversation() {
19409
19497
  }
19410
19498
  if (conv && api.ensureConversation) {
19411
19499
  var requestedTarget = currentConversationTarget(conv.id);
19500
+ state.activeBackendConversationId = conv.id;
19501
+ var historyPromise = loadActiveConversationMessages(conv.id);
19412
19502
  var activate = api.activateConversation ? api.activateConversation(requestedTarget) : api.ensureConversation(requestedTarget);
19413
- return activate.then(function(s) {
19503
+ var activationPromise = activate.then(function(s) {
19414
19504
  var id = (s && s.conversationId) || conv.id;
19415
19505
  if (!requestIsCurrent() || String(id) !== requestedConversationId) return;
19416
19506
  state.activeBackendConversationId = id;
19417
- applyConversationSnapshot(s, id);
19418
- if (s && s.runtimeDeferred) return loadActiveConversationMessages(id);
19419
- }).catch(function(){});
19507
+ applyConversationSnapshot(s, id, { preserveReadableHistory: true });
19508
+ }).catch(function(error) {
19509
+ if (!requestIsCurrent()) return;
19510
+ showUiNotice(
19511
+ currentLang() === 'zh' ? '对话运行时激活失败,历史记录仍可查看。' : 'Conversation runtime activation failed; history remains available.',
19512
+ 'error',
19513
+ 'conversation-activation-' + requestedWorkspaceKey + '-' + requestedConversationId
19514
+ );
19515
+ console.error('[Conversation] runtime activation failed:', error && error.message ? error.message : String(error));
19516
+ });
19517
+ return Promise.allSettled([historyPromise, activationPromise]);
19420
19518
  }
19421
19519
  if (conv) {
19422
19520
  if (!requestIsCurrent()) return Promise.resolve();
@@ -19632,10 +19730,11 @@ window.newConversation = function(workspaceReference, branchCommunication) {
19632
19730
  els['chat-area'].innerHTML = '';
19633
19731
  var target = currentConversationTarget(id);
19634
19732
  var activation = api.activateConversation ? api.activateConversation(target) : (api.ensureConversation ? api.ensureConversation(target) : Promise.resolve(null));
19635
- return activation.then(function(s) {
19733
+ var activationKey = runtimeKeyFor(target.workspaceId, target.conversationId);
19734
+ var activationReady = activation.then(function(s) {
19636
19735
  if (createdWorkspaceKey !== currentWorkspaceKey() || id !== String(activeConversationId() || 'default')) return;
19637
19736
  state.activeBackendConversationId = String((s && s.conversationId) || id);
19638
- if (s) applyConversationSnapshot(s, id);
19737
+ if (s) applyConversationSnapshot(s, id, { preserveReadableHistory: true });
19639
19738
  if (branchCommunication && api.setConversationBranchCommunication) {
19640
19739
  api.setConversationBranchCommunication(target, true).then(function() {
19641
19740
  var convsNow = currentWorkspaceConversations();
@@ -19645,16 +19744,23 @@ window.newConversation = function(workspaceReference, branchCommunication) {
19645
19744
  }
19646
19745
  }).then(function() {
19647
19746
  if (createdWorkspaceKey !== currentWorkspaceKey() || id !== String(activeConversationId() || 'default')) return;
19648
- state.foregroundConversationHoldId = '';
19649
- state.foregroundConversationHoldUntil = 0;
19650
- renderChatMessages([]);
19651
- addMsg('system', '[System] ' + t('workspace.newConversation') + ': ' + summary, 'system', '');
19747
+ if (!conversationHasReadableHistory(target)) {
19748
+ renderChatMessages([], target);
19749
+ addMsg('system', '[System] ' + t('workspace.newConversation') + ': ' + summary, 'system', '');
19750
+ }
19652
19751
  }).catch(function(error) {
19653
19752
  if (createdWorkspaceKey === currentWorkspaceKey() && id === String(activeConversationId() || 'default')) {
19654
19753
  showUiNotice((currentLang() === 'zh' ? '新对话持久化失败:' : 'Failed to persist the new conversation: ') + (error && error.message ? error.message : String(error)), 'error', 'new-conversation-activation-' + id);
19655
19754
  }
19656
19755
  throw error;
19756
+ }).finally(function() {
19757
+ if (state.pendingConversationActivations && state.pendingConversationActivations[activationKey] === activationReady) {
19758
+ delete state.pendingConversationActivations[activationKey];
19759
+ }
19657
19760
  });
19761
+ if (!state.pendingConversationActivations) state.pendingConversationActivations = {};
19762
+ state.pendingConversationActivations[activationKey] = activationReady;
19763
+ return activationReady;
19658
19764
  };
19659
19765
 
19660
19766
  window.showNewConversationPage = function() {
@@ -19724,7 +19830,7 @@ window.switchConversation = function(idx) {
19724
19830
  renderConversations();
19725
19831
  var switchSeq = (state._conversationSwitchSeq || 0) + 1;
19726
19832
  state._conversationSwitchSeq = switchSeq;
19727
- els['chat-area'].innerHTML = '';
19833
+ var restoredReadableHistory = renderConversationHistoryFirst(activeBrowserTarget);
19728
19834
  // The Flow takeover bubble is conversation-local. Immediately reconcile it to
19729
19835
  // the newly active conversation: if the incoming conversation owns a Flow,
19730
19836
  // show it; otherwise hide the bubble instantly so no takeover ever leaks
@@ -19733,7 +19839,9 @@ window.switchConversation = function(idx) {
19733
19839
  if (state._conversationLoadingMsgEl && state._conversationLoadingMsgEl.parentNode) {
19734
19840
  state._conversationLoadingMsgEl.parentNode.removeChild(state._conversationLoadingMsgEl);
19735
19841
  }
19736
- state._conversationLoadingMsgEl = addMsg('workflow running', t('conversation.loadingIsolated'), 'system', state.model);
19842
+ state._conversationLoadingMsgEl = restoredReadableHistory
19843
+ ? null
19844
+ : addMsg('workflow running', t('conversation.loadingIsolated'), 'system', state.model);
19737
19845
  syncBackendConversation().then(function() {
19738
19846
  // Only the latest switch may touch the UI: an earlier switch's sync
19739
19847
  // resolution must not restore drafts into or re-render the now-current