newmark-agent 0.6.3 → 0.6.7

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 (72) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +53 -2
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +3046 -1038
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +149 -12
  8. package/dist/core/agent.js +843 -176
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +171 -22
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/branchIdentity.d.ts +29 -0
  14. package/dist/core/branchIdentity.js +54 -0
  15. package/dist/core/config.js +27 -11
  16. package/dist/core/continuation/contracts.d.ts +1 -1
  17. package/dist/core/continuation/store.d.ts +119 -1
  18. package/dist/core/continuation/store.js +337 -19
  19. package/dist/core/conversationKernel.d.ts +62 -8
  20. package/dist/core/conversationKernel.js +361 -47
  21. package/dist/core/conversationStateDocument.d.ts +18 -0
  22. package/dist/core/conversationStateDocument.js +88 -0
  23. package/dist/core/conversationVisualBudget.d.ts +74 -0
  24. package/dist/core/conversationVisualBudget.js +153 -0
  25. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  26. package/dist/core/electronUtilityAgentClient.js +32 -8
  27. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  28. package/dist/core/electronUtilityRuntimePool.js +93 -24
  29. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  30. package/dist/core/hostRuntimeHooks.js +17 -0
  31. package/dist/core/jevDecision.d.ts +144 -0
  32. package/dist/core/jevDecision.js +226 -0
  33. package/dist/core/memoryProbe.d.ts +24 -0
  34. package/dist/core/memoryProbe.js +104 -0
  35. package/dist/core/mobilePairing.d.ts +7 -1
  36. package/dist/core/mobilePairing.js +9 -1
  37. package/dist/core/performanceDiagnostics.d.ts +1 -1
  38. package/dist/core/routeDecisionValidator.d.ts +86 -0
  39. package/dist/core/routeDecisionValidator.js +249 -0
  40. package/dist/core/routeEligibility.d.ts +52 -0
  41. package/dist/core/routeEligibility.js +108 -0
  42. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  43. package/dist/core/runtimeMemoryBudget.js +12 -0
  44. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  45. package/dist/core/utilityHostToolRouter.js +2 -0
  46. package/dist/core/visualDownscale.d.ts +6 -0
  47. package/dist/core/visualDownscale.js +137 -0
  48. package/dist/core/wslAgentClient.d.ts +1 -2
  49. package/dist/core/wslAgentClient.js +0 -8
  50. package/dist/core/wslAgentProtocol.d.ts +1 -10
  51. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  52. package/dist/core/wslAgentRuntimePool.js +17 -22
  53. package/dist/llm/provider.d.ts +3 -1
  54. package/dist/llm/provider.js +53 -9
  55. package/dist/main.js +108 -31
  56. package/dist/preload.js +3 -1
  57. package/dist/server.js +30 -18
  58. package/dist/tools/computerUse.d.ts +4 -0
  59. package/dist/tools/computerUse.js +202 -4
  60. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  61. package/dist/tools/computerUsePowerShellHost.js +105 -31
  62. package/dist/tools/index.js +4 -1
  63. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  64. package/dist/tui/src/i18n.js +12 -1
  65. package/dist/tui/src/render.js +9 -2
  66. package/dist/tui/src/settings-schema.js +19 -0
  67. package/dist/tui/src/state.js +69 -1
  68. package/dist/ui/index.html +994 -277
  69. package/dist/ui/lucide-sprite.svg +0 -8
  70. package/dist/wsl-agent-host.bundle.cjs +2851 -1013
  71. package/dist/wsl-agent-host.js +0 -3
  72. package/package.json +38 -9
@@ -4,7 +4,7 @@ import { ConversationRuntimeTarget } from './conversationTarget';
4
4
  import { TerminalTakeoverEvent, TerminalTakeoverOwnerFilter, TerminalTakeoverState } from '../tools/terminalTakeover';
5
5
  import { BrowserUseRequest } from './browserUse';
6
6
  import { BrowserControlRequest } from './browserControl';
7
- import type { AutoRouteRatingResult, ConversationSnapshot } from './agent';
7
+ import type { ConversationSnapshot } from './agent';
8
8
  export interface WslAgentWorkspace {
9
9
  id?: string;
10
10
  name: string;
@@ -136,14 +136,6 @@ export type WslAgentRequest = {
136
136
  target: ConversationRuntimeTarget;
137
137
  options?: ConversationContextCompressOptions;
138
138
  };
139
- } | {
140
- id: string;
141
- method: 'rate_auto_route';
142
- params: {
143
- target: ConversationRuntimeTarget;
144
- score: number;
145
- routeId?: string;
146
- };
147
139
  } | {
148
140
  id: string;
149
141
  method: 'set_work_run_expanded';
@@ -272,7 +264,6 @@ export type WslAgentStopResult = ConversationStopResult & {
272
264
  distro: string;
273
265
  };
274
266
  export type WslGuideResult = GuideReceipt;
275
- export type WslAutoRouteRatingResult = AutoRouteRatingResult;
276
267
  export type WslConversationRewindResult = ConversationSnapshot;
277
268
  export {};
278
269
  //# sourceMappingURL=wslAgentProtocol.d.ts.map
@@ -2,7 +2,7 @@ import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } fr
2
2
  import { ConversationQueueAction, ConversationQueueActionInput } from './conversationKernel';
3
3
  import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
4
4
  import { WslHostToolHandler } from './wslAgentClient';
5
- import { WslAgentPromptRequest, WslAgentPromptResult, WslAutoRouteRatingResult, WslAgentStopResult, WslConversationRewindResult } from './wslAgentProtocol';
5
+ import { WslAgentPromptRequest, WslAgentPromptResult, WslAgentStopResult, WslConversationRewindResult } from './wslAgentProtocol';
6
6
  export interface WslTargetRuntimeClient {
7
7
  subscribe(listener: (event: AgentWorkEvent) => void): () => void;
8
8
  setHostToolHandler(handler: WslHostToolHandler | null): void;
@@ -20,7 +20,6 @@ export interface WslTargetRuntimeClient {
20
20
  keepRecent?: number;
21
21
  force?: boolean;
22
22
  }): Promise<Record<string, unknown>>;
23
- rateAutoRoute?(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
24
23
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
25
24
  setMode?(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
26
25
  setModel?(target: ConversationRuntimeTarget, model: string): Promise<string>;
@@ -87,7 +86,6 @@ export declare class WslAgentRuntimePool {
87
86
  keepRecent?: number;
88
87
  force?: boolean;
89
88
  }): Promise<Record<string, unknown>>;
90
- rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
91
89
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
92
90
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
93
91
  setMode(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode | null>;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WslAgentRuntimePool = void 0;
4
+ const runtimeMemoryBudget_1 = require("./runtimeMemoryBudget");
4
5
  const conversationTarget_1 = require("./conversationTarget");
5
6
  const wslAgentClient_1 = require("./wslAgentClient");
6
7
  const runtimePoolCapacity_1 = require("./runtimePoolCapacity");
@@ -68,6 +69,15 @@ class WslAgentRuntimePool {
68
69
  }
69
70
  async snapshot(target, options = {}) {
70
71
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
72
+ const wantsDefaultWindow = options.before == null && (options.window == null || options.window === 200);
73
+ // Same contract as the Electron pool: a timed status poll must not 500
74
+ // while the target's runtime is being force-stopped by an archive.
75
+ if (wantsDefaultWindow) {
76
+ const pending = this.entries.get(normalized.runtimeKey);
77
+ if (pending && (this.disposing.has(normalized.runtimeKey) || this.restarting.has(normalized.runtimeKey))) {
78
+ return this.supervisorSnapshot(pending);
79
+ }
80
+ }
71
81
  const entry = await this.acquire(normalized);
72
82
  let scheduleIdle = false;
73
83
  try {
@@ -246,19 +256,6 @@ class WslAgentRuntimePool {
246
256
  this.release(entry, true);
247
257
  }
248
258
  }
249
- async rateAutoRoute(target, score, routeId = '') {
250
- const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
251
- const entry = await this.acquireExisting(normalized);
252
- if (!entry || !entry.client.rateAutoRoute) {
253
- return { ok: false, reason: 'no_active_auto_route' };
254
- }
255
- try {
256
- return await entry.client.rateAutoRoute(normalized, score, routeId);
257
- }
258
- finally {
259
- this.release(entry, true);
260
- }
261
- }
262
259
  async setWorkRunExpanded(target, runId, expanded) {
263
260
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
264
261
  const entry = await this.acquire(normalized);
@@ -575,12 +572,9 @@ class WslAgentRuntimePool {
575
572
  return this.accessSequence;
576
573
  }
577
574
  maxResidentRuntimes() {
578
- // Same rationale as the Electron utility pool: one runtime per active
579
- // conversation target. The previous default of 2 blocked parallel
580
- // conversations with a capacity error; idle LRU eviction still bounds
581
- // resident memory.
582
- const configured = Number(this.options.maxResidentRuntimes ?? 8);
583
- return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 8;
575
+ const fallback = (0, runtimeMemoryBudget_1.runtimeMemoryBudget)().maxResidentRuntimes;
576
+ const configured = Number(this.options.maxResidentRuntimes ?? fallback);
577
+ return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : fallback;
584
578
  }
585
579
  async serializeCapacity(operation) {
586
580
  const previous = this.capacityTail;
@@ -628,7 +622,7 @@ class WslAgentRuntimePool {
628
622
  }
629
623
  scheduleIdle(entry) {
630
624
  this.touch(entry);
631
- const ttl = Math.max(10, Number(this.options.idleTtlMs ?? 5 * 60 * 1000));
625
+ const ttl = Math.max(10, Number(this.options.idleTtlMs ?? (0, runtimeMemoryBudget_1.runtimeMemoryBudget)().idleTtlMs));
632
626
  const timer = setTimeout(() => {
633
627
  void this.evictIfIdle(entry.target.runtimeKey, entry.lastUsedAt).catch(() => {
634
628
  // Failed cleanup retains the entry and its client's explicit error /
@@ -771,11 +765,12 @@ class WslAgentRuntimePool {
771
765
  ...cached,
772
766
  target: entry.target,
773
767
  runtime: {
768
+ ...(cached.runtime || {}),
774
769
  target: entry.target,
775
770
  workspaceKey: entry.target.workspaceKey,
776
771
  runtimeKey: entry.target.runtimeKey,
777
- runId: intent.runId,
778
- generation: intent.generation,
772
+ runId: intent?.runId || entry.lastRunId,
773
+ generation: intent?.generation || entry.lastGeneration,
779
774
  running: true,
780
775
  stopRequested: true,
781
776
  workRuns: cachedRuntime?.workRuns || [],
@@ -144,7 +144,9 @@ export declare class LLMProvider {
144
144
  * protocol field. Prompt-only JSON requests must not be treated as evidence
145
145
  * that a deployment supports structured output.
146
146
  */
147
- chatStrictJson(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, schema: Readonly<Record<string, unknown>>, signal?: AbortSignal): Promise<string>;
147
+ chatStrictJson(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, schema: Readonly<Record<string, unknown>>, signal?: AbortSignal, reasoningTier?: string, schemaName?: string, onUsage?: (usage: NonNullable<StreamToken['usage']>) => void, reasoningControlRequired?: boolean): Promise<string>;
148
+ private requiresNativeReasoningControl;
149
+ private supportsNativeNoReasoningEffort;
148
150
  /**
149
151
  * Small streaming probe with explicit terminal-event evidence. Reading until
150
152
  * the socket closes is insufficient: truncated SSE must not validate.
@@ -238,6 +238,12 @@ class LLMProvider {
238
238
  }
239
239
  }
240
240
  reasoningEffort(model, tier) {
241
+ // JEV route decisions explicitly request the lowest native reasoning mode.
242
+ // This bypasses per-model tier maps so a configured map cannot silently
243
+ // turn a "no thinking" decision request back into medium effort.
244
+ if (tier === 'none') {
245
+ return this.supportsNativeNoReasoningEffort(model) ? 'none' : undefined;
246
+ }
241
247
  const mapped = this.mappedNativeEffort(model, tier);
242
248
  if (mapped !== undefined)
243
249
  return mapped;
@@ -410,6 +416,20 @@ class LLMProvider {
410
416
  // several isolated workers concurrently call a plain-HTTP local provider.
411
417
  // Node's HTTP client owns the full body lifecycle and is deterministic for
412
418
  // loopback transports, while remote HTTPS providers retain fetch semantics.
419
+ /**
420
+ * dev-0.6.6: serialize the request body **once** per attempt and reuse that
421
+ * same string for the loopback transport and for the Node-HTTP fallback.
422
+ * Re-serializing the whole context on the fallback path held a second full
423
+ * copy of a potentially very large payload (the heap snapshot showed an
424
+ * 84 MiB serialized request body) for no behavioural difference — the bytes
425
+ * are identical.
426
+ */
427
+ let serializedBody = '';
428
+ const bodyText = () => {
429
+ if (!serializedBody)
430
+ serializedBody = JSON.stringify(body);
431
+ return serializedBody;
432
+ };
413
433
  if (this.isPlainHttpLoopback(url)) {
414
434
  const pathname = (() => { try {
415
435
  return new URL(url).pathname;
@@ -418,7 +438,7 @@ class LLMProvider {
418
438
  return '';
419
439
  } })();
420
440
  this.transportDiagnostic('loopback:start', pathname);
421
- const local = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal, effectiveTimeout);
441
+ const local = await this.nodeHttpJson('POST', url, headers, bodyText(), signal, effectiveTimeout);
422
442
  this.transportDiagnostic('loopback:complete', `status=${local.status} bytes=${Buffer.byteLength(local.body || '')}`);
423
443
  return {
424
444
  ok: local.status >= 200 && local.status < 300,
@@ -441,7 +461,7 @@ class LLMProvider {
441
461
  const response = await this.providerFetch(url, {
442
462
  method: 'POST',
443
463
  headers,
444
- body: JSON.stringify(body),
464
+ body: bodyText(),
445
465
  signal: abort.signal,
446
466
  }, true);
447
467
  // Keep the caller's cancellation and explicit deadline attached through
@@ -462,7 +482,7 @@ class LLMProvider {
462
482
  throw abortFailure(abort.signal);
463
483
  if (!this.shouldUseNodeHttpFallback(e))
464
484
  throw e;
465
- const fallback = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal, effectiveTimeout);
485
+ const fallback = await this.nodeHttpJson('POST', url, headers, bodyText(), signal, effectiveTimeout);
466
486
  return {
467
487
  ok: fallback.status >= 200 && fallback.status < 300,
468
488
  status: fallback.status,
@@ -890,7 +910,7 @@ class LLMProvider {
890
910
  };
891
911
  const effort = this.reasoningEffort(model, reasoningTier);
892
912
  if (effort)
893
- body.reasoning = { effort, summary: 'auto' };
913
+ body.reasoning = effort === 'none' ? { effort } : { effort, summary: 'auto' };
894
914
  if (systemPrompt)
895
915
  body.instructions = systemPrompt;
896
916
  const convertedTools = this.responsesTools(tools);
@@ -1449,8 +1469,14 @@ class LLMProvider {
1449
1469
  * protocol field. Prompt-only JSON requests must not be treated as evidence
1450
1470
  * that a deployment supports structured output.
1451
1471
  */
1452
- async chatStrictJson(model, messages, systemPrompt, temperature, maxTokens, schema, signal) {
1453
- const schemaName = 'newmark_validation_probe';
1472
+ async chatStrictJson(model, messages, systemPrompt, temperature, maxTokens, schema, signal, reasoningTier, schemaName = 'newmark_validation_probe', onUsage, reasoningControlRequired = false) {
1473
+ if (reasoningTier === 'none' && this.protocol() !== 'anthropic'
1474
+ && (reasoningControlRequired || this.requiresNativeReasoningControl(model))) {
1475
+ const supportsNone = this.protocol() !== 'github_models' && this.supportsNativeNoReasoningEffort(model);
1476
+ if (!supportsNone) {
1477
+ throw new Error('The current model does not expose a native no-reasoning mode; JEV requires thinking off.');
1478
+ }
1479
+ }
1454
1480
  if (this.protocol() === 'anthropic') {
1455
1481
  const { system, messages: anthropicMessages } = this.anthropicMessages(messages, systemPrompt);
1456
1482
  const body = {
@@ -1462,26 +1488,34 @@ class LLMProvider {
1462
1488
  format: { type: 'json_schema', schema },
1463
1489
  },
1464
1490
  };
1491
+ if (reasoningTier === 'none') {
1492
+ // Fail closed if the provider rejects disabled thinking for this
1493
+ // request; Auto routing has no alternate decision selector.
1494
+ body.thinking = { type: 'disabled' };
1495
+ }
1465
1496
  if (system)
1466
1497
  body.system = system;
1467
1498
  const response = await this.postJsonWithFetchFallback((0, provider_request_compat_1.providerEndpoint)(this.cleanBaseUrl(), '/messages'), this.anthropicHeaders(), body, 120000, signal);
1468
1499
  if (!response.ok)
1469
1500
  throw new Error(this.llmErrorText(response, await response.text()));
1470
1501
  const json = await response.json();
1502
+ onUsage?.((0, agentKernelDiagnostics_1.extractProviderUsage)(json, { protocol: 'anthropic' }));
1471
1503
  return (json.content || [])
1472
1504
  .filter(block => block.type === 'text' && block.text)
1473
1505
  .map(block => this.extractTextValue(block.text))
1474
1506
  .join('');
1475
1507
  }
1476
1508
  if (this.protocol() !== 'github_models' && this.openAITransportMode() === 'responses') {
1477
- const body = this.responsesBody(model, messages, systemPrompt, temperature, maxTokens);
1509
+ const body = this.responsesBody(model, messages, systemPrompt, temperature, maxTokens, [], reasoningTier);
1478
1510
  body.text = {
1479
1511
  format: { type: 'json_schema', name: schemaName, strict: true, schema },
1480
1512
  };
1481
1513
  const response = await this.postJsonWithFetchFallback((0, provider_request_compat_1.providerEndpoint)(this.cleanBaseUrl(), '/responses'), this.openAIHeaders(), body, 120000, signal);
1482
1514
  if (!response.ok)
1483
1515
  throw new Error(this.llmErrorText(response, await response.text()));
1484
- return this.extractResponsesText(this.normalizeResponsesPayload(await response.json()));
1516
+ const json = this.normalizeResponsesPayload(await response.json());
1517
+ onUsage?.((0, agentKernelDiagnostics_1.extractProviderUsage)(json));
1518
+ return this.extractResponsesText(json);
1485
1519
  }
1486
1520
  const isGitHubModels = this.protocol() === 'github_models';
1487
1521
  const url = isGitHubModels
@@ -1500,10 +1534,20 @@ class LLMProvider {
1500
1534
  json_schema: { name: schemaName, strict: true, schema },
1501
1535
  },
1502
1536
  };
1537
+ if (!isGitHubModels)
1538
+ this.applyChatReasoningEffort(body, model, reasoningTier);
1503
1539
  const response = await this.postJsonWithFetchFallback(url, isGitHubModels ? this.githubModelsHeaders() : this.openAIHeaders(), body, 120000, signal);
1504
1540
  if (!response.ok)
1505
1541
  throw new Error(this.llmErrorText(response, await response.text()));
1506
- return this.extractChatCompletionText(await response.json());
1542
+ const json = await response.json();
1543
+ onUsage?.((0, agentKernelDiagnostics_1.extractProviderUsage)(json));
1544
+ return this.extractChatCompletionText(json);
1545
+ }
1546
+ requiresNativeReasoningControl(model) {
1547
+ return /(?:^|[/:])(?:gpt-5(?:[-.]|$)|o[134](?:[-.]|$))|(?:reasoner|reasoning|deepseek-r1|deepseek-reasoner|\br1\b)/i.test(model);
1548
+ }
1549
+ supportsNativeNoReasoningEffort(model) {
1550
+ return /(?:^|[/:])gpt-5\.(?:[1-9]\d*)(?:[-.]|$)/i.test(model);
1507
1551
  }
1508
1552
  /**
1509
1553
  * Small streaming probe with explicit terminal-event evidence. Reading until
package/dist/main.js CHANGED
@@ -361,6 +361,13 @@ function dispatchAgentWorkEvent(event, mirrorToMobile = true) {
361
361
  }
362
362
  const workEventCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => dispatchAgentWorkEvent(event, true));
363
363
  const workEventNoMobileCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => dispatchAgentWorkEvent(event, false));
364
+ /**
365
+ * dev-0.6.6: transcript revisions a renderer has actually painted/cached, keyed
366
+ * by `webContentsId::workspaceId::conversationId`. Only what the renderer
367
+ * itself acknowledged is ever attached to a state request, so a slim snapshot
368
+ * can never be handed to a renderer that does not hold that exact revision.
369
+ */
370
+ const transcriptRevisionAcks = new Map();
364
371
  let projectConversationEvent = null;
365
372
  function publishConversationList(workspace = agent?.workspace.current || null) {
366
373
  if (agent && workspace)
@@ -2153,9 +2160,7 @@ else {
2153
2160
  conversationLocked: false,
2154
2161
  routeDecision: startupAgent.lastRouteDecision,
2155
2162
  resolvedDeployment: startupAgent.activeDeployment(),
2156
- autoRouteRatingAvailable: startupAgent.model === 'auto'
2157
- && startupAgent.lastRouteDecision?.requestedSelection.kind === 'auto'
2158
- && !!startupAgent.lastRouteDecision.resolvedDeployment,
2163
+ jevDecisionModel: startupAgent.jevDecisionModelState(),
2159
2164
  };
2160
2165
  };
2161
2166
  const ensureStartupAutomation = async () => {
@@ -2804,7 +2809,22 @@ else {
2804
2809
  });
2805
2810
  const ensureElectronUtilityPool = () => {
2806
2811
  if (!electronUtilityRuntimePool) {
2807
- electronUtilityRuntimePool = new electronUtilityRuntimePool_1.ElectronUtilityRuntimePool(root, ensureElectronUtilityRuntimeHost());
2812
+ electronUtilityRuntimePool = new electronUtilityRuntimePool_1.ElectronUtilityRuntimePool(root, ensureElectronUtilityRuntimeHost(), undefined, {
2813
+ // dev-0.6.6: per-conversation visual budget. The pool recycles an idle
2814
+ // host whose resident memory grew past the machine-derived watermark
2815
+ // (long ComputerUse/BrowserControl conversations) instead of reusing a
2816
+ // process that is already close to a V8 abort.
2817
+ sampleProcessMemoryBytes: pid => {
2818
+ try {
2819
+ const metric = electron_1.app.getAppMetrics().find(item => Number(item.pid) === Number(pid));
2820
+ // Electron reports workingSetSize in kilobytes.
2821
+ return metric && metric.memory ? Math.round(Number(metric.memory.workingSetSize || 0) * 1024) : 0;
2822
+ }
2823
+ catch {
2824
+ return 0;
2825
+ }
2826
+ },
2827
+ });
2808
2828
  electronUtilityRuntimePool.subscribe(event => broadcastAgentWorkEvent(event));
2809
2829
  electronUtilityRuntimePool.setHostToolHandler(utilityHostToolHandler);
2810
2830
  }
@@ -3123,7 +3143,7 @@ else {
3123
3143
  };
3124
3144
  const mutateConversationQueue = async (target, rawAction, input = {}) => {
3125
3145
  const action = rawAction.replace(/^queue_/, '');
3126
- if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'set_pause', 'guide'].includes(action))
3146
+ if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'set_pause', 'repair_blocked', 'guide'].includes(action))
3127
3147
  throw new Error('Unknown queue action');
3128
3148
  const key = activeFlowStateKey(target);
3129
3149
  const flow = activeFlowStateFor(target);
@@ -3972,6 +3992,18 @@ else {
3972
3992
  const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
3973
3993
  return (await applyConversationAction(target, 'goal_clear')).cleared;
3974
3994
  });
3995
+ // dev-0.6.6: a renderer reports the transcript revision it just painted (or
3996
+ // `''` to clear, e.g. on reload). Nothing else may set this value.
3997
+ electron_1.ipcMain.handle('conversation:ackTranscript', (event, targetInput, revision) => {
3998
+ const target = conversationRuntimeTarget(targetInput || agent?.activeConversationId || 'default');
3999
+ const key = `${event.sender.id}::${String(target.workspaceId || '')}::${String(target.conversationId || '')}`;
4000
+ const value = typeof revision === 'string' ? revision.slice(0, 200) : '';
4001
+ if (value)
4002
+ transcriptRevisionAcks.set(key, value);
4003
+ else
4004
+ transcriptRevisionAcks.delete(key);
4005
+ return true;
4006
+ });
3975
4007
  electron_1.ipcMain.handle('agent:getState', async (event, targetInput) => {
3976
4008
  const startupPrewarmRequest = isStartupPrewarmSender(event);
3977
4009
  if (startupPrewarmRequest && !agent && startupAgentReady)
@@ -3989,11 +4021,19 @@ else {
3989
4021
  : {});
3990
4022
  const requestedWindow = Math.max(1, Math.floor(Number(windowInput.window) || 0) || 200);
3991
4023
  const requestedBefore = windowInput.before == null ? undefined : Math.max(0, Math.floor(Number(windowInput.before) || 0));
4024
+ // dev-0.6.6 long-run memory reclamation: attach the transcript revision
4025
+ // this exact renderer acknowledged, so the runtime can answer with a slim
4026
+ // snapshot instead of re-serializing the whole conversation. The revision
4027
+ // is only ever set from that renderer's own acknowledgement, and a caller
4028
+ // that holds nothing simply never matches.
4029
+ const acknowledgedRevision = transcriptRevisionAcks.get(`${event.sender.id}::${String(target.workspaceId || '')}::${String(target.conversationId || '')}`);
3992
4030
  let conversationSnapshot;
3993
4031
  try {
3994
4032
  conversationSnapshot = startupPrewarmRequest
3995
4033
  ? localConversationSnapshotForStartup(target, { window: requestedWindow, before: requestedBefore })
3996
- : await runtimeSnapshotForTarget(target, { window: requestedWindow, before: requestedBefore });
4034
+ : await runtimeSnapshotForTarget(target, acknowledgedRevision
4035
+ ? { window: requestedWindow, before: requestedBefore, transcript: 'auto', transcriptRevision: acknowledgedRevision }
4036
+ : { window: requestedWindow, before: requestedBefore });
3997
4037
  }
3998
4038
  catch (error) {
3999
4039
  conversationSnapshot = {
@@ -4015,6 +4055,7 @@ else {
4015
4055
  modelLabel: agent.modelLabel(),
4016
4056
  resolvedDeployment: agent.activeDeployment(),
4017
4057
  routeDecision: agent.lastRouteDecision,
4058
+ jevDecisionModel: agent.jevDecisionModelState(),
4018
4059
  intelligence: agent.intelligence,
4019
4060
  ...conversationSnapshot,
4020
4061
  chatMessages: conversationSnapshot.chatMessages || [],
@@ -4247,11 +4288,7 @@ else {
4247
4288
  agent.config.set('models', 'fallback_on_unavailable', value === true || value === 'on');
4248
4289
  break;
4249
4290
  case 'switchTendency':
4250
- agent.config.set('models', 'auto_switch_preference', value);
4251
- break;
4252
- case 'clearLearnedAutoPreferences':
4253
- if (value === true)
4254
- agent.clearLearnedModelPreferences();
4291
+ agent.config.set('models', 'auto_switch_preference', ['conservative', 'balanced', 'aggressive'].includes(String(value)) ? String(value) : 'balanced');
4255
4292
  break;
4256
4293
  case 'openAIApiMode':
4257
4294
  agent.config.set('models', 'openai_api_mode', ['chat_stream', 'chat', 'responses'].includes(String(value)) ? value : 'chat_stream');
@@ -4482,17 +4519,38 @@ else {
4482
4519
  ? await ensureWslConversationPool().contextCompress(target, options)
4483
4520
  : await ensureElectronUtilityPool().contextCompress(target, options);
4484
4521
  });
4485
- electron_1.ipcMain.handle('agent:rateAutoRoute', async (_event, request) => {
4522
+ /** Current-model JEV policy editor; the Agent remains authoritative. */
4523
+ electron_1.ipcMain.handle('agent:setJevDecisionModel', async (_event, request) => {
4486
4524
  if (!agent)
4487
- return { ok: false, reason: 'no_active_auto_route' };
4488
- const target = conversationRuntimeTarget(request);
4489
- const score = Number(request?.score);
4490
- const routeId = String(request?.routeId || '');
4491
- if (mainConversationOwners.has(activeFlowStateKey(target)))
4492
- return ensureConversationKernel(root).rateAutoRoute(target, score, routeId);
4493
- return wslBackendEnabled()
4494
- ? await ensureWslConversationPool().rateAutoRoute(target, score, routeId)
4495
- : await ensureElectronUtilityPool().rateAutoRoute(target, score, routeId);
4525
+ return { ok: false, reason: 'auto_disabled', state: null };
4526
+ const patch = request && typeof request === 'object' ? request : {};
4527
+ const result = agent.updateJevDecisionModel({
4528
+ ...(patch.enabled === undefined ? {} : { enabled: patch.enabled === true }),
4529
+ ...(patch.timeoutMs === undefined ? {} : { timeoutMs: Number(patch.timeoutMs) }),
4530
+ });
4531
+ if (result.ok) {
4532
+ resetConversationKernel();
4533
+ // Every runtime (main, Electron Utility, WSL) uses the same current
4534
+ // conversation-model decision policy.
4535
+ const propagated = [
4536
+ ['jev_enabled', result.state.enabled],
4537
+ ['jev_timeout_ms', result.state.timeoutMs],
4538
+ ];
4539
+ for (const pool of [electronUtilityRuntimePool, wslAgentRuntimePool]) {
4540
+ if (!pool)
4541
+ continue;
4542
+ for (const [key, value] of propagated) {
4543
+ try {
4544
+ await pool.updateSetting('models', key, value);
4545
+ }
4546
+ catch {
4547
+ // A runtime that is not currently running picks the value up from
4548
+ // the shared config on its next start.
4549
+ }
4550
+ }
4551
+ }
4552
+ }
4553
+ return result;
4496
4554
  });
4497
4555
  electron_1.ipcMain.handle('agent:stopConversation', async (_event, request) => {
4498
4556
  if (!agent)
@@ -4655,19 +4713,38 @@ else {
4655
4713
  try {
4656
4714
  // Archive is a destructive lifecycle command. It intentionally
4657
4715
  // bypasses the normal mutation/active-prompt guard. Cancel the
4658
- // conversation-local Flow synchronously, then start the resident
4659
- // runtime hard-stop in the background so a stuck child cannot hold
4660
- // the archive click hostage.
4716
+ // conversation-local Flow synchronously, then release the occupancy
4717
+ // the conversation is holding -- the hard stop of its resident
4718
+ // runtime plus the kernel's own abort bookkeeping -- and let those
4719
+ // teardown paths run *in parallel* instead of one after the other.
4720
+ //
4721
+ // Releasing the occupancy is what ends the wait for a wedged run:
4722
+ // the hard stop invalidates the in-flight request, so the run really
4723
+ // settles instead of the archive giving up on it after some budget
4724
+ // (and abandoning its transcript to a later resurrecting flush).
4661
4725
  const ownedFlow = activeFlowStateFor(normalized);
4662
- const mainOwnerPreparation = mainConversationOwners.has(normalized.runtimeKey)
4663
- ? ensureConversationKernel(root).prepareForArchive(normalized) : Promise.resolve(null);
4664
- interruptActiveFlowForArchive(normalized);
4665
- const mainArchiveOwner = await mainOwnerPreparation;
4666
- if (ownedFlow?.settlement)
4667
- await ownedFlow.settlement;
4668
- void forceStopTargetRuntime(normalized).catch(error => {
4726
+ const kernelOwned = mainConversationOwners.has(normalized.runtimeKey);
4727
+ const targetRuntime = kernelOwned
4728
+ ? { resident: true, running: false, stopping: false, connected: true }
4729
+ : peekTargetRuntime(normalized);
4730
+ // An idle runtime has nothing to settle, so its stop stays off the
4731
+ // critical path; a running (or already stopping) one must be down and
4732
+ // checkpointed before the writer snapshots the store.
4733
+ const occupancyMustSettle = !kernelOwned && (targetRuntime.running === true || targetRuntime.stopping === true);
4734
+ const occupancyRelease = forceStopTargetRuntime(normalized).catch(error => {
4669
4735
  console.error('[Newmark] archive runtime force-stop failed:', error instanceof Error ? error.message : String(error));
4670
4736
  });
4737
+ const mainOwnerPreparation = kernelOwned
4738
+ ? ensureConversationKernel(root).prepareForArchive(normalized, {
4739
+ releaseOccupancy: () => occupancyRelease,
4740
+ })
4741
+ : Promise.resolve(null);
4742
+ interruptActiveFlowForArchive(normalized);
4743
+ const [mainArchiveOwner] = await Promise.all([
4744
+ mainOwnerPreparation,
4745
+ occupancyMustSettle ? occupancyRelease : Promise.resolve(),
4746
+ ownedFlow?.settlement ?? Promise.resolve(),
4747
+ ]);
4671
4748
  const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
4672
4749
  const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
4673
4750
  const ownsTargetWorkspace = !!normalized.workspace
package/dist/preload.js CHANGED
@@ -15,7 +15,7 @@ contextBridge.exposeInMainWorld('api', {
15
15
  queueAction: (action, input, target) => ipcRenderer.invoke('agent:queueAction', action, input, target),
16
16
  checkpointConversation: (request) => ipcRenderer.invoke('agent:checkpointConversation', request),
17
17
  compressContext: (request) => ipcRenderer.invoke('agent:compressContext', request),
18
- rateAutoRoute: (request) => ipcRenderer.invoke('agent:rateAutoRoute', request),
18
+ setJevDecisionModel: (request) => ipcRenderer.invoke('agent:setJevDecisionModel', request),
19
19
  stopConversation: (request) => ipcRenderer.invoke('agent:stopConversation', request),
20
20
  setWorkRunExpanded: (request) => ipcRenderer.invoke('agent:setWorkRunExpanded', request),
21
21
  sendPrompt: (message, _model) => ipcRenderer.invoke('agent:sendPrompt', message),
@@ -30,6 +30,8 @@ contextBridge.exposeInMainWorld('api', {
30
30
  clearGoal: (target) => ipcRenderer.invoke('agent:clearGoal', target),
31
31
  ensureConversation: (target) => ipcRenderer.invoke('agent:ensureConversation', target),
32
32
  getState: (target) => ipcRenderer.invoke('agent:getState', target),
33
+ /** dev-0.6.6: report (or clear with '') the transcript revision this renderer holds. */
34
+ ackTranscript: (target, revision) => ipcRenderer.invoke('conversation:ackTranscript', target, revision),
33
35
  getConversationDraft: (conversationId) => ipcRenderer.invoke('conversation:getDraft', conversationId),
34
36
  saveConversationDraft: (conversationId, draft) => ipcRenderer.invoke('conversation:saveDraft', conversationId, draft),
35
37
  getConversationPlan: (conversationId) => ipcRenderer.invoke('agent:getConversationPlan', conversationId),