newmark-agent 0.5.14 → 0.6.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.
Files changed (68) hide show
  1. package/dist/conversation-utility-host.bundle.cjs +4565 -2998
  2. package/dist/conversation-utility-host.js +1 -1
  3. package/dist/core/agent.d.ts +108 -16
  4. package/dist/core/agent.js +698 -195
  5. package/dist/core/agentKernel/agent.d.ts +1 -0
  6. package/dist/core/agentKernel/agent.js +9 -0
  7. package/dist/core/agentKernelDiagnostics.d.ts +4 -6
  8. package/dist/core/agentKernelDiagnostics.js +12 -9
  9. package/dist/core/agentKernelRunner.d.ts +5 -0
  10. package/dist/core/agentKernelRunner.js +250 -107
  11. package/dist/core/autoRouter.js +17 -27
  12. package/dist/core/config.d.ts +2 -0
  13. package/dist/core/config.js +7 -1
  14. package/dist/core/conversationCommandState.d.ts +17 -0
  15. package/dist/core/conversationCommandState.js +140 -0
  16. package/dist/core/conversationKernel.d.ts +25 -4
  17. package/dist/core/conversationKernel.js +360 -89
  18. package/dist/core/conversationListEvent.d.ts +6 -0
  19. package/dist/core/conversationListEvent.js +14 -0
  20. package/dist/core/electronUtilityAgentClient.d.ts +4 -1
  21. package/dist/core/electronUtilityAgentClient.js +4 -4
  22. package/dist/core/electronUtilityRuntimePool.d.ts +8 -2
  23. package/dist/core/electronUtilityRuntimePool.js +11 -5
  24. package/dist/core/installUpdate.js +41 -29
  25. package/dist/core/modelResponseHealth.d.ts +17 -0
  26. package/dist/core/modelResponseHealth.js +193 -0
  27. package/dist/core/providerUsageAccounting.d.ts +47 -0
  28. package/dist/core/providerUsageAccounting.js +71 -0
  29. package/dist/core/requestContextEstimate.d.ts +18 -0
  30. package/dist/core/requestContextEstimate.js +54 -0
  31. package/dist/core/subagent.d.ts +89 -5
  32. package/dist/core/subagent.js +264 -41
  33. package/dist/core/subagentCommunication.d.ts +43 -0
  34. package/dist/core/subagentCommunication.js +167 -0
  35. package/dist/core/types.d.ts +34 -1
  36. package/dist/core/utilityAgentProtocol.d.ts +4 -0
  37. package/dist/core/workEventCoalescer.js +4 -1
  38. package/dist/core/wslAgentClient.d.ts +18 -5
  39. package/dist/core/wslAgentClient.js +79 -27
  40. package/dist/core/wslAgentProtocol.d.ts +7 -0
  41. package/dist/core/wslAgentRuntimePool.d.ts +8 -2
  42. package/dist/core/wslAgentRuntimePool.js +19 -6
  43. package/dist/core/wslRuntimeProcessTree.d.ts +10 -0
  44. package/dist/core/wslRuntimeProcessTree.js +104 -0
  45. package/dist/llm/provider.d.ts +5 -12
  46. package/dist/llm/provider.js +150 -269
  47. package/dist/main.js +582 -356
  48. package/dist/preload.js +3 -3
  49. package/dist/providers/chat-completions.adapter.d.ts +2 -0
  50. package/dist/providers/chat-completions.adapter.js +84 -36
  51. package/dist/providers/provider-adapter.d.ts +2 -0
  52. package/dist/providers/provider-events.d.ts +23 -14
  53. package/dist/providers/provider-events.js +148 -43
  54. package/dist/providers/provider-headers.d.ts +7 -3
  55. package/dist/providers/provider-headers.js +109 -39
  56. package/dist/providers/provider-request-compat.d.ts +6 -0
  57. package/dist/providers/provider-request-compat.js +37 -0
  58. package/dist/providers/responses.adapter.d.ts +2 -0
  59. package/dist/providers/responses.adapter.js +192 -126
  60. package/dist/server.d.ts +9 -2
  61. package/dist/server.js +100 -24
  62. package/dist/tools/index.js +17 -1
  63. package/dist/ui/index.html +2515 -693
  64. package/dist/ui/lucide-sprite.svg +7 -0
  65. package/dist/ui/startup.html +6 -6
  66. package/dist/wsl-agent-host.bundle.cjs +4575 -3006
  67. package/dist/wsl-agent-host.js +9 -4
  68. package/package.json +23 -4
package/dist/main.js CHANGED
@@ -41,6 +41,8 @@ const crypto_1 = require("crypto");
41
41
  const child_process_1 = require("child_process");
42
42
  const agent_1 = require("./core/agent");
43
43
  const conversationKernel_1 = require("./core/conversationKernel");
44
+ const conversationCommandState_1 = require("./core/conversationCommandState");
45
+ const conversationListEvent_1 = require("./core/conversationListEvent");
44
46
  const conversationTarget_1 = require("./core/conversationTarget");
45
47
  const automation_1 = require("./core/automation");
46
48
  const automationWake_1 = require("./core/automationWake");
@@ -107,7 +109,25 @@ function activeFlowStateKey(target) {
107
109
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
108
110
  }
109
111
  function activeFlowStateFor(target) {
110
- return activeFlowsByRuntimeKey.get(activeFlowStateKey(target)) || null;
112
+ const key = activeFlowStateKey(target);
113
+ const current = activeFlowsByRuntimeKey.get(key);
114
+ if (current)
115
+ return current;
116
+ const saved = agent?.getStoredFlowSuspension(target.conversationId, (target.workspace || null));
117
+ if (!agent || !saved || (saved.target && activeFlowStateKey(saved.target) !== key))
118
+ return null;
119
+ const dir = path.join(agent.rootPath, 'Flow');
120
+ const found = flow_1.FlowEngine.findWorkflow(saved.workflowName, dir);
121
+ const workflow = found ? flow_1.FlowEngine.load(dir, found) : null;
122
+ if (!workflow)
123
+ return null;
124
+ const restored = {
125
+ workflow, input: saved.input, componentId: saved.componentId, completedResults: saved.completedResults,
126
+ previousMode: saved.previousMode, previousFlow: null, previousPc: 0, queueWasPaused: saved.queueWasPaused,
127
+ reason: saved.reason, message: saved.message, target, abortController: null, name: workflow.name, flowAgent: null,
128
+ };
129
+ activeFlowsByRuntimeKey.set(key, restored);
130
+ return restored;
111
131
  }
112
132
  let wslAgentClient = null;
113
133
  let electronUtilityRuntimePool = null;
@@ -216,8 +236,9 @@ async function runShellCommand(command, shellId, cwd) {
216
236
  return { output };
217
237
  }
218
238
  function resetConversationKernel() {
219
- if (conversationKernel?.isAnyRunning())
239
+ if (conversationKernel?.hasRetainedWork())
220
240
  return;
241
+ conversationKernel?.disposeIdle();
221
242
  conversationKernel = null;
222
243
  }
223
244
  function runCommandAsync(command, args, cwd, timeoutMs = 30000) {
@@ -339,8 +360,13 @@ function dispatchAgentWorkEvent(event, mirrorToMobile = true) {
339
360
  }
340
361
  const workEventCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => dispatchAgentWorkEvent(event, true));
341
362
  const workEventNoMobileCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => dispatchAgentWorkEvent(event, false));
363
+ let projectConversationEvent = null;
364
+ function publishConversationList(workspace = agent?.workspace.current || null) {
365
+ if (agent && workspace)
366
+ broadcastAgentWorkEvent((0, conversationListEvent_1.conversationListEvent)(agent, workspace));
367
+ }
342
368
  function broadcastAgentWorkEvent(event, mirrorToMobile = true) {
343
- const workEvent = event;
369
+ const workEvent = projectConversationEvent?.(event) || event;
344
370
  // Response and thought deltas are coalesced at the IPC/SSE boundary; all
345
371
  // lifecycle/tool events retain immediate ordering.
346
372
  if (workEvent.type === 'text' || workEvent.type === 'thought_delta') {
@@ -349,7 +375,7 @@ function broadcastAgentWorkEvent(event, mirrorToMobile = true) {
349
375
  }
350
376
  workEventCoalescer.flushAll();
351
377
  workEventNoMobileCoalescer.flushAll();
352
- dispatchAgentWorkEvent(event, mirrorToMobile);
378
+ dispatchAgentWorkEvent(workEvent, mirrorToMobile);
353
379
  }
354
380
  function ensureConversationKernel(root) {
355
381
  if (!agent)
@@ -367,6 +393,7 @@ function persistedFlowSuspensionRecord(suspension, message = '') {
367
393
  input: String(suspension.input || ''),
368
394
  completedResults: Array.isArray(suspension.completedResults) ? suspension.completedResults : [],
369
395
  previousMode: suspension.previousMode,
396
+ queueWasPaused: suspension.queueWasPaused,
370
397
  reason: suspension.reason === '' ? 'interrupted' : suspension.reason,
371
398
  message: String(message || suspension.message || ''),
372
399
  target: suspension.target ? {
@@ -1303,12 +1330,12 @@ function viewerDocument(request) {
1303
1330
  ? `<img src="${viewerEscape(request.dataUrl)}" alt="${title}">`
1304
1331
  : '<div class="empty">Image unavailable</div>';
1305
1332
  return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'"><title>${title}</title><style>
1306
- :root{color-scheme:dark}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#090d16;color:#dce8f7;font:12px system-ui,sans-serif}main{height:100%;display:grid;grid-template-rows:auto 1fr;padding:14px;gap:10px}header{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#a8bdd4}section{min-height:0;display:flex;align-items:center;justify-content:center;border:1px solid #233044;border-radius:10px;background:#0d1420}img{display:block;max-width:100%;max-height:100%;object-fit:contain}svg{width:100%;height:100%}.edges line{stroke:#30435b;stroke-width:1;opacity:.62}text{fill:#9fb2c7;font-size:9px}.empty{color:#6f8196}</style></head><body><main><header>${title}</header><section>${image}</section></main></body></html>`;
1333
+ :root{color-scheme:dark}*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#101010;color:#f2f2f2;font:12px system-ui,sans-serif}main{height:100%;display:grid;grid-template-rows:auto 1fr;padding:14px;gap:10px}header{font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#cecece}section{min-height:0;display:flex;align-items:center;justify-content:center;border:1px solid #383838;border-radius:10px;background:#181818}img{display:block;max-width:100%;max-height:100%;object-fit:contain}svg{width:100%;height:100%}.edges line{stroke:#484848;stroke-width:1;opacity:.62}text{fill:#cecece;font-size:9px}.empty{color:#949494}</style></head><body><main><header>${title}</header><section>${image}</section></main></body></html>`;
1307
1334
  }
1308
1335
  async function createViewerWindow(request) {
1309
1336
  const viewerRequest = request && typeof request === 'object' ? request : {};
1310
1337
  const windowTitle = String(viewerRequest.title || viewerRequest.caption || (viewerRequest.type === 'memory-overview' ? 'Memory Lab Overview' : '示意图')).trim().slice(0, 160) || '示意图';
1311
- const win = new electron_1.BrowserWindow({ width: 760, height: 600, minWidth: 420, minHeight: 320, show: false, autoHideMenuBar: true, title: windowTitle, backgroundColor: '#090d16', webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true } });
1338
+ const win = new electron_1.BrowserWindow({ width: 760, height: 600, minWidth: 420, minHeight: 320, show: false, autoHideMenuBar: true, title: windowTitle, backgroundColor: '#101010', webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true } });
1312
1339
  if (viewerRequest.type === 'memory-overview') {
1313
1340
  await win.loadFile(path.join(__dirname, 'ui', 'index.html'), {
1314
1341
  query: { newmarkViewer: 'memory-overview', viewerTitle: windowTitle },
@@ -1827,7 +1854,7 @@ else {
1827
1854
  frame: false,
1828
1855
  title: 'Newmark Agent',
1829
1856
  icon: themedAppIconPath(),
1830
- backgroundColor: '#0a0a1a',
1857
+ backgroundColor: '#101010',
1831
1858
  webPreferences: {
1832
1859
  preload: path.join(__dirname, 'preload.js'),
1833
1860
  contextIsolation: true,
@@ -1989,6 +2016,7 @@ else {
1989
2016
  componentId: Math.max(0, Math.floor(Number(stored.componentId) || 0)),
1990
2017
  completedResults: Array.isArray(stored.completedResults) ? stored.completedResults : [],
1991
2018
  previousMode: stored.previousMode === 'plan' || stored.previousMode === 'goal' ? stored.previousMode : 'build',
2019
+ queueWasPaused: stored.queueWasPaused,
1992
2020
  previousFlow: null,
1993
2021
  previousPc: 0,
1994
2022
  reason,
@@ -2053,179 +2081,9 @@ else {
2053
2081
  mobileServerWorkEventSubscribers.add(listener);
2054
2082
  return () => mobileServerWorkEventSubscribers.delete(listener);
2055
2083
  },
2056
- conversationUiState: async (requested) => {
2057
- const target = conversationRuntimeTarget(requested);
2058
- const snapshot = wslBackendEnabled()
2059
- ? await ensureWslConversationPool().snapshot(target)
2060
- : await ensureElectronUtilityPool().snapshot(target);
2061
- const flowRunning = flowRunningForTarget(target);
2062
- const flowSuspension = flowSuspensionForTarget(target);
2063
- return {
2064
- ...snapshot,
2065
- flow: flowRunning ? {
2066
- running: true,
2067
- paused: false,
2068
- name: flowRunning.name,
2069
- promptText: String(activeFlowStateFor(target)?.input || ''),
2070
- message: '',
2071
- } : flowSuspension ? {
2072
- running: true,
2073
- paused: true,
2074
- name: flowSuspension.workflowName || '',
2075
- promptText: String(flowSuspension.input || ''),
2076
- message: String(flowSuspension.message || ''),
2077
- reason: flowSuspension.reason || '',
2078
- } : null,
2079
- };
2080
- },
2081
- conversationUiAction: async (requested, action, value, input) => {
2082
- const target = conversationRuntimeTarget(requested);
2083
- if (action.startsWith('queue_')) {
2084
- const queueAction = action === 'queue_enqueue' ? 'enqueue'
2085
- : action === 'queue_update' ? 'update'
2086
- : action === 'queue_delete' ? 'delete'
2087
- : action === 'queue_reorder' ? 'reorder'
2088
- : action === 'queue_toggle_pause' ? 'toggle_pause'
2089
- : 'guide';
2090
- const queueInput = {
2091
- id: String(input?.id || ''),
2092
- text: String(input?.text || value || ''),
2093
- requestedMode: String(input?.requestedMode || 'build'),
2094
- goalObjective: String(input?.goalObjective || ''),
2095
- createdAt: String(input?.createdAt || ''),
2096
- orderedIds: Array.isArray(input?.orderedIds)
2097
- ? input.orderedIds.map((id) => String(id || ''))
2098
- : undefined,
2099
- };
2100
- return wslBackendEnabled()
2101
- ? await ensureWslConversationPool().queueAction(target, queueAction, queueInput)
2102
- : await ensureElectronUtilityPool().queueAction(target, queueAction, queueInput);
2103
- }
2104
- if (action === 'goal_update') {
2105
- return { goal: await mutateTargetConversation(target, () => {
2106
- const isolated = isolatedConversationAgent(target);
2107
- isolated.updateGoal(String(value || '').trim());
2108
- isolated.setMode('goal');
2109
- isolated.saveWorkspaceConversationState(true);
2110
- return isolated.getConversationSnapshot(target.conversationId).goal;
2111
- }) };
2112
- }
2113
- if (action === 'goal_guide' || action === 'conversation_guide') {
2114
- const objective = action === 'goal_guide' ? String(value || '').trim() : '';
2115
- const guideText = String(value || '').trim();
2116
- const snapshot = wslBackendEnabled()
2117
- ? await ensureWslConversationPool().snapshot(target)
2118
- : await ensureElectronUtilityPool().snapshot(target);
2119
- const runtime = snapshot.runtime;
2120
- // `running` may turn false one IPC task before a finalization-
2121
- // window Guide reaches the worker. Preserve the authoritative
2122
- // runId and let ConversationKernel decide whether that run can
2123
- // be reactivated or must reject a genuinely stale request.
2124
- if (!guideText || !runtime?.runId) {
2125
- return { ok: false, error: action === 'goal_guide'
2126
- ? 'There is no active Build for this Goal Guide.'
2127
- : 'Target conversation is not running.' };
2128
- }
2129
- const prefix = 'Goal for the current Build:\n';
2130
- const now = new Date().toISOString();
2131
- const envelope = {
2132
- clientMessageId: (0, crypto_1.randomUUID)(),
2133
- guideId: (0, crypto_1.randomUUID)(),
2134
- target: { workspaceId: target.workspaceId, conversationId: target.conversationId },
2135
- runId: runtime.runId,
2136
- deliveryMode: 'steer',
2137
- text: objective ? prefix + objective : guideText,
2138
- goalObjective: objective || undefined,
2139
- createdAt: now,
2140
- };
2141
- const receipt = wslBackendEnabled()
2142
- ? await ensureWslConversationPool().enqueueGuide(envelope)
2143
- : await ensureElectronUtilityPool().enqueueGuide(envelope);
2144
- return { ok: receipt.status !== 'rejected', receipt };
2145
- }
2146
- if (action === 'goal_toggle_pause') {
2147
- const resident = wslBackendEnabled()
2148
- ? await ensureWslConversationPool().toggleGoalPause(target)
2149
- : await ensureElectronUtilityPool().toggleGoalPause(target);
2150
- const paused = resident !== null ? resident : await mutateTargetConversation(target, () => isolatedConversationAgent(target).toggleGoalPause());
2151
- return { ok: true, paused };
2152
- }
2153
- if (action === 'goal_clear') {
2154
- const resident = wslBackendEnabled()
2155
- ? await ensureWslConversationPool().clearGoal(target)
2156
- : await ensureElectronUtilityPool().clearGoal(target);
2157
- if (resident === null)
2158
- await mutateTargetConversation(target, () => isolatedConversationAgent(target).clearGoal());
2159
- return { ok: true, cleared: true };
2160
- }
2161
- if (action === 'flow_pause')
2162
- return await stopFlowForTarget(target);
2163
- if (action === 'flow_resume')
2164
- return await resumeFlowForTarget(String(value || ''), target);
2165
- if (action === 'conversation_stop') {
2166
- const snapshot = wslBackendEnabled()
2167
- ? await ensureWslConversationPool().snapshot(target)
2168
- : await ensureElectronUtilityPool().snapshot(target);
2169
- const runtime = snapshot.runtime;
2170
- return wslBackendEnabled()
2171
- ? await ensureWslConversationPool().requestStop(target, runtime?.runId)
2172
- : await ensureElectronUtilityPool().requestStop(target, runtime?.runId);
2173
- }
2174
- if (action === 'input_mode') {
2175
- const mode = String(value || '') === 'next' ? 'next' : 'guide';
2176
- const selected = wslBackendEnabled()
2177
- ? await ensureWslConversationPool().setInputMode(target, mode)
2178
- : await ensureElectronUtilityPool().setInputMode(target, mode);
2179
- return { ok: true, inputMode: selected || mode };
2180
- }
2181
- const flow = activeFlowStateFor(target);
2182
- if (!flow?.abortController || !flow.flowAgent)
2183
- return { ok: false, error: 'No active Flow accepts Guide input.' };
2184
- const accepted = flow.flowAgent.queueActiveKernelMessage(String(value || '').trim(), 'steer') || false;
2185
- return accepted ? { ok: true, accepted: true, flow: flow.name } : { ok: false, error: 'The current Flow Build is not accepting Guide input.' };
2186
- },
2187
- conversationPrompt: async (requested, message, requestedOptions) => {
2188
- const target = conversationRuntimeTarget(requested);
2189
- if (activeFlowStateFor(target))
2190
- clearFlowSuspensionForNewWork(target);
2191
- const snapshot = wslBackendEnabled()
2192
- ? await ensureWslConversationPool().snapshot(target)
2193
- : await ensureElectronUtilityPool().snapshot(target);
2194
- const requestedMode = String(requestedOptions?.requestedMode || '').toLowerCase();
2195
- const goalObjective = String(requestedOptions?.goalObjective || '').trim();
2196
- const mode = (goalObjective || requestedMode === 'goal') ? 'build' : String(snapshot.mode || 'build');
2197
- const inputMode = String(requestedOptions?.inputMode || snapshot.inputMode || 'guide') === 'next' ? 'next' : 'guide';
2198
- const options = {
2199
- mode,
2200
- model: String(snapshot.model || agent.ensureUsableModelSelection()),
2201
- intelligence: String(snapshot.intelligence || agent.intelligence),
2202
- inputMode,
2203
- engine: agent.engine,
2204
- };
2205
- const queueMode = inputMode === 'guide' ? 'steer' : 'followUp';
2206
- const promptMessage = goalObjective ? {
2207
- text: message,
2208
- visibleMode: 'goal',
2209
- goalObjective,
2210
- } : message;
2211
- const result = wslBackendEnabled()
2212
- ? await ensureWslConversationPool().prompt({
2213
- message: promptMessage,
2214
- target,
2215
- conversationId: target.conversationId,
2216
- options,
2217
- queueMode,
2218
- workspace: target.workspace ? {
2219
- id: target.workspace.id,
2220
- name: target.workspace.name,
2221
- path: target.workspace.path,
2222
- isInternal: target.workspace.isInternal,
2223
- kind: target.workspace.kind,
2224
- } : null,
2225
- })
2226
- : await ensureElectronUtilityPool().prompt({ message: promptMessage, target, options, queueMode });
2227
- return { ...result };
2228
- },
2084
+ conversationUiState: async (requested, options) => await runtimeSnapshotForTarget(conversationRuntimeTarget(requested), options),
2085
+ conversationUiAction: async (requested, action, value, input) => await applyConversationAction(requested, action, value, input),
2086
+ conversationPrompt: async (requested, message, options) => await submitConversationCommand(message, requested, options),
2229
2087
  });
2230
2088
  if (agent.config.getBool('remote', 'touch_enabled')) {
2231
2089
  runServer(root);
@@ -2237,7 +2095,7 @@ else {
2237
2095
  }
2238
2096
  }
2239
2097
  };
2240
- const localConversationSnapshotForStartup = (target) => {
2098
+ const localConversationSnapshotForStartup = (target, options = {}) => {
2241
2099
  const startupAgent = agent;
2242
2100
  if (!startupAgent)
2243
2101
  throw new Error('Agent is unavailable for startup conversation hydration');
@@ -2246,7 +2104,7 @@ else {
2246
2104
  if (normalizedTarget.runtimeKey !== currentTarget.runtimeKey) {
2247
2105
  throw new Error('Startup hydration requested a conversation other than the current Agent conversation');
2248
2106
  }
2249
- const conversationSnapshot = startupAgent.ensureConversationSnapshot(target.conversationId);
2107
+ const conversationSnapshot = startupAgent.ensureConversationSnapshot(target.conversationId, options);
2250
2108
  if (conversationSnapshot.conversationId !== normalizedTarget.conversationId) {
2251
2109
  throw new Error('Startup conversation snapshot identity mismatch');
2252
2110
  }
@@ -2835,7 +2693,7 @@ else {
2835
2693
  if (state && (state.reason === 'question' || state.reason === 'interrupted')) {
2836
2694
  return persistedFlowSuspensionRecord(state, state.message || '');
2837
2695
  }
2838
- const stored = agent?.getStoredFlowSuspension(target.conversationId);
2696
+ const stored = agent?.getStoredFlowSuspension(target.conversationId, (target.workspace || null));
2839
2697
  if (!stored)
2840
2698
  return null;
2841
2699
  if (!stored.target)
@@ -2846,6 +2704,9 @@ else {
2846
2704
  };
2847
2705
  const flowRunningForTarget = (target) => {
2848
2706
  const state = activeFlowStateFor(target);
2707
+ const pending = pendingFlowStarts.get(activeFlowStateKey(target));
2708
+ if (!state && pending)
2709
+ return { name: pending.name, promptText: pending.input, waiting: true };
2849
2710
  if (!state || state.reason === 'question' || state.reason === 'interrupted')
2850
2711
  return null;
2851
2712
  return { name: state.name || state.workflow.name || '' };
@@ -2854,7 +2715,12 @@ else {
2854
2715
  workspaceId: owner.workspace.current?.id || 'none',
2855
2716
  conversationId: owner.activeConversationId || 'default',
2856
2717
  });
2857
- const setTargetFlowMode = async (target, mode) => {
2718
+ const setTargetFlowMode = async (target, mode, restoreSelection = false) => {
2719
+ if (restoreSelection) {
2720
+ const selected = conversationSelections.get(activeFlowStateKey(target))?.mode;
2721
+ mode = selected && selected !== 'flow' ? selected : mode === 'flow' ? 'build' : mode;
2722
+ conversationSelections.set(activeFlowStateKey(target), { mode });
2723
+ }
2858
2724
  conversationKernel?.setMode(target, mode);
2859
2725
  if (wslBackendEnabled())
2860
2726
  await ensureWslConversationPool().setMode(target, mode);
@@ -2877,22 +2743,8 @@ else {
2877
2743
  owner.flowPc = suspension.previousPc;
2878
2744
  owner.setMode(suspension.previousMode);
2879
2745
  }
2880
- agent.clearStoredFlowSuspension(suspension.target.conversationId);
2881
- await setTargetFlowMode(suspension.target, suspension.previousMode);
2882
- };
2883
- // Drops a paused Flow for exactly one conversation without restoring the
2884
- // previous mode so a user's new Build/Plan/Goal/Flow instruction owns the
2885
- // current mode. The renderer keeps the mode in sync through agent:setMode
2886
- // before the send lands. Other conversations' Flow state is untouched.
2887
- const clearFlowSuspensionForNewWork = (target) => {
2888
- if (!agent)
2889
- return;
2890
- const key = activeFlowStateKey(target);
2891
- const state = activeFlowsByRuntimeKey.get(key) || null;
2892
- if (!state)
2893
- return;
2894
- activeFlowsByRuntimeKey.delete(key);
2895
- agent.clearStoredFlowSuspension(state.target.conversationId);
2746
+ agent.clearStoredFlowSuspension(suspension.target.conversationId, (suspension.target.workspace || null));
2747
+ await setTargetFlowMode(suspension.target, suspension.previousMode, true);
2896
2748
  };
2897
2749
  // Archive is a destructive lifecycle boundary. It must cancel the Flow
2898
2750
  // owner itself before touching the runtime pool; Flow runs bypass the
@@ -3066,10 +2918,58 @@ else {
3066
2918
  throw new Error('This conversation or workspace is being mutated. Retry after the operation completes.');
3067
2919
  }
3068
2920
  };
3069
- const runtimeSnapshotForTarget = async (target) => {
3070
- return wslBackendEnabled()
3071
- ? await ensureWslConversationPool().snapshot(target)
3072
- : await ensureElectronUtilityPool().snapshot(target);
2921
+ const conversationSelections = new conversationCommandState_1.ConversationCommandStateStore(root);
2922
+ const conversationCommands = new Map();
2923
+ const mainConversationOwners = new Set();
2924
+ const pendingFlowStarts = new Map();
2925
+ projectConversationEvent = event => {
2926
+ if (event.type !== 'queue_update' || !event.workspaceId || !event.conversationId)
2927
+ return event;
2928
+ const target = conversationRuntimeTarget({ workspaceId: event.workspaceId, conversationId: event.conversationId });
2929
+ const selected = conversationSelections.get(activeFlowStateKey(target));
2930
+ if (typeof event.queuePaused === 'boolean')
2931
+ conversationSelections.set(activeFlowStateKey(target), { queuePaused: event.queuePaused });
2932
+ return {
2933
+ ...event,
2934
+ stateScope: 'conversation',
2935
+ mode: selected?.mode || (activeFlowStateFor(target) ? 'flow' : event.mode),
2936
+ inputMode: selected?.inputMode,
2937
+ flowRunning: flowRunningForTarget(target),
2938
+ flowSuspension: flowSuspensionForTarget(target),
2939
+ };
2940
+ };
2941
+ const runtimeSnapshotForTarget = async (target, options = {}) => {
2942
+ const key = activeFlowStateKey(target);
2943
+ let snapshot = mainConversationOwners.has(key)
2944
+ ? ensureConversationKernel(root).snapshot(target, options)
2945
+ : wslBackendEnabled()
2946
+ ? await ensureWslConversationPool().snapshot(target, options)
2947
+ : await ensureElectronUtilityPool().snapshot(target, options);
2948
+ const flow = activeFlowStateFor(target);
2949
+ const selected = conversationSelections.get(key);
2950
+ if (!flow && !pendingFlowStarts.has(key) && selected?.queuePaused === true && !snapshot.queuePaused) {
2951
+ if (mainConversationOwners.has(key))
2952
+ ensureConversationKernel(root).queueAction(target, 'set_pause', { paused: true });
2953
+ else if (wslBackendEnabled())
2954
+ await ensureWslConversationPool().queueAction(target, 'set_pause', { paused: true });
2955
+ else
2956
+ await ensureElectronUtilityPool().queueAction(target, 'set_pause', { paused: true });
2957
+ snapshot = mainConversationOwners.has(key) ? ensureConversationKernel(root).snapshot(target, options)
2958
+ : wslBackendEnabled() ? await ensureWslConversationPool().snapshot(target, options) : await ensureElectronUtilityPool().snapshot(target, options);
2959
+ }
2960
+ return {
2961
+ ...snapshot,
2962
+ executionMode: snapshot.mode,
2963
+ mode: selected?.mode || (flow || pendingFlowStarts.has(key) ? 'flow' : snapshot.mode),
2964
+ inputMode: selected?.inputMode || snapshot.inputMode || 'guide',
2965
+ flow: flow ? {
2966
+ running: true, paused: flow.reason === 'interrupted', name: flow.name,
2967
+ promptText: flow.input, message: flow.message || '', reason: flow.reason,
2968
+ } : pendingFlowStarts.has(key) ? {
2969
+ running: true, paused: false, name: pendingFlowStarts.get(key).name,
2970
+ promptText: pendingFlowStarts.get(key).input, message: '', reason: '', waiting: true,
2971
+ } : null,
2972
+ };
3073
2973
  };
3074
2974
  const runtimeIsRestarting = (target) => wslBackendEnabled()
3075
2975
  ? !!wslAgentRuntimePool?.isRestarting(target)
@@ -3090,9 +2990,16 @@ else {
3090
2990
  throw new Error('Cannot mutate a conversation while its runtime is running or stopping.');
3091
2991
  }
3092
2992
  };
3093
- const peekTargetRuntime = (target) => wslBackendEnabled()
3094
- ? (wslAgentRuntimePool?.peek(target) || { resident: false, running: false, stopping: false, connected: false })
3095
- : (electronUtilityRuntimePool?.peek(target) || { resident: false, running: false, stopping: false, connected: false });
2993
+ const peekTargetRuntime = (target) => {
2994
+ if (mainConversationOwners.has(activeFlowStateKey(target))) {
2995
+ const runtime = conversationKernel?.runtimeState(target);
2996
+ if (runtime)
2997
+ return { resident: true, running: runtime.running, stopping: runtime.stopRequested, connected: true };
2998
+ }
2999
+ return wslBackendEnabled()
3000
+ ? (wslAgentRuntimePool?.peek(target) || { resident: false, running: false, stopping: false, connected: false })
3001
+ : (electronUtilityRuntimePool?.peek(target) || { resident: false, running: false, stopping: false, connected: false });
3002
+ };
3096
3003
  const stopTargetRuntime = async (target) => {
3097
3004
  if (wslBackendEnabled())
3098
3005
  await wslAgentRuntimePool?.stopTarget(target);
@@ -3128,6 +3035,14 @@ else {
3128
3035
  isolated.setConversation(normalized.conversationId);
3129
3036
  return isolated;
3130
3037
  };
3038
+ const conversationAgentForTarget = (target) => {
3039
+ if (mainConversationOwners.has(activeFlowStateKey(target))) {
3040
+ const owner = conversationKernel?.conversationOwner(target);
3041
+ if (owner)
3042
+ return owner;
3043
+ }
3044
+ return isolatedConversationAgent(target);
3045
+ };
3131
3046
  const mutateTargetConversation = async (target, mutation) => {
3132
3047
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
3133
3048
  assertTargetNotMutating(normalized);
@@ -3142,7 +3057,213 @@ else {
3142
3057
  mutatingRuntimeKeys.delete(normalized.runtimeKey);
3143
3058
  }
3144
3059
  };
3145
- electron_1.ipcMain.handle('agent:send', async (_event, message, targetInput) => {
3060
+ const selectConversationMode = async (target, raw) => {
3061
+ const mode = (['build', 'plan', 'chat', 'goal', 'flow'].includes(raw) ? raw : 'build');
3062
+ const key = activeFlowStateKey(target);
3063
+ conversationSelections.set(key, { ...conversationSelections.get(key), mode });
3064
+ if (!activeFlowStateFor(target)) {
3065
+ if (mainConversationOwners.has(key))
3066
+ ensureConversationKernel(root).setMode(target, mode);
3067
+ else {
3068
+ const result = wslBackendEnabled() ? await ensureWslConversationPool().setMode(target, mode) : await ensureElectronUtilityPool().setMode(target, mode);
3069
+ if (result === null)
3070
+ isolatedConversationAgent(target).selectConversationMode(mode);
3071
+ }
3072
+ }
3073
+ if (agent && activeFlowStateKey(conversationRuntimeTarget(agent.activeConversationId || 'default')) === key)
3074
+ agent.mode = mode;
3075
+ await publishConversationState(target);
3076
+ return mode;
3077
+ };
3078
+ const selectConversationInputMode = async (target, raw) => {
3079
+ const mode = raw === 'next' ? 'next' : 'guide';
3080
+ const key = activeFlowStateKey(target);
3081
+ conversationSelections.set(key, { ...conversationSelections.get(key), inputMode: mode });
3082
+ if (mainConversationOwners.has(key))
3083
+ ensureConversationKernel(root).setInputMode(target, mode);
3084
+ else {
3085
+ const selected = wslBackendEnabled() ? await ensureWslConversationPool().setInputMode(target, mode) : await ensureElectronUtilityPool().setInputMode(target, mode);
3086
+ if (selected === null)
3087
+ isolatedConversationAgent(target).setInputMode(mode);
3088
+ }
3089
+ if (agent && activeFlowStateKey(conversationRuntimeTarget(agent.activeConversationId || 'default')) === key)
3090
+ agent.inputMode = mode;
3091
+ await publishConversationState(target);
3092
+ return mode;
3093
+ };
3094
+ const publishConversationState = async (target) => {
3095
+ const snapshot = await runtimeSnapshotForTarget(target);
3096
+ broadcastAgentWorkEvent({
3097
+ id: (0, crypto_1.randomUUID)(), type: 'queue_update', content: '', timestamp: new Date().toISOString(),
3098
+ workspaceId: target.workspaceId, conversationId: target.conversationId, mode: snapshot.mode,
3099
+ model: snapshot.model, inputMode: snapshot.inputMode, queue: snapshot.queued,
3100
+ queueItems: snapshot.queueItems, queuePaused: snapshot.queuePaused,
3101
+ });
3102
+ };
3103
+ const enqueueConversationGuide = async (envelope) => {
3104
+ const target = conversationRuntimeTarget(envelope.target);
3105
+ return mainConversationOwners.has(activeFlowStateKey(target))
3106
+ ? ensureConversationKernel(root).enqueueGuide(envelope)
3107
+ : wslBackendEnabled() ? await ensureWslConversationPool().enqueueGuide(envelope) : await ensureElectronUtilityPool().enqueueGuide(envelope);
3108
+ };
3109
+ const mutateConversationQueue = async (target, rawAction, input = {}) => {
3110
+ const action = rawAction.replace(/^queue_/, '');
3111
+ if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'set_pause', 'guide'].includes(action))
3112
+ throw new Error('Unknown queue action');
3113
+ const key = activeFlowStateKey(target);
3114
+ const flow = activeFlowStateFor(target);
3115
+ const resumesQueue = action === 'set_pause' ? input.paused === false : action === 'toggle_pause' && flow;
3116
+ const pending = pendingFlowStarts.get(key);
3117
+ if (pending && (resumesQueue || action === 'toggle_pause')) {
3118
+ pending.cancelled = true;
3119
+ // Startup may be disposing the previous utility/WSL owner. Keep the
3120
+ // cancellation visible until that transfer settles before mutating it.
3121
+ await pending.settlement;
3122
+ }
3123
+ if (flow && resumesQueue) {
3124
+ if (flow.abortController) {
3125
+ await stopFlowForTarget(target);
3126
+ await flow.settlement;
3127
+ }
3128
+ await discardFlowSuspensionForTarget(target);
3129
+ ensureConversationKernel(root).releaseExternalRun(target, false);
3130
+ conversationSelections.set(key, { queuePaused: false });
3131
+ return { ok: true, ...await runtimeSnapshotForTarget(target) };
3132
+ }
3133
+ const result = mainConversationOwners.has(key)
3134
+ ? ensureConversationKernel(root).queueAction(target, action, input)
3135
+ : wslBackendEnabled() ? await ensureWslConversationPool().queueAction(target, action, input) : await ensureElectronUtilityPool().queueAction(target, action, input);
3136
+ if (typeof result.queuePaused === 'boolean')
3137
+ conversationSelections.set(key, { queuePaused: result.queuePaused });
3138
+ return { ...result, ...await runtimeSnapshotForTarget(target) };
3139
+ };
3140
+ const applyConversationAction = async (requested, action, value = '', input = {}) => {
3141
+ const target = conversationRuntimeTarget(requested);
3142
+ if (action === 'conversation_branch_inspect')
3143
+ return await inspectConversationBranchForTarget(target, String(input.branchId || ''), String(input.branchGroupId || ''));
3144
+ if (action === 'conversation_branch_activate')
3145
+ return await activateConversationBranchForTarget(target, String(input.branchId || ''), String(input.branchGroupId || ''));
3146
+ if (action === 'conversation_branch_create')
3147
+ return await branchConversationForTarget(target, Number(input.messageIndex), String(input.editedText ?? input.text ?? value), {
3148
+ messageId: input.messageId, guideId: input.guideId, clientMessageId: input.clientMessageId,
3149
+ runId: input.runId, branchNodePath: input.branchNodePath,
3150
+ });
3151
+ if (action === 'conversation_archive')
3152
+ return await archiveConversationForTarget(target);
3153
+ if (action.startsWith('queue_'))
3154
+ return await mutateConversationQueue(target, action, input);
3155
+ if (action === 'mode')
3156
+ return { ok: true, mode: await selectConversationMode(target, value), ...await runtimeSnapshotForTarget(target) };
3157
+ if (action === 'goal_update') {
3158
+ const goal = mainConversationOwners.has(activeFlowStateKey(target))
3159
+ ? ensureConversationKernel(root).updateGoal(target, String(value || '').trim())
3160
+ : await mutateTargetConversation(target, () => {
3161
+ const isolated = isolatedConversationAgent(target);
3162
+ isolated.updateGoal(String(value || '').trim());
3163
+ isolated.setMode('goal');
3164
+ isolated.saveWorkspaceConversationState(true);
3165
+ return isolated.getConversationSnapshot(target.conversationId).goal;
3166
+ });
3167
+ await selectConversationMode(target, 'goal');
3168
+ return { goal };
3169
+ }
3170
+ if (action === 'goal_guide' || action === 'conversation_guide') {
3171
+ const objective = action === 'goal_guide' ? String(value || '').trim() : '';
3172
+ const guideText = String(value || '').trim();
3173
+ const snapshot = await runtimeSnapshotForTarget(target);
3174
+ const runtime = snapshot.runtime;
3175
+ // `running` may turn false one IPC task before a finalization-
3176
+ // window Guide reaches the worker. Preserve the authoritative
3177
+ // runId and let ConversationKernel decide whether that run can
3178
+ // be reactivated or must reject a genuinely stale request.
3179
+ if (!guideText || !runtime?.runId) {
3180
+ return { ok: false, error: action === 'goal_guide'
3181
+ ? 'There is no active Build for this Goal Guide.'
3182
+ : 'Target conversation is not running.' };
3183
+ }
3184
+ const prefix = 'Goal for the current Build:\n';
3185
+ const now = new Date().toISOString();
3186
+ const envelope = {
3187
+ clientMessageId: (0, crypto_1.randomUUID)(),
3188
+ guideId: (0, crypto_1.randomUUID)(),
3189
+ target: { workspaceId: target.workspaceId, conversationId: target.conversationId },
3190
+ runId: runtime.runId,
3191
+ deliveryMode: 'steer',
3192
+ text: objective ? prefix + objective : guideText,
3193
+ goalObjective: objective || undefined,
3194
+ createdAt: now,
3195
+ };
3196
+ const receipt = await enqueueConversationGuide(envelope);
3197
+ return { ok: receipt.status !== 'rejected', receipt };
3198
+ }
3199
+ if (action === 'goal_toggle_pause') {
3200
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
3201
+ return { ok: true, paused: await ensureConversationKernel(root).toggleGoalPause(target) };
3202
+ const resident = wslBackendEnabled()
3203
+ ? await ensureWslConversationPool().toggleGoalPause(target)
3204
+ : await ensureElectronUtilityPool().toggleGoalPause(target);
3205
+ const paused = resident !== null ? resident : await mutateTargetConversation(target, () => isolatedConversationAgent(target).toggleGoalPause());
3206
+ return { ok: true, paused };
3207
+ }
3208
+ if (action === 'goal_clear') {
3209
+ if (mainConversationOwners.has(activeFlowStateKey(target))) {
3210
+ const cleared = ensureConversationKernel(root).clearGoal(target);
3211
+ if (conversationSelections.get(activeFlowStateKey(target))?.mode === 'goal')
3212
+ await selectConversationMode(target, 'build');
3213
+ return { ok: true, cleared };
3214
+ }
3215
+ const resident = wslBackendEnabled()
3216
+ ? await ensureWslConversationPool().clearGoal(target)
3217
+ : await ensureElectronUtilityPool().clearGoal(target);
3218
+ if (resident === null)
3219
+ await mutateTargetConversation(target, () => isolatedConversationAgent(target).clearGoal());
3220
+ if (conversationSelections.get(activeFlowStateKey(target))?.mode === 'goal')
3221
+ await selectConversationMode(target, 'build');
3222
+ return { ok: true, cleared: true };
3223
+ }
3224
+ if (action === 'flow_pause')
3225
+ return await stopFlowForTarget(target);
3226
+ if (action === 'flow_resume')
3227
+ return { ...await resumeFlowForTarget(String(value || ''), target), ...await runtimeSnapshotForTarget(target) };
3228
+ if (action === 'conversation_stop') {
3229
+ if (activeFlowStateFor(target) || pendingFlowStarts.has(activeFlowStateKey(target)))
3230
+ return await stopFlowForTarget(target);
3231
+ const snapshot = await runtimeSnapshotForTarget(target);
3232
+ const runId = snapshot.runtime?.runId;
3233
+ return mainConversationOwners.has(activeFlowStateKey(target))
3234
+ ? { ...ensureConversationKernel(root).requestStop(target, runId) }
3235
+ : wslBackendEnabled() ? await ensureWslConversationPool().requestStop(target, runId) : await ensureElectronUtilityPool().requestStop(target, runId);
3236
+ }
3237
+ if (action === 'input_mode')
3238
+ return { ok: true, inputMode: await selectConversationInputMode(target, value) };
3239
+ const flow = activeFlowStateFor(target);
3240
+ if (!flow?.abortController || !flow.flowAgent)
3241
+ return { ok: false, error: 'No active Flow accepts Guide input.' };
3242
+ const accepted = flow.flowAgent.queueActiveKernelMessage(String(value || '').trim(), 'steer') || false;
3243
+ return accepted ? { ok: true, accepted: true, flow: flow.name } : { ok: false, error: 'The current Flow Build is not accepting Guide input.' };
3244
+ };
3245
+ const submitConversationCommand = async (message, targetInput, requested = {}) => {
3246
+ const target = conversationRuntimeTarget(targetInput);
3247
+ const id = String(requested.clientMessageId || (typeof message === 'string' ? '' : message.clientMessageId) || (0, crypto_1.randomUUID)());
3248
+ const key = activeFlowStateKey(target) + '::' + id;
3249
+ const accepted = conversationCommands.get(key);
3250
+ if (accepted)
3251
+ return { ...await accepted, ...await runtimeSnapshotForTarget(target) };
3252
+ const work = executeConversationCommand(message, target, { ...requested, clientMessageId: id });
3253
+ conversationCommands.set(key, work);
3254
+ // Retain settled ids to reject ambiguous transport replays without
3255
+ // retaining an unbounded history or deduplicating different user inputs.
3256
+ void work.then(result => {
3257
+ conversationCommands.set(key, Promise.resolve({
3258
+ ...(result.error ? { error: result.error } : { ok: true }), duplicate: true,
3259
+ workspaceId: target.workspaceId, conversationId: target.conversationId,
3260
+ }));
3261
+ if (conversationCommands.size > 1000)
3262
+ conversationCommands.delete(conversationCommands.keys().next().value);
3263
+ }).catch(() => { });
3264
+ return await work;
3265
+ };
3266
+ const executeConversationCommand = async (message, targetInput, requested = {}) => {
3146
3267
  if (!agent)
3147
3268
  return { tokens: [], error: 'Agent not initialized' };
3148
3269
  let promptLeaseKey = '';
@@ -3154,8 +3275,42 @@ else {
3154
3275
  // conversation into the user's new process (Flow pause state exit rule).
3155
3276
  // flow:run already discards the suspension for a new Flow workflow.
3156
3277
  // Other conversations' paused Flows are never touched by this send.
3157
- if (activeFlowStateFor(target))
3158
- clearFlowSuspensionForNewWork(target);
3278
+ const flow = activeFlowStateFor(target);
3279
+ const snapshot = await runtimeSnapshotForTarget(target);
3280
+ const structured = typeof message === 'string' ? { text: message } : { ...message };
3281
+ const requestedMode = String(requested.requestedMode || structured.visibleMode || snapshot.mode || 'build');
3282
+ const inputMode = String(requested.inputMode || snapshot.inputMode || 'guide') === 'next' ? 'next' : 'guide';
3283
+ const objective = String(requested.goalObjective ?? structured.goalObjective ?? (requestedMode === 'goal' ? structured.text : '')).trim();
3284
+ const id = String(requested.clientMessageId || structured.clientMessageId || (0, crypto_1.randomUUID)());
3285
+ message = { ...structured, clientMessageId: id, userMessageId: id, visibleMode: requestedMode, goalObjective: objective || undefined };
3286
+ const runtime = snapshot.runtime;
3287
+ if (flow?.reason === 'interrupted') {
3288
+ await discardFlowSuspensionForTarget(target);
3289
+ ensureConversationKernel(root).releaseExternalRun(target, true);
3290
+ }
3291
+ if (requestedMode === 'flow' && !activeFlowStateFor(target) && !pendingFlowStarts.has(activeFlowStateKey(target))) {
3292
+ if (inputMode === 'guide' && runtime?.running)
3293
+ return { ok: false, error: 'Flow Guide cannot take over a running Build. Use Next to start after it finishes.' };
3294
+ const selectedFlow = snapshot.flowSelection;
3295
+ const name = String(requested.flowName || selectedFlow?.name || agent.flow?.name || agent.config.getStr('flow', 'default_flow') || '');
3296
+ return { ...await runFlowForTarget(name, structured.text, requested.flowStart || 0, target), ...await runtimeSnapshotForTarget(target) };
3297
+ }
3298
+ if (flow?.reason !== 'interrupted' && (flow || pendingFlowStarts.has(activeFlowStateKey(target)) || runtime?.running || snapshot.queuePaused)) {
3299
+ if (inputMode === 'guide' && (runtime?.running || flow?.abortController)) {
3300
+ const receipt = await enqueueConversationGuide({
3301
+ clientMessageId: id, guideId: structured.guideId || (0, crypto_1.randomUUID)(), target,
3302
+ runId: runtime?.runId || '', deliveryMode: 'steer', text: objective ? 'Goal for the current Build:\n' + objective : structured.text,
3303
+ goalObjective: objective || undefined, images: structured.images, createdAt: new Date().toISOString(),
3304
+ });
3305
+ return { ok: receipt.status !== 'rejected', accepted: receipt.status !== 'rejected', receipt, ...await runtimeSnapshotForTarget(target) };
3306
+ }
3307
+ return await applyConversationAction(target, 'queue_enqueue', '', {
3308
+ id, text: structured.visibleUserInput || structured.text, requestedMode: requestedMode === 'flow' ? 'build' : requestedMode,
3309
+ goalObjective: objective, images: structured.images, createdAt: new Date().toISOString(),
3310
+ });
3311
+ }
3312
+ await selectConversationMode(target, requestedMode);
3313
+ await selectConversationInputMode(target, inputMode);
3159
3314
  const normalizedPromptTarget = (0, conversationTarget_1.normalizeConversationTarget)(target);
3160
3315
  promptLeaseKey = normalizedPromptTarget.runtimeKey;
3161
3316
  promptLeaseWorkspaceKey = normalizedPromptTarget.workspaceKey;
@@ -3165,12 +3320,12 @@ else {
3165
3320
  // 发送命令时锁定输入框选择的模型:接受命令后的整个运行过程都以该
3166
3321
  // 模型为准(唯一例外是显式的不可用回退,且回退会以结构化事件同步
3167
3322
  // 到前端输入框下方的选择区,而不是隐藏的参数回退)。
3168
- const requestedModel = agent.model;
3323
+ const requestedModel = String(snapshot.model || agent.model);
3169
3324
  const options = {
3170
- mode: agent.mode,
3171
- model: agent.ensureUsableModelSelection(),
3172
- intelligence: agent.intelligence,
3173
- inputMode: agent.inputMode,
3325
+ mode: requestedMode === 'goal' ? 'goal' : requestedMode,
3326
+ model: String(snapshot.model || agent.ensureUsableModelSelection()),
3327
+ intelligence: String(snapshot.intelligence || agent.intelligence),
3328
+ inputMode: inputMode,
3174
3329
  engine: agent.engine,
3175
3330
  };
3176
3331
  if (options.model && requestedModel && options.model !== requestedModel) {
@@ -3188,9 +3343,12 @@ else {
3188
3343
  fallback: { from: requestedModel, to: options.model, providerId: usableConfig?.provider_id || agent.activeDeployment()?.providerId },
3189
3344
  });
3190
3345
  }
3191
- const queueMode = agent.inputMode === 'guide' ? 'steer' : 'followUp';
3346
+ const queueMode = inputMode === 'guide' ? 'steer' : 'followUp';
3192
3347
  let result;
3193
- if (wslBackendEnabled()) {
3348
+ if (mainConversationOwners.has(activeFlowStateKey(target))) {
3349
+ result = await ensureConversationKernel(root).prompt(message, target, options, queueMode);
3350
+ }
3351
+ else if (wslBackendEnabled()) {
3194
3352
  const pool = ensureWslConversationPool();
3195
3353
  if (!pool)
3196
3354
  return { error: 'WSL Agent backend is enabled but unavailable.' };
@@ -3220,6 +3378,7 @@ else {
3220
3378
  // target-scoped snapshot consumed by the renderer.
3221
3379
  return {
3222
3380
  ...result,
3381
+ ...await runtimeSnapshotForTarget(target),
3223
3382
  conversationId: targetConversation,
3224
3383
  activeConversationId: agent.activeConversationId || previousConversation,
3225
3384
  conversationLocked: false,
@@ -3245,7 +3404,8 @@ else {
3245
3404
  activePromptWorkspaces.delete(promptLeaseWorkspaceKey);
3246
3405
  }
3247
3406
  }
3248
- });
3407
+ };
3408
+ electron_1.ipcMain.handle('agent:send', async (_event, message, target, options) => await submitConversationCommand(message, target, options));
3249
3409
  electron_1.ipcMain.handle('browser:registerGuest', (event, guestContentsId, target) => {
3250
3410
  const guest = electron_1.webContents.fromId(Number(guestContentsId || 0));
3251
3411
  return { accepted: !!guest && registerBrowserGuest(event.sender, guest, target) };
@@ -3253,7 +3413,7 @@ else {
3253
3413
  electron_1.ipcMain.handle('browser:control', async (_event, request) => {
3254
3414
  return await browserControl_1.BrowserControl.run(request);
3255
3415
  });
3256
- electron_1.ipcMain.handle('flow:run', async (_event, name, input = '', start = 0) => {
3416
+ const runFlowForTarget = async (name, input = '', start = 0, requestedTarget) => {
3257
3417
  if (!agent)
3258
3418
  return { ok: false, error: 'Agent not initialized' };
3259
3419
  // Bind the Flow to the conversation that started it, exactly like resume.
@@ -3261,7 +3421,7 @@ else {
3261
3421
  // to its owning conversation id and branch, so switching conversations
3262
3422
  // mid-run can never make the next component write into another
3263
3423
  // conversation or strand the running flag.
3264
- const flowTarget = conversationRuntimeTarget(agent.activeConversationId || 'default');
3424
+ const flowTarget = conversationRuntimeTarget(requestedTarget || agent.activeConversationId || 'default');
3265
3425
  const flowKey = activeFlowStateKey(flowTarget);
3266
3426
  const existing = activeFlowsByRuntimeKey.get(flowKey) || null;
3267
3427
  if (existing && (existing.reason === 'question' || existing.reason === 'interrupted')) {
@@ -3277,12 +3437,45 @@ else {
3277
3437
  const workflow = flow_1.FlowEngine.load(flowDir, found);
3278
3438
  if (!workflow)
3279
3439
  return { ok: false, error: `Workflow failed to load: ${found}` };
3280
- const flowAgent = isolatedConversationAgent(flowTarget);
3440
+ if (pendingFlowStarts.has(flowKey))
3441
+ return { ok: false, error: 'A Flow is already waiting for this conversation.' };
3442
+ let resolvePendingSettlement;
3443
+ const pending = {
3444
+ name: workflow.name, input: String(input || ''), cancelled: false,
3445
+ settlement: new Promise(resolve => { resolvePendingSettlement = resolve; }),
3446
+ };
3447
+ pendingFlowStarts.set(flowKey, pending);
3448
+ let previousQueuePaused = false;
3449
+ try {
3450
+ let state = await runtimeSnapshotForTarget(flowTarget);
3451
+ previousQueuePaused = state.queuePaused === true;
3452
+ await mutateConversationQueue(flowTarget, 'queue_set_pause', { paused: true });
3453
+ while (state.runtime?.running) {
3454
+ if (pending.cancelled)
3455
+ return { ok: false, interrupted: true, error: 'Flow start cancelled.' };
3456
+ await new Promise(resolve => setTimeout(resolve, 50));
3457
+ state = await runtimeSnapshotForTarget(flowTarget);
3458
+ }
3459
+ if (pending.cancelled)
3460
+ return { ok: false, interrupted: true, error: 'Flow start cancelled.' };
3461
+ if (!mainConversationOwners.has(flowKey))
3462
+ await stopTargetRuntime(flowTarget);
3463
+ if (pending.cancelled)
3464
+ return { ok: false, interrupted: true, error: 'Flow start cancelled.' };
3465
+ }
3466
+ finally {
3467
+ if (pendingFlowStarts.get(flowKey) === pending)
3468
+ pendingFlowStarts.delete(flowKey);
3469
+ resolvePendingSettlement();
3470
+ }
3471
+ const flowAgent = ensureConversationKernel(root).beginExternalRun(flowTarget, {
3472
+ mode: 'flow', model: agent.model, intelligence: agent.intelligence, inputMode: agent.inputMode, engine: agent.engine,
3473
+ }, () => isolatedConversationAgent(flowTarget), previousQueuePaused);
3474
+ mainConversationOwners.add(flowKey);
3475
+ conversationSelections.set(flowKey, { ...conversationSelections.get(flowKey), mode: 'flow' });
3281
3476
  const previousMode = flowAgent.mode;
3282
3477
  const previousFlow = flowAgent.flow;
3283
3478
  const previousPc = flowAgent.flowPc;
3284
- if (previousMode === 'plan')
3285
- return { ok: false, error: 'Plan mode is fully read-only; Flow execution is blocked.' };
3286
3479
  const flowAbortController = new AbortController();
3287
3480
  const flowState = {
3288
3481
  workflow,
@@ -3290,6 +3483,7 @@ else {
3290
3483
  componentId: 0,
3291
3484
  completedResults: [],
3292
3485
  previousMode,
3486
+ queueWasPaused: previousQueuePaused,
3293
3487
  previousFlow,
3294
3488
  previousPc,
3295
3489
  reason: '',
@@ -3299,7 +3493,10 @@ else {
3299
3493
  flowAgent,
3300
3494
  };
3301
3495
  activeFlowsByRuntimeKey.set(flowKey, flowState);
3302
- const stopRelayingFlowEvents = relayFlowAgentWorkEvents(flowAgent, flowTarget);
3496
+ let resolveSettlement;
3497
+ flowState.settlement = new Promise(resolve => { resolveSettlement = resolve; });
3498
+ void publishConversationState(flowTarget).catch(console.error);
3499
+ const stopRelayingFlowEvents = () => { }; // The shared kernel already relays this exact owner.
3303
3500
  let suspended = false;
3304
3501
  try {
3305
3502
  flowAgent.flow = workflow;
@@ -3347,7 +3544,7 @@ else {
3347
3544
  flowState.reason = 'question';
3348
3545
  flowState.message = '';
3349
3546
  flowState.abortController = null;
3350
- agent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(flowState), flowTarget.conversationId);
3547
+ flowAgent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(flowState), flowTarget.conversationId);
3351
3548
  return {
3352
3549
  ok: true,
3353
3550
  pending: true,
@@ -3383,7 +3580,7 @@ else {
3383
3580
  flowState.message = interruptedMessage;
3384
3581
  flowState.abortController = null;
3385
3582
  flowAgent.pendingOptions = [];
3386
- agent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(flowState), flowTarget.conversationId);
3583
+ flowAgent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(flowState), flowTarget.conversationId);
3387
3584
  return {
3388
3585
  ok: true,
3389
3586
  pending: true,
@@ -3403,9 +3600,19 @@ else {
3403
3600
  flowAgent.flowPc = previousPc;
3404
3601
  flowAgent.setMode(previousMode);
3405
3602
  activeFlowsByRuntimeKey.delete(flowKey);
3603
+ if (!flowState.archiveRequested)
3604
+ await setTargetFlowMode(flowTarget, previousMode, true);
3605
+ }
3606
+ try {
3607
+ ensureConversationKernel(root).settleExternalRun(flowTarget, !suspended);
3608
+ conversationSelections.set(flowKey, { queuePaused: ensureConversationKernel(root).snapshot(flowTarget).queuePaused });
3609
+ }
3610
+ finally {
3611
+ resolveSettlement();
3406
3612
  }
3407
3613
  }
3408
- });
3614
+ };
3615
+ electron_1.ipcMain.handle('flow:run', async (_event, name, input, start, target) => await runFlowForTarget(name, input, start, target));
3409
3616
  const resumeFlowForTarget = async (response, targetInput) => {
3410
3617
  if (!agent)
3411
3618
  return { ok: false, error: 'No suspended Flow is waiting for user input.' };
@@ -3428,18 +3635,33 @@ else {
3428
3635
  }
3429
3636
  const flowTarget = suspension.target;
3430
3637
  const flowKey = activeFlowStateKey(flowTarget);
3431
- const flowAgent = isolatedConversationAgent(flowTarget);
3638
+ if (!mainConversationOwners.has(flowKey))
3639
+ await stopTargetRuntime(flowTarget);
3640
+ const flowAgent = ensureConversationKernel(root).beginExternalRun(flowTarget, {
3641
+ mode: 'flow', model: suspension.flowAgent?.model || agent.model, intelligence: suspension.flowAgent?.intelligence || agent.intelligence,
3642
+ inputMode: suspension.flowAgent?.inputMode || agent.inputMode, engine: agent.engine,
3643
+ }, () => suspension.flowAgent || isolatedConversationAgent(flowTarget), suspension.queueWasPaused ?? true);
3644
+ mainConversationOwners.add(flowKey);
3432
3645
  suspension.flowAgent = flowAgent;
3433
3646
  // A previous Flow may have been interrupted just before its isolated
3434
3647
  // Agent flushed the work ledger. Reconcile only this explicitly paused
3435
3648
  // target before starting the next component; the normal runner guard
3436
3649
  // remains intact for genuine concurrent Builds.
3437
3650
  flowAgent.interruptRunningConversationWorkRuns(flowTarget, 'interrupted');
3651
+ const resumeReason = suspension.reason;
3438
3652
  const flowAbortController = new AbortController();
3653
+ // The pause reason belongs to the previous component wait. Once this
3654
+ // owner resumes, mobile projection and the next Stop must see a running
3655
+ // Flow, while the captured reason retains question-answer semantics.
3656
+ suspension.reason = '';
3657
+ suspension.message = '';
3439
3658
  suspension.abortController = flowAbortController;
3440
3659
  activeFlowsByRuntimeKey.set(flowKey, suspension);
3441
- agent.clearStoredFlowSuspension(flowTarget.conversationId);
3442
- const stopRelayingFlowEvents = relayFlowAgentWorkEvents(flowAgent, flowTarget);
3660
+ flowAgent.clearStoredFlowSuspension(flowTarget.conversationId);
3661
+ let resolveSettlement;
3662
+ suspension.settlement = new Promise(resolve => { resolveSettlement = resolve; });
3663
+ void publishConversationState(flowTarget).catch(console.error);
3664
+ const stopRelayingFlowEvents = () => { };
3443
3665
  let suspendedAgain = false;
3444
3666
  try {
3445
3667
  flowAgent.flow = suspension.workflow;
@@ -3448,7 +3670,7 @@ else {
3448
3670
  await (0, flow_runner_1.runFlow)(flowAgent, suspension.workflow, {
3449
3671
  startInput: suspension.input,
3450
3672
  startPc: suspension.componentId,
3451
- resumePrompt: suspension.reason === 'interrupted' ? '' : String(response || ''),
3673
+ resumePrompt: resumeReason === 'interrupted' ? '' : String(response || ''),
3452
3674
  completedResults: suspension.completedResults,
3453
3675
  quiet: true,
3454
3676
  signal: flowAbortController.signal,
@@ -3484,7 +3706,7 @@ else {
3484
3706
  suspension.reason = 'question';
3485
3707
  suspension.message = '';
3486
3708
  suspension.abortController = null;
3487
- agent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(suspension), flowTarget.conversationId);
3709
+ flowAgent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(suspension), flowTarget.conversationId);
3488
3710
  return {
3489
3711
  ok: true,
3490
3712
  pending: true,
@@ -3517,7 +3739,7 @@ else {
3517
3739
  suspension.message = interruptedMessage;
3518
3740
  suspension.abortController = null;
3519
3741
  flowAgent.pendingOptions = [];
3520
- agent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(suspension), flowTarget.conversationId);
3742
+ flowAgent.saveStoredFlowSuspension(persistedFlowSuspensionRecord(suspension), flowTarget.conversationId);
3521
3743
  return {
3522
3744
  ok: true,
3523
3745
  pending: true,
@@ -3537,27 +3759,19 @@ else {
3537
3759
  flowAgent.flowPc = suspension.previousPc;
3538
3760
  flowAgent.setMode(suspension.previousMode);
3539
3761
  activeFlowsByRuntimeKey.delete(flowKey);
3540
- await setTargetFlowMode(flowTarget, suspension.previousMode);
3762
+ await setTargetFlowMode(flowTarget, suspension.previousMode, true);
3763
+ }
3764
+ try {
3765
+ ensureConversationKernel(root).settleExternalRun(flowTarget, !suspendedAgain);
3766
+ conversationSelections.set(flowKey, { queuePaused: ensureConversationKernel(root).snapshot(flowTarget).queuePaused });
3767
+ }
3768
+ finally {
3769
+ resolveSettlement();
3541
3770
  }
3542
3771
  }
3543
3772
  };
3544
3773
  electron_1.ipcMain.handle('flow:resume', async (_event, response, targetInput) => await resumeFlowForTarget(response, targetInput));
3545
- electron_1.ipcMain.handle('agent:setMode', async (_event, mode) => {
3546
- if (agent) {
3547
- const nextMode = mode;
3548
- agent.setMode(nextMode);
3549
- const target = conversationRuntimeTarget(agent.activeConversationId || 'default');
3550
- // The target-bound runner owns conversation persistence. Updating only
3551
- // the host Agent lets an existing/cold runner restore the stale Build
3552
- // snapshot and silently erase Plan/Goal/Flow on the next send.
3553
- ensureConversationKernel(root)?.setMode(target, nextMode);
3554
- if (wslBackendEnabled())
3555
- await ensureWslConversationPool().setMode(target, nextMode);
3556
- else
3557
- await ensureElectronUtilityPool().setMode(target, nextMode);
3558
- }
3559
- return agent?.mode;
3560
- });
3774
+ electron_1.ipcMain.handle('agent:setMode', async (_event, mode, target) => await selectConversationMode(conversationRuntimeTarget(target), mode));
3561
3775
  electron_1.ipcMain.handle('agent:setModel', async (_event, model) => {
3562
3776
  if (agent) {
3563
3777
  // Compare the resolved selection (qualified deployment or 'auto')
@@ -3613,23 +3827,7 @@ else {
3613
3827
  models: agent.allModelNames(),
3614
3828
  };
3615
3829
  });
3616
- electron_1.ipcMain.handle('agent:setInputMode', async (_event, mode, targetInput) => {
3617
- if (!agent)
3618
- return mode === 'next' ? 'next' : 'guide';
3619
- const target = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget(targetInput));
3620
- const current = (0, conversationTarget_1.normalizeConversationTarget)(conversationRuntimeTarget({
3621
- workspaceId: agent.workspace.current?.id || '',
3622
- conversationId: agent.activeConversationId,
3623
- }));
3624
- const persisted = target.runtimeKey === current.runtimeKey
3625
- ? agent.setInputMode(mode)
3626
- : isolatedConversationAgent(target).setInputMode(mode);
3627
- if (wslBackendEnabled())
3628
- await ensureWslConversationPool().setInputMode(target, persisted);
3629
- else
3630
- await ensureElectronUtilityPool().setInputMode(target, persisted);
3631
- return persisted;
3632
- });
3830
+ electron_1.ipcMain.handle('agent:setInputMode', async (_event, mode, target) => await selectConversationInputMode(conversationRuntimeTarget(target), mode));
3633
3831
  electron_1.ipcMain.handle('agent:setConversation', async (_event, id) => {
3634
3832
  return agent?.setConversationFromStorage(id);
3635
3833
  });
@@ -3637,7 +3835,12 @@ else {
3637
3835
  if (!agent)
3638
3836
  return {};
3639
3837
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3640
- return await runtimeSnapshotForTarget(target);
3838
+ const workspace = (target.workspace || agent.workspace.current);
3839
+ const existed = agent.hasConversationInWorkspace(target.conversationId, workspace);
3840
+ const snapshot = await runtimeSnapshotForTarget(target);
3841
+ if (!existed)
3842
+ publishConversationList(workspace);
3843
+ return snapshot;
3641
3844
  });
3642
3845
  electron_1.ipcMain.handle('agent:activateConversation', async (_event, targetInput) => {
3643
3846
  if (!agent)
@@ -3653,24 +3856,31 @@ else {
3653
3856
  icon: '',
3654
3857
  kind: target.workspace.kind === 'ssh' ? 'ssh' : 'local',
3655
3858
  } : agent.workspace.current;
3859
+ const existed = agent.hasConversationInWorkspace(target.conversationId, workspace);
3656
3860
  agent.persistActiveConversationSelection(target.conversationId, workspace);
3657
3861
  const resident = peekTargetRuntime(target).resident;
3658
3862
  if (!resident) {
3659
- return {
3863
+ const snapshot = {
3660
3864
  ...localConversationSnapshotForStartup(target),
3661
3865
  runtimeDeferred: true,
3662
3866
  };
3867
+ if (!existed)
3868
+ publishConversationList(workspace);
3869
+ return snapshot;
3663
3870
  }
3664
- return {
3871
+ const snapshot = {
3665
3872
  ...await runtimeSnapshotForTarget(target),
3666
3873
  runtimeDeferred: false,
3667
3874
  };
3875
+ if (!existed)
3876
+ publishConversationList(workspace);
3877
+ return snapshot;
3668
3878
  });
3669
3879
  electron_1.ipcMain.handle('agent:setConversationBranchCommunication', async (_event, targetInput, enabled) => {
3670
3880
  if (!agent)
3671
3881
  return false;
3672
3882
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3673
- return mutateTargetConversation(target, () => isolatedConversationAgent(target).setBranchCommunication(enabled !== false));
3883
+ return mutateTargetConversation(target, () => conversationAgentForTarget(target).setBranchCommunication(enabled !== false));
3674
3884
  });
3675
3885
  electron_1.ipcMain.handle('agent:computerUseState', async (_event, targetInput) => {
3676
3886
  if (!agent)
@@ -3698,38 +3908,19 @@ else {
3698
3908
  if (!agent)
3699
3909
  return null;
3700
3910
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3701
- return mutateTargetConversation(target, () => {
3702
- const isolated = isolatedConversationAgent(target);
3703
- isolated.updateGoal(goal);
3704
- isolated.setMode('goal');
3705
- isolated.saveWorkspaceConversationState(true);
3706
- return isolated.getConversationSnapshot(target.conversationId).goal;
3707
- });
3911
+ return (await applyConversationAction(target, 'goal_update', goal)).goal;
3708
3912
  });
3709
3913
  electron_1.ipcMain.handle('agent:toggleGoalPause', async (_event, targetInput) => {
3710
3914
  if (!agent)
3711
3915
  return false;
3712
3916
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3713
- const residentResult = wslBackendEnabled()
3714
- ? await ensureWslConversationPool().toggleGoalPause(target)
3715
- : await ensureElectronUtilityPool().toggleGoalPause(target);
3716
- if (residentResult !== null)
3717
- return residentResult;
3718
- return mutateTargetConversation(target, () => isolatedConversationAgent(target).toggleGoalPause());
3917
+ return (await applyConversationAction(target, 'goal_toggle_pause')).paused;
3719
3918
  });
3720
3919
  electron_1.ipcMain.handle('agent:clearGoal', async (_event, targetInput) => {
3721
3920
  if (!agent)
3722
3921
  return false;
3723
3922
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3724
- const residentResult = wslBackendEnabled()
3725
- ? await ensureWslConversationPool().clearGoal(target)
3726
- : await ensureElectronUtilityPool().clearGoal(target);
3727
- if (residentResult !== null)
3728
- return residentResult;
3729
- return mutateTargetConversation(target, () => {
3730
- isolatedConversationAgent(target).clearGoal();
3731
- return true;
3732
- });
3923
+ return (await applyConversationAction(target, 'goal_clear')).cleared;
3733
3924
  });
3734
3925
  electron_1.ipcMain.handle('agent:getState', async (event, targetInput) => {
3735
3926
  const startupPrewarmRequest = isStartupPrewarmSender(event);
@@ -3747,14 +3938,12 @@ else {
3747
3938
  ? targetInput
3748
3939
  : {});
3749
3940
  const requestedWindow = Math.max(1, Math.floor(Number(windowInput.window) || 0) || 200);
3750
- const requestedBefore = Math.max(0, Math.floor(Number(windowInput.before) || 0));
3941
+ const requestedBefore = windowInput.before == null ? undefined : Math.max(0, Math.floor(Number(windowInput.before) || 0));
3751
3942
  let conversationSnapshot;
3752
3943
  try {
3753
3944
  conversationSnapshot = startupPrewarmRequest
3754
- ? localConversationSnapshotForStartup(target)
3755
- : wslBackendEnabled()
3756
- ? await ensureWslConversationPool().snapshot(target)
3757
- : await ensureElectronUtilityPool().snapshot(target);
3945
+ ? localConversationSnapshotForStartup(target, { window: requestedWindow, before: requestedBefore })
3946
+ : await runtimeSnapshotForTarget(target, { window: requestedWindow, before: requestedBefore });
3758
3947
  }
3759
3948
  catch (error) {
3760
3949
  conversationSnapshot = {
@@ -3768,11 +3957,8 @@ else {
3768
3957
  workRuns: [],
3769
3958
  };
3770
3959
  }
3771
- const fullMessages = Array.isArray(conversationSnapshot.chatMessages) ? conversationSnapshot.chatMessages : [];
3772
- const totalMessages = fullMessages.length;
3773
- const before = requestedBefore > 0 ? Math.min(totalMessages, requestedBefore) : totalMessages;
3774
- const windowStart = Math.max(0, before - requestedWindow);
3775
- const windowedMessages = fullMessages.slice(windowStart, before);
3960
+ // The owner already applied this window to its complete live history.
3961
+ // Re-slicing here would lose the absolute cursor and repeat recent rows.
3776
3962
  return {
3777
3963
  mode: agent.mode,
3778
3964
  model: agent.modelSelectionValue(),
@@ -3781,9 +3967,9 @@ else {
3781
3967
  routeDecision: agent.lastRouteDecision,
3782
3968
  intelligence: agent.intelligence,
3783
3969
  ...conversationSnapshot,
3784
- chatMessages: windowedMessages,
3785
- totalMessages,
3786
- windowStart,
3970
+ chatMessages: conversationSnapshot.chatMessages || [],
3971
+ totalMessages: conversationSnapshot.totalMessages || 0,
3972
+ windowStart: conversationSnapshot.windowStart || 0,
3787
3973
  conversationLocked: conversationSnapshot.conversationLocked ?? false,
3788
3974
  status: conversationSnapshot.status ?? 'idle',
3789
3975
  goal: conversationSnapshot.goal ?? null,
@@ -3822,7 +4008,7 @@ else {
3822
4008
  fallbackOnUnavailable: agent.config.getBool('models', 'fallback_on_unavailable'),
3823
4009
  openAIApiMode: agent.config.openAIApiMode(),
3824
4010
  autoAdjust: agent.config.getBool('agent', 'auto_adjust_settings'),
3825
- inputMode: agent.inputMode,
4011
+ inputMode: conversationSnapshot.inputMode || agent.inputMode,
3826
4012
  terminalInterruptTimeoutMs: agent.config.getNum('terminal', 'interrupt_timeout_ms'),
3827
4013
  platform: process.platform,
3828
4014
  defaultTerminalShell: resolveTerminalShell(agent.config.getStr('terminal', 'default_shell') || defaultTerminalShell()).id,
@@ -3880,17 +4066,11 @@ else {
3880
4066
  const windowInput = targetInput && typeof targetInput === 'object' ? targetInput : {};
3881
4067
  const requestedWindow = Math.max(1, Math.floor(Number(windowInput.window) || 0) || 200);
3882
4068
  const requestedBefore = Math.max(0, Math.floor(Number(windowInput.before) || 0));
3883
- const snapshot = wslBackendEnabled()
3884
- ? await ensureWslConversationPool().snapshot(target)
3885
- : await ensureElectronUtilityPool().snapshot(target);
3886
- const fullMessages = Array.isArray(snapshot.chatMessages) ? snapshot.chatMessages : [];
3887
- const totalMessages = fullMessages.length;
3888
- const before = Math.min(totalMessages, requestedBefore);
3889
- const windowStart = Math.max(0, before - requestedWindow);
4069
+ const snapshot = await runtimeSnapshotForTarget(target, { window: requestedWindow, before: requestedBefore });
3890
4070
  return {
3891
- chatMessages: fullMessages.slice(windowStart, before),
3892
- totalMessages,
3893
- windowStart,
4071
+ chatMessages: snapshot.chatMessages || [],
4072
+ totalMessages: snapshot.totalMessages || 0,
4073
+ windowStart: snapshot.windowStart || 0,
3894
4074
  conversationId: target.conversationId,
3895
4075
  workspaceId: target.workspaceId,
3896
4076
  };
@@ -3909,17 +4089,26 @@ else {
3909
4089
  electron_1.ipcMain.handle('agent:setConversationPinned', async (_event, id, pinned) => {
3910
4090
  if (!agent)
3911
4091
  return false;
3912
- return agent.setConversationPinned(id, pinned);
4092
+ const ok = agent.setConversationPinned(id, pinned);
4093
+ if (ok)
4094
+ publishConversationList();
4095
+ return ok;
3913
4096
  });
3914
4097
  electron_1.ipcMain.handle('agent:renameConversation', async (_event, id, title) => {
3915
4098
  if (!agent)
3916
4099
  return false;
3917
- return agent.renameConversation(id, title);
4100
+ const ok = agent.renameConversation(id, title);
4101
+ if (ok)
4102
+ publishConversationList();
4103
+ return ok;
3918
4104
  });
3919
4105
  electron_1.ipcMain.handle('agent:reorderConversations', async (_event, ids) => {
3920
4106
  if (!agent)
3921
4107
  return false;
3922
- return agent.reorderConversations(ids);
4108
+ const ok = agent.reorderConversations(ids);
4109
+ if (ok)
4110
+ publishConversationList();
4111
+ return ok;
3923
4112
  });
3924
4113
  electron_1.ipcMain.handle('automation:list', async () => {
3925
4114
  return automation?.list() || [];
@@ -4087,8 +4276,8 @@ else {
4087
4276
  electron_1.ipcMain.handle('agent:reloadGlobalConfig', async () => {
4088
4277
  if (!agent)
4089
4278
  return { error: 'Agent is not initialized' };
4090
- if (conversationKernel?.isAnyRunning())
4091
- return { error: 'Wait for the active Agent turn to finish before refreshing config.json.' };
4279
+ if (conversationKernel?.hasRetainedWork())
4280
+ return { error: 'Finish or stop retained Flow and queued work before refreshing config.json.' };
4092
4281
  try {
4093
4282
  agent.config.reload();
4094
4283
  if (agent.workspace.current)
@@ -4103,7 +4292,9 @@ else {
4103
4292
  throw new AggregateError(failures.map(result => result.reason), 'Runtime cleanup failed');
4104
4293
  }), CONFIG_RELOAD_RUNTIME_TIMEOUT_MS, 'Config refresh runtime cleanup');
4105
4294
  }
4295
+ conversationKernel?.disposeIdle();
4106
4296
  conversationKernel = null;
4297
+ mainConversationOwners.clear();
4107
4298
  return { ok: true, path: path.join(agent.rootPath, 'config.json') };
4108
4299
  }
4109
4300
  catch (error) {
@@ -4130,6 +4321,15 @@ else {
4130
4321
  ? conversationRuntimeTarget(targetInput)
4131
4322
  : conversationRuntimeTarget(agent.activeConversationId || 'default');
4132
4323
  const state = activeFlowStateFor(target);
4324
+ const pending = pendingFlowStarts.get(activeFlowStateKey(target));
4325
+ if (pending) {
4326
+ pending.cancelled = true;
4327
+ // Stop can arrive while startup awaits disposal of the previous owner.
4328
+ // Reading a new snapshot before disposal ends re-enters a stopping pool.
4329
+ await pending.settlement;
4330
+ await publishConversationState(target);
4331
+ return { ok: true, action: 'stopping' };
4332
+ }
4133
4333
  if (!state)
4134
4334
  return { ok: true, action: 'not_running' };
4135
4335
  if (state.reason === 'question' || state.reason === 'interrupted') {
@@ -4196,8 +4396,6 @@ else {
4196
4396
  if (!agent)
4197
4397
  throw new Error('Agent not initialized');
4198
4398
  const target = conversationRuntimeTarget({ target: raw?.target });
4199
- if (activeFlowStateFor(target))
4200
- clearFlowSuspensionForNewWork(target);
4201
4399
  const envelope = {
4202
4400
  clientMessageId: String(raw?.clientMessageId || '').trim().slice(0, 200),
4203
4401
  target: { workspaceId: target.workspaceId, conversationId: target.conversationId },
@@ -4208,14 +4406,14 @@ else {
4208
4406
  images: Array.isArray(raw?.images) ? raw.images : [],
4209
4407
  createdAt: String(raw?.createdAt || new Date().toISOString()),
4210
4408
  };
4211
- return wslBackendEnabled()
4212
- ? await ensureWslConversationPool().enqueueGuide(envelope)
4213
- : await ensureElectronUtilityPool().enqueueGuide(envelope);
4409
+ return await enqueueConversationGuide(envelope);
4214
4410
  });
4215
4411
  electron_1.ipcMain.handle('agent:checkpointConversation', async (_event, request) => {
4216
4412
  if (!agent)
4217
4413
  throw new Error('Agent not initialized');
4218
4414
  const target = conversationRuntimeTarget(request);
4415
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4416
+ return ensureConversationKernel(root).checkpoint(target);
4219
4417
  return wslBackendEnabled()
4220
4418
  ? await ensureWslConversationPool().checkpoint(target)
4221
4419
  : await ensureElectronUtilityPool().checkpoint(target);
@@ -4228,6 +4426,8 @@ else {
4228
4426
  keepRecent: Number.isFinite(Number(request?.keepRecent)) ? Math.floor(Number(request.keepRecent)) : undefined,
4229
4427
  force: request?.force !== false,
4230
4428
  };
4429
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4430
+ return await ensureConversationKernel(root).compressContext(target, options);
4231
4431
  return wslBackendEnabled()
4232
4432
  ? await ensureWslConversationPool().contextCompress(target, options)
4233
4433
  : await ensureElectronUtilityPool().contextCompress(target, options);
@@ -4238,6 +4438,8 @@ else {
4238
4438
  const target = conversationRuntimeTarget(request);
4239
4439
  const score = Number(request?.score);
4240
4440
  const routeId = String(request?.routeId || '');
4441
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4442
+ return ensureConversationKernel(root).rateAutoRoute(target, score, routeId);
4241
4443
  return wslBackendEnabled()
4242
4444
  ? await ensureWslConversationPool().rateAutoRoute(target, score, routeId)
4243
4445
  : await ensureElectronUtilityPool().rateAutoRoute(target, score, routeId);
@@ -4247,6 +4449,10 @@ else {
4247
4449
  throw new Error('Agent not initialized');
4248
4450
  const target = conversationRuntimeTarget(request);
4249
4451
  const runId = String(request?.runId || '').trim() || undefined;
4452
+ if (activeFlowStateFor(target) || pendingFlowStarts.has(activeFlowStateKey(target)))
4453
+ return await stopFlowForTarget(target);
4454
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4455
+ return ensureConversationKernel(root).requestStop(target, runId);
4250
4456
  // The runtime, not a renderer flag, decides whether this is the first
4251
4457
  // checkpointing stop or the second target-local hard restart.
4252
4458
  return wslBackendEnabled()
@@ -4260,6 +4466,8 @@ else {
4260
4466
  const runId = String(request?.runId || '').trim();
4261
4467
  if (!runId)
4262
4468
  return false;
4469
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4470
+ return ensureConversationKernel(root).setWorkRunExpanded(target, runId, request?.expanded !== false);
4263
4471
  return wslBackendEnabled()
4264
4472
  ? await ensureWslConversationPool().setWorkRunExpanded(target, runId, request?.expanded !== false)
4265
4473
  : await ensureElectronUtilityPool().setWorkRunExpanded(target, runId, request?.expanded !== false);
@@ -4268,9 +4476,7 @@ else {
4268
4476
  if (!agent)
4269
4477
  return false;
4270
4478
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
4271
- const result = wslBackendEnabled()
4272
- ? await ensureWslConversationPool().requestStop(target)
4273
- : await ensureElectronUtilityPool().requestStop(target);
4479
+ const result = await applyConversationAction(target, 'conversation_stop');
4274
4480
  return result.action !== 'not_running' && result.action !== 'stale';
4275
4481
  });
4276
4482
  electron_1.ipcMain.handle('agent:rewindConversation', async (_event, targetInput, messageIndex) => {
@@ -4278,36 +4484,40 @@ else {
4278
4484
  return { error: 'Agent not initialized' };
4279
4485
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
4280
4486
  try {
4281
- const snapshot = await mutateTargetConversation(target, () => wslBackendEnabled()
4282
- ? ensureWslConversationPool().rewind(target, messageIndex)
4283
- : ensureElectronUtilityPool().rewind(target, messageIndex));
4487
+ const snapshot = await mutateTargetConversation(target, () => mainConversationOwners.has(activeFlowStateKey(target))
4488
+ ? ensureConversationKernel(root).rewind(target, messageIndex)
4489
+ : wslBackendEnabled() ? ensureWslConversationPool().rewind(target, messageIndex) : ensureElectronUtilityPool().rewind(target, messageIndex));
4284
4490
  return { ...snapshot, queued: { steering: [], followUp: [] }, workEvents: [] };
4285
4491
  }
4286
4492
  catch (error) {
4287
4493
  return { error: error instanceof Error ? error.message : String(error) };
4288
4494
  }
4289
4495
  });
4290
- electron_1.ipcMain.handle('agent:branchConversation', async (_event, targetInput, messageIndex, text, locator) => {
4496
+ const branchConversationForTarget = async (targetInput, messageIndex, text, locator) => {
4291
4497
  if (!agent)
4292
4498
  return { error: 'Agent not initialized' };
4293
4499
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
4294
4500
  try {
4295
4501
  const snapshot = await mutateTargetConversation(target, () => {
4296
- const isolated = isolatedConversationAgent(target);
4297
- return isolated.branchConversation(target.conversationId, messageIndex, text, locator);
4502
+ const isolated = conversationAgentForTarget(target);
4503
+ const branched = isolated.branchConversation(target.conversationId, messageIndex, text, locator);
4504
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4505
+ conversationKernel?.refreshIdleConversation(target);
4506
+ return branched;
4298
4507
  });
4299
4508
  return { ...snapshot, queued: { steering: [], followUp: [] }, workEvents: [] };
4300
4509
  }
4301
4510
  catch (error) {
4302
4511
  return { error: error instanceof Error ? error.message : String(error) };
4303
4512
  }
4304
- });
4305
- electron_1.ipcMain.handle('agent:inspectConversationBranch', async (_event, targetInput, branchId, branchGroupId) => {
4513
+ };
4514
+ electron_1.ipcMain.handle('agent:branchConversation', async (_event, targetInput, messageIndex, text, locator) => await branchConversationForTarget(targetInput, messageIndex, text, locator));
4515
+ const inspectConversationBranchForTarget = async (targetInput, branchId, branchGroupId) => {
4306
4516
  if (!agent)
4307
4517
  return { error: 'Agent not initialized' };
4308
4518
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
4309
4519
  try {
4310
- const isolated = isolatedConversationAgent(target);
4520
+ const isolated = conversationAgentForTarget(target);
4311
4521
  const snapshot = isolated.inspectConversationBranch(target.conversationId, branchId, branchGroupId);
4312
4522
  if (snapshot.runtimeBranchId === snapshot.activeBranchId && peekTargetRuntime(target).resident) {
4313
4523
  const live = await runtimeSnapshotForTarget(target);
@@ -4327,8 +4537,9 @@ else {
4327
4537
  catch (error) {
4328
4538
  return { error: error instanceof Error ? error.message : String(error) };
4329
4539
  }
4330
- });
4331
- electron_1.ipcMain.handle('agent:activateConversationBranch', async (_event, targetInput, branchId, branchGroupId) => {
4540
+ };
4541
+ electron_1.ipcMain.handle('agent:inspectConversationBranch', async (_event, targetInput, branchId, branchGroupId) => await inspectConversationBranchForTarget(targetInput, branchId, branchGroupId));
4542
+ const activateConversationBranchForTarget = async (targetInput, branchId, branchGroupId) => {
4332
4543
  if (!agent)
4333
4544
  return { error: 'Agent not initialized' };
4334
4545
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
@@ -4336,15 +4547,19 @@ else {
4336
4547
  if (peekTargetRuntime(target).running)
4337
4548
  await stopTargetRuntime(target);
4338
4549
  const snapshot = await mutateTargetConversation(target, () => {
4339
- const isolated = isolatedConversationAgent(target);
4340
- return isolated.switchConversationBranch(target.conversationId, branchId, branchGroupId);
4550
+ const isolated = conversationAgentForTarget(target);
4551
+ const switched = isolated.switchConversationBranch(target.conversationId, branchId, branchGroupId);
4552
+ if (mainConversationOwners.has(activeFlowStateKey(target)))
4553
+ conversationKernel?.refreshIdleConversation(target);
4554
+ return switched;
4341
4555
  });
4342
4556
  return { ...snapshot, queued: { steering: [], followUp: [] }, workEvents: [] };
4343
4557
  }
4344
4558
  catch (error) {
4345
4559
  return { error: error instanceof Error ? error.message : String(error) };
4346
4560
  }
4347
- });
4561
+ };
4562
+ electron_1.ipcMain.handle('agent:activateConversationBranch', async (_event, targetInput, branchId, branchGroupId) => await activateConversationBranchForTarget(targetInput, branchId, branchGroupId));
4348
4563
  electron_1.ipcMain.handle('flow:list', async () => {
4349
4564
  if (!agent)
4350
4565
  return [];
@@ -4376,7 +4591,7 @@ else {
4376
4591
  flow_1.FlowEngine.save(flowDir, workflow);
4377
4592
  return { ok: true, workflow };
4378
4593
  });
4379
- electron_1.ipcMain.handle('agent:archive', async (_event, targetInput) => {
4594
+ const archiveConversationForTarget = async (targetInput) => {
4380
4595
  if (!agent)
4381
4596
  return { ok: false, error: 'Agent not initialized' };
4382
4597
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
@@ -4386,13 +4601,20 @@ else {
4386
4601
  return existing;
4387
4602
  const operation = (async () => {
4388
4603
  mutatingRuntimeKeys.add(normalized.runtimeKey);
4604
+ let completed = false;
4389
4605
  try {
4390
4606
  // Archive is a destructive lifecycle command. It intentionally
4391
4607
  // bypasses the normal mutation/active-prompt guard. Cancel the
4392
4608
  // conversation-local Flow synchronously, then start the resident
4393
4609
  // runtime hard-stop in the background so a stuck child cannot hold
4394
4610
  // the archive click hostage.
4611
+ const ownedFlow = activeFlowStateFor(normalized);
4612
+ const mainOwnerPreparation = mainConversationOwners.has(normalized.runtimeKey)
4613
+ ? ensureConversationKernel(root).prepareForArchive(normalized) : Promise.resolve(null);
4395
4614
  interruptActiveFlowForArchive(normalized);
4615
+ const mainArchiveOwner = await mainOwnerPreparation;
4616
+ if (ownedFlow?.settlement)
4617
+ await ownedFlow.settlement;
4396
4618
  void forceStopTargetRuntime(normalized).catch(error => {
4397
4619
  console.error('[Newmark] archive runtime force-stop failed:', error instanceof Error ? error.message : String(error));
4398
4620
  });
@@ -4406,17 +4628,23 @@ else {
4406
4628
  // starts payload I/O in parallel and finalizes deletion against the
4407
4629
  // latest locked state snapshot, so rapid clicks do not serialize on
4408
4630
  // large Markdown bodies or lose a sibling deletion.
4409
- const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
4631
+ const archiveOwner = mainArchiveOwner || (ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized));
4410
4632
  archiveOwner.clearStoredFlowSuspension(normalized.conversationId);
4411
4633
  const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
4412
4634
  if (!archived)
4413
4635
  return { ok: false, error: 'Conversation archive could not be written.' };
4636
+ completed = true;
4637
+ conversationSelections.delete(normalized.runtimeKey);
4638
+ publishConversationList((normalized.workspace || agent.workspace.current));
4414
4639
  return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
4415
4640
  }
4416
4641
  catch (error) {
4417
4642
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
4418
4643
  }
4419
4644
  finally {
4645
+ conversationKernel?.finishArchive(normalized, completed);
4646
+ if (completed)
4647
+ mainConversationOwners.delete(normalized.runtimeKey);
4420
4648
  mutatingRuntimeKeys.delete(normalized.runtimeKey);
4421
4649
  }
4422
4650
  })();
@@ -4428,7 +4656,8 @@ else {
4428
4656
  if (archiveInFlight.get(normalized.runtimeKey) === operation)
4429
4657
  archiveInFlight.delete(normalized.runtimeKey);
4430
4658
  }
4431
- });
4659
+ };
4660
+ electron_1.ipcMain.handle('agent:archive', async (_event, targetInput) => await archiveConversationForTarget(targetInput));
4432
4661
  electron_1.ipcMain.handle('agent:listArchives', async (_event, scope) => {
4433
4662
  if (!agent)
4434
4663
  return [];
@@ -4452,6 +4681,7 @@ else {
4452
4681
  return restored;
4453
4682
  agent.selectWorkspaceFromStorage(restored.workspaceId);
4454
4683
  agent.setConversationFromStorage(restored.conversationId);
4684
+ publishConversationList();
4455
4685
  return { ...restored, snapshot: agent.getConversationSnapshot(restored.conversationId) };
4456
4686
  });
4457
4687
  async function listTreeLevel(current) {
@@ -4600,16 +4830,9 @@ else {
4600
4830
  if (mainWindow && !mainWindow.isDestroyed())
4601
4831
  mainWindow.webContents.send('pty:data', sessionId, text);
4602
4832
  });
4603
- electron_1.ipcMain.handle('agent:queueAction', async (_event, actionInput, input = {}, targetInput) => {
4604
- const target = conversationRuntimeTarget(targetInput);
4605
- const action = String(actionInput || '').replace(/^queue_/, '');
4606
- if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'guide'].includes(action)) {
4607
- return { ok: false, error: 'Unknown queue action' };
4608
- }
4833
+ electron_1.ipcMain.handle('agent:queueAction', async (_event, action, input = {}, target) => {
4609
4834
  try {
4610
- return wslBackendEnabled()
4611
- ? await ensureWslConversationPool().queueAction(target, action, input)
4612
- : await ensureElectronUtilityPool().queueAction(target, action, input);
4835
+ return await applyConversationAction(conversationRuntimeTarget(target), 'queue_' + action.replace(/^queue_/, ''), '', input);
4613
4836
  }
4614
4837
  catch (error) {
4615
4838
  return { ok: false, error: error instanceof Error ? error.message : String(error) };
@@ -4911,6 +5134,9 @@ else {
4911
5134
  ]);
4912
5135
  if (nativeActive || wslActive)
4913
5136
  throw new Error('Cannot delete a workspace while one of its conversations is running, stopping, or restarting.');
5137
+ const releasedOwners = conversationKernel?.disposeIdle(normalized.workspaceKey) || [];
5138
+ for (const key of releasedOwners)
5139
+ mainConversationOwners.delete(key);
4914
5140
  await Promise.all([
4915
5141
  electronUtilityRuntimePool?.stopWorkspace(normalized),
4916
5142
  wslAgentRuntimePool?.stopWorkspace(normalized),