newmark-agent 0.3.11 → 0.3.12

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 (39) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +7 -0
  3. package/dist/cli-commands.js +206 -15
  4. package/dist/cli-discovery.d.ts +15 -0
  5. package/dist/cli-discovery.js +182 -0
  6. package/dist/cli-help.d.ts +2 -0
  7. package/dist/cli-help.js +25 -1
  8. package/dist/conversation-utility-host.bundle.cjs +357 -84
  9. package/dist/core/agent.d.ts +18 -3
  10. package/dist/core/agent.js +214 -30
  11. package/dist/core/agentKernelRunner.js +53 -8
  12. package/dist/core/config.d.ts +7 -2
  13. package/dist/core/config.js +24 -6
  14. package/dist/core/conversationKernel.js +1 -1
  15. package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
  16. package/dist/core/electronUtilityRuntimePool.js +62 -0
  17. package/dist/core/flow-runner.js +1 -1
  18. package/dist/core/modelValidationStore.d.ts +4 -1
  19. package/dist/core/modelValidationStore.js +7 -1
  20. package/dist/core/workspace.d.ts +6 -0
  21. package/dist/core/workspace.js +14 -0
  22. package/dist/core/wslAgentRuntimePool.d.ts +4 -0
  23. package/dist/core/wslAgentRuntimePool.js +56 -0
  24. package/dist/launcher.js +40 -11
  25. package/dist/llm/provider.d.ts +8 -5
  26. package/dist/llm/provider.js +85 -33
  27. package/dist/main.js +167 -45
  28. package/dist/preload.js +6 -0
  29. package/dist/providers/chat-completions.adapter.js +1 -5
  30. package/dist/providers/provider-events.d.ts +7 -0
  31. package/dist/providers/provider-events.js +44 -0
  32. package/dist/providers/responses.adapter.js +1 -3
  33. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  34. package/dist/tui/src/app.js +47 -13
  35. package/dist/tui/src/render.js +23 -7
  36. package/dist/tui/src/state.js +61 -9
  37. package/dist/ui/index.html +250 -84
  38. package/dist/wsl-agent-host.bundle.cjs +357 -84
  39. package/package.json +14 -5
@@ -326959,7 +326959,12 @@ async function runFlowBuild(agent, prompt, options) {
326959
326959
  if (!options.signal?.aborted && typeof agent.emitWorkEvent === "function") {
326960
326960
  agent.emitWorkEvent({ type: "error", content: reportedError.message, runId });
326961
326961
  }
326962
- agent.finishConversationWorkRun(runId, options.signal?.aborted ? "interrupted" : "error");
326962
+ agent.finishConversationWorkRun(
326963
+ runId,
326964
+ options.signal?.aborted ? "interrupted" : "error",
326965
+ void 0,
326966
+ options.signal?.aborted ? "" : reportedError.message
326967
+ );
326963
326968
  agent.flushWorkspaceConversationState();
326964
326969
  }
326965
326970
  throw reportedError;
@@ -327619,8 +327624,10 @@ var ConfigManager = class {
327619
327624
  rootPath;
327620
327625
  config;
327621
327626
  workspaceOverrides;
327622
- constructor(rootPath) {
327627
+ readOnly;
327628
+ constructor(rootPath, options = {}) {
327623
327629
  this.rootPath = rootPath;
327630
+ this.readOnly = options.readOnly === true;
327624
327631
  this.workspaceOverrides = /* @__PURE__ */ new Map();
327625
327632
  this.config = this.load();
327626
327633
  }
@@ -327635,17 +327642,19 @@ var ConfigManager = class {
327635
327642
  const raw = JSON.parse(readJsonText(cp));
327636
327643
  const normalized = normalizeConfigShape(raw, true);
327637
327644
  if (isCorruptConfig(raw, normalized)) {
327645
+ if (this.readOnly) return defaultConfig();
327638
327646
  this.backupConfig(cp, "invalid-shape");
327639
327647
  return this.writeRecoveredConfig(cp);
327640
327648
  }
327641
327649
  if (migrateProviderIdsInConfig(normalized)) {
327642
327650
  try {
327643
- fs3.writeFileSync(cp, JSON.stringify(normalized, null, 2), "utf-8");
327651
+ if (!this.readOnly) fs3.writeFileSync(cp, JSON.stringify(normalized, null, 2), "utf-8");
327644
327652
  } catch {
327645
327653
  }
327646
327654
  }
327647
327655
  return normalized;
327648
327656
  } catch {
327657
+ if (this.readOnly) return defaultConfig();
327649
327658
  this.backupConfig(cp, "invalid-json");
327650
327659
  return this.writeRecoveredConfig(cp);
327651
327660
  }
@@ -327691,10 +327700,12 @@ var ConfigManager = class {
327691
327700
  this.config[section][key3] = { value: normalizedValue };
327692
327701
  }
327693
327702
  save() {
327703
+ if (this.readOnly) return;
327694
327704
  const j2 = JSON.stringify(this.config, null, 2);
327695
327705
  fs3.writeFileSync(path3.join(this.rootPath, "config.json"), j2, "utf-8");
327696
327706
  }
327697
327707
  saveTo(targetPath) {
327708
+ if (this.readOnly) return;
327698
327709
  const j2 = JSON.stringify(this.config, null, 2);
327699
327710
  fs3.writeFileSync(targetPath, j2, "utf-8");
327700
327711
  }
@@ -327919,12 +327930,14 @@ var ConfigManager = class {
327919
327930
  return providers;
327920
327931
  }
327921
327932
  writeRecoveredConfig(configPath) {
327933
+ if (this.readOnly) return defaultConfig();
327922
327934
  const config = loadExampleConfig();
327923
327935
  fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
327924
327936
  fs3.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
327925
327937
  return config;
327926
327938
  }
327927
327939
  backupConfig(configPath, reason) {
327940
+ if (this.readOnly) return;
327928
327941
  try {
327929
327942
  if (!fs3.existsSync(configPath)) return;
327930
327943
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -328269,7 +328282,10 @@ function defaultConfig() {
328269
328282
  general: {
328270
328283
  tone: { _description: "Conversation style", _type: "choice", _values: ["strict_simple", "casual_friendly"], value: "strict_simple" },
328271
328284
  language: { _description: "Default language", _type: "choice", _values: ["en", "zh", "auto"], value: "auto" },
328272
- close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "minimize" },
328285
+ // A first-run desktop window must have a deterministic close/exit
328286
+ // contract. Users who explicitly choose minimize-to-tray keep that
328287
+ // choice, but a fresh install must not hide the process on OS close.
328288
+ close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "exit" },
328273
328289
  default_input: { _description: "Default input mode", _type: "choice", _values: ["guide", "next"], value: "guide" },
328274
328290
  auto_archive_on_close: { _description: "Auto archive on close", _type: "boolean", value: true }
328275
328291
  },
@@ -328608,6 +328624,38 @@ function providerAbortError(signal) {
328608
328624
  if (!error.name || error.name === "Error") error.name = "AbortError";
328609
328625
  return error;
328610
328626
  }
328627
+ function providerStreamTimeoutError(timeoutMs) {
328628
+ const error = new Error("Stream read timeout");
328629
+ error.name = "TimeoutError";
328630
+ error.message = `Stream read timeout after ${timeoutMs}ms`;
328631
+ return error;
328632
+ }
328633
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328634
+ if (signal.aborted) throw providerAbortError(signal);
328635
+ let timer;
328636
+ let onAbort;
328637
+ const abortPromise = new Promise((_3, reject) => {
328638
+ onAbort = () => reject(providerAbortError(signal));
328639
+ signal.addEventListener("abort", onAbort, { once: true });
328640
+ });
328641
+ const timeoutPromise = new Promise((_3, reject) => {
328642
+ timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
328643
+ });
328644
+ try {
328645
+ return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
328646
+ } catch (error) {
328647
+ if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
328648
+ try {
328649
+ await reader.cancel(error);
328650
+ } catch {
328651
+ }
328652
+ }
328653
+ throw error;
328654
+ } finally {
328655
+ if (timer) clearTimeout(timer);
328656
+ if (onAbort) signal.removeEventListener("abort", onAbort);
328657
+ }
328658
+ }
328611
328659
  function parseProviderSse(raw) {
328612
328660
  const events = [];
328613
328661
  for (const block of String(raw || "").replace(/\r\n/g, "\n").split(/\n\n+/)) {
@@ -328843,12 +328891,7 @@ var ChatCompletionsAdapter = class {
328843
328891
  let emittedTool = false;
328844
328892
  try {
328845
328893
  while (true) {
328846
- if (signal.aborted) throw providerAbortError(signal);
328847
- const readPromise = reader.read();
328848
- const timeoutPromise = new Promise(
328849
- (_3, reject) => setTimeout(() => reject(new Error("Stream read timeout")), 3e4)
328850
- );
328851
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
328894
+ const { done, value } = await readProviderStreamChunk(reader, signal);
328852
328895
  if (done) break;
328853
328896
  buffer += decoder.decode(value, { stream: true });
328854
328897
  const lines = buffer.split("\n");
@@ -329086,8 +329129,7 @@ var ResponsesAdapter = class {
329086
329129
  let streamError = "";
329087
329130
  try {
329088
329131
  while (true) {
329089
- if (signal.aborted) throw providerAbortError(signal);
329090
- const { done, value } = await reader.read();
329132
+ const { done, value } = await readProviderStreamChunk(reader, signal);
329091
329133
  if (done) break;
329092
329134
  buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
329093
329135
  const blocks = buffer.split(/\n\n+/);
@@ -329297,6 +329339,16 @@ function createProviderAdapter(providerId, apiMode) {
329297
329339
  }
329298
329340
 
329299
329341
  // src/llm/provider.ts
329342
+ var DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 9e4;
329343
+ var MIN_PROVIDER_REQUEST_TIMEOUT_MS = 50;
329344
+ function providerTimeoutError(timeoutMs) {
329345
+ const error = new Error(`Provider request timed out after ${timeoutMs}ms`);
329346
+ error.name = "TimeoutError";
329347
+ return error;
329348
+ }
329349
+ function isProviderTimeoutError(error) {
329350
+ return error instanceof Error && error.name === "TimeoutError";
329351
+ }
329300
329352
  function abortFailure(signal) {
329301
329353
  const reason = signal?.reason;
329302
329354
  const error = reason instanceof Error ? reason : new Error(reason ? String(reason) : "LLM request aborted");
@@ -329326,13 +329378,14 @@ function parseProviderSse2(raw) {
329326
329378
  return events;
329327
329379
  }
329328
329380
  var LLMProvider = class _LLMProvider {
329329
- constructor(name50, baseUrl, apiKey, explicitProtocol, openAIMode = "chat_stream", useProviderAdaptersV2 = false) {
329381
+ constructor(name50, baseUrl, apiKey, explicitProtocol, openAIMode = "chat_stream", useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329330
329382
  this.name = name50;
329331
329383
  this.baseUrl = baseUrl;
329332
329384
  this.apiKey = apiKey;
329333
329385
  this.explicitProtocol = explicitProtocol;
329334
329386
  this.openAIMode = openAIMode;
329335
329387
  this.useProviderAdaptersV2 = useProviderAdaptersV2;
329388
+ this.requestTimeoutMs = requestTimeoutMs;
329336
329389
  }
329337
329390
  name;
329338
329391
  baseUrl;
@@ -329340,8 +329393,25 @@ var LLMProvider = class _LLMProvider {
329340
329393
  explicitProtocol;
329341
329394
  openAIMode;
329342
329395
  useProviderAdaptersV2;
329396
+ requestTimeoutMs;
329343
329397
  static nodeHttpTransport = null;
329344
329398
  static powershellTransport = null;
329399
+ effectiveRequestTimeout(timeoutMs) {
329400
+ const requested = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
329401
+ const configured = Number.isFinite(this.requestTimeoutMs) && this.requestTimeoutMs > 0 ? this.requestTimeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
329402
+ return Math.max(MIN_PROVIDER_REQUEST_TIMEOUT_MS, Math.min(requested, configured));
329403
+ }
329404
+ async withRequestTimeout(promise, timeoutMs, signal) {
329405
+ let timer;
329406
+ const timeoutPromise = new Promise((_3, reject) => {
329407
+ timer = setTimeout(() => reject(providerTimeoutError(timeoutMs)), timeoutMs);
329408
+ });
329409
+ try {
329410
+ return await abortable(Promise.race([promise, timeoutPromise]), signal);
329411
+ } finally {
329412
+ if (timer) clearTimeout(timer);
329413
+ }
329414
+ }
329345
329415
  intelligenceConfig(tier) {
329346
329416
  switch (tier) {
329347
329417
  case "low":
@@ -329443,6 +329513,7 @@ var LLMProvider = class _LLMProvider {
329443
329513
  };
329444
329514
  }
329445
329515
  async postJsonWithFetchFallback(url, headers, body, timeoutMs = 12e4, signal) {
329516
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329446
329517
  if (this.isPlainHttpLoopback(url)) {
329447
329518
  const pathname = (() => {
329448
329519
  try {
@@ -329452,7 +329523,7 @@ var LLMProvider = class _LLMProvider {
329452
329523
  }
329453
329524
  })();
329454
329525
  this.transportDiagnostic("loopback:start", pathname);
329455
- const local = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal);
329526
+ const local = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal, effectiveTimeout);
329456
329527
  this.transportDiagnostic("loopback:complete", `status=${local.status} bytes=${Buffer.byteLength(local.body || "")}`);
329457
329528
  return {
329458
329529
  ok: local.status >= 200 && local.status < 300,
@@ -329466,7 +329537,7 @@ var LLMProvider = class _LLMProvider {
329466
329537
  const forwardAbort = () => abort.abort(signal?.reason);
329467
329538
  if (signal?.aborted) forwardAbort();
329468
329539
  else signal?.addEventListener("abort", forwardAbort, { once: true });
329469
- const timer = setTimeout(() => abort.abort(), timeoutMs);
329540
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
329470
329541
  try {
329471
329542
  const response = await fetch(url, {
329472
329543
  method: "POST",
@@ -329477,8 +329548,9 @@ var LLMProvider = class _LLMProvider {
329477
329548
  return response;
329478
329549
  } catch (e3) {
329479
329550
  if (signal?.aborted) throw abortFailure(signal);
329551
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
329480
329552
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
329481
- const fallback = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal);
329553
+ const fallback = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal, effectiveTimeout);
329482
329554
  return {
329483
329555
  ok: fallback.status >= 200 && fallback.status < 300,
329484
329556
  status: fallback.status,
@@ -329492,14 +329564,16 @@ var LLMProvider = class _LLMProvider {
329492
329564
  }
329493
329565
  }
329494
329566
  async getJsonWithFetchFallback(url, headers, timeoutMs = 3e4) {
329567
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329495
329568
  const abort = new AbortController();
329496
- const timer = setTimeout(() => abort.abort(), timeoutMs);
329569
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
329497
329570
  try {
329498
329571
  const response = await fetch(url, { method: "GET", headers, signal: abort.signal });
329499
329572
  return response;
329500
329573
  } catch (e3) {
329574
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
329501
329575
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
329502
- const fallback = await this.nodeHttpJson("GET", url, headers);
329576
+ const fallback = await this.nodeHttpJson("GET", url, headers, "", void 0, effectiveTimeout);
329503
329577
  return {
329504
329578
  ok: fallback.status >= 200 && fallback.status < 300,
329505
329579
  status: fallback.status,
@@ -329512,14 +329586,16 @@ var LLMProvider = class _LLMProvider {
329512
329586
  }
329513
329587
  }
329514
329588
  shouldUseNodeHttpFallback(error) {
329515
- return error instanceof TypeError && /fetch failed/i.test(error.message) || error instanceof Error && /abort/i.test(error.name || error.message);
329589
+ return error instanceof TypeError && /fetch failed/i.test(error.message);
329516
329590
  }
329517
- nodeHttpJson(method, urlValue, headers, body = "", signal) {
329591
+ nodeHttpJson(method, urlValue, headers, body = "", signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329592
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329518
329593
  if (_LLMProvider.nodeHttpTransport) {
329519
- return abortable(_LLMProvider.nodeHttpTransport(method, urlValue, headers, body), signal).catch((error) => {
329594
+ return this.withRequestTimeout(_LLMProvider.nodeHttpTransport(method, urlValue, headers, body), effectiveTimeout, signal).catch((error) => {
329520
329595
  if (signal?.aborted) throw abortFailure(signal);
329596
+ if (isProviderTimeoutError(error)) throw error;
329521
329597
  if (process.platform === "win32") {
329522
- return this.powershellJson(method, urlValue, headers, body, signal);
329598
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
329523
329599
  }
329524
329600
  throw error;
329525
329601
  });
@@ -329564,8 +329640,8 @@ var LLMProvider = class _LLMProvider {
329564
329640
  else fail(new Error("Node HTTP response closed before completion"));
329565
329641
  });
329566
329642
  });
329567
- req.setTimeout(12e4, () => {
329568
- req.destroy(new Error("Node HTTP fallback timeout"));
329643
+ req.setTimeout(effectiveTimeout, () => {
329644
+ req.destroy(providerTimeoutError(effectiveTimeout));
329569
329645
  });
329570
329646
  req.on("error", reject);
329571
329647
  const onAbort = () => req.destroy(abortFailure(signal));
@@ -329576,15 +329652,17 @@ var LLMProvider = class _LLMProvider {
329576
329652
  req.end();
329577
329653
  }).catch((error) => {
329578
329654
  if (signal?.aborted) throw abortFailure(signal);
329655
+ if (isProviderTimeoutError(error)) throw error;
329579
329656
  if (process.platform === "win32") {
329580
- return this.powershellJson(method, urlValue, headers, body, signal);
329657
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
329581
329658
  }
329582
329659
  throw error;
329583
329660
  });
329584
329661
  }
329585
- powershellJson(method, urlValue, headers, body = "", signal) {
329662
+ powershellJson(method, urlValue, headers, body = "", signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329663
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329586
329664
  if (_LLMProvider.powershellTransport) {
329587
- return _LLMProvider.powershellTransport(method, urlValue, headers, body);
329665
+ return this.withRequestTimeout(_LLMProvider.powershellTransport(method, urlValue, headers, body), effectiveTimeout, signal);
329588
329666
  }
329589
329667
  return new Promise((resolve16, reject) => {
329590
329668
  const headerJson = JSON.stringify(headers);
@@ -329613,7 +329691,7 @@ var LLMProvider = class _LLMProvider {
329613
329691
  " $raw = $headerJson | ConvertFrom-Json",
329614
329692
  " foreach ($p in $raw.PSObject.Properties) { $headers[$p.Name] = [string]$p.Value }",
329615
329693
  "}",
329616
- "$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = 120 }",
329694
+ `'$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = ${Math.max(1, Math.ceil(effectiveTimeout / 1e3))} }`,
329617
329695
  'if ($method -eq "POST") { $params["Body"] = $bodyJson }',
329618
329696
  'if ($method -eq "POST") { $params["ContentType"] = "application/json; charset=utf-8" }',
329619
329697
  "$resp = Invoke-WebRequest @params",
@@ -329645,8 +329723,8 @@ var LLMProvider = class _LLMProvider {
329645
329723
  const timer = setTimeout(() => {
329646
329724
  child.kill();
329647
329725
  cleanup();
329648
- reject(new Error("PowerShell HTTP fallback timeout"));
329649
- }, 13e4);
329726
+ reject(providerTimeoutError(effectiveTimeout));
329727
+ }, effectiveTimeout + 5e3);
329650
329728
  child.stdout.setEncoding("utf8");
329651
329729
  child.stderr.setEncoding("utf8");
329652
329730
  child.stdout.on("data", (chunk) => {
@@ -330094,10 +330172,10 @@ ${responsePath}
330094
330172
  return this.shouldUseResponsesFallback(Number(match[1]), errorText);
330095
330173
  }
330096
330174
  /**
330097
- * Loopback-aware transport injected into adapter `execute`. Mirrors the
330098
- * legacy orchestration exactly: streaming requests go through fetch with a
330099
- * 120s timeout and degrade to a non-streaming node-http request on fetch
330100
- * failure; non-streaming requests reuse postJsonWithFetchFallback.
330175
+ * Loopback-aware transport injected into adapter `execute`. Streaming
330176
+ * requests retain the fetch-to-node fallback for transport failures, while
330177
+ * a local deadline is returned directly so one request cannot become a
330178
+ * second Windows fallback request.
330101
330179
  */
330102
330180
  buildProviderAdapterTransport() {
330103
330181
  return async (request, signal) => {
@@ -330106,7 +330184,8 @@ ${responsePath}
330106
330184
  const forwardAbort = () => abort.abort(signal?.reason);
330107
330185
  if (signal?.aborted) forwardAbort();
330108
330186
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330109
- const timer = setTimeout(() => abort.abort(), 12e4);
330187
+ const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330188
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
330110
330189
  try {
330111
330190
  try {
330112
330191
  return await fetch(request.url, {
@@ -330117,6 +330196,7 @@ ${responsePath}
330117
330196
  });
330118
330197
  } catch (error) {
330119
330198
  if (signal?.aborted) throw abortFailure(signal);
330199
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
330120
330200
  if (!this.shouldUseNodeHttpFallback(error)) throw error;
330121
330201
  const fallbackHeaders = { ...request.headers };
330122
330202
  delete fallbackHeaders["Accept"];
@@ -330124,7 +330204,7 @@ ${responsePath}
330124
330204
  request.url,
330125
330205
  fallbackHeaders,
330126
330206
  { ...request.body, stream: false },
330127
- 12e4,
330207
+ effectiveTimeout,
330128
330208
  signal
330129
330209
  );
330130
330210
  return this.toTransportResponse(fallback);
@@ -330234,7 +330314,8 @@ ${responsePath}
330234
330314
  const forwardAbort = () => abort.abort(signal?.reason);
330235
330315
  if (signal?.aborted) forwardAbort();
330236
330316
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330237
- const timeout = setTimeout(() => abort.abort(), 12e4);
330317
+ const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330318
+ const timeout = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
330238
330319
  let reader = null;
330239
330320
  try {
330240
330321
  let response;
@@ -330246,9 +330327,10 @@ ${responsePath}
330246
330327
  signal: abort.signal
330247
330328
  });
330248
330329
  } catch (e3) {
330330
+ if (signal?.aborted) throw abortFailure(signal);
330331
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
330249
330332
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
330250
330333
  clearTimeout(timeout);
330251
- if (signal?.aborted) throw abortFailure(signal);
330252
330334
  yield* this.githubModelsChatNonStreaming(url, body, signal);
330253
330335
  return;
330254
330336
  }
@@ -330268,13 +330350,9 @@ ${responsePath}
330268
330350
  let currentReasoningContent = "";
330269
330351
  let contentPolicyBlocked = false;
330270
330352
  let emittedContent = false;
330353
+ const streamSignal = signal || new AbortController().signal;
330271
330354
  while (true) {
330272
- if (signal?.aborted) throw abortFailure(signal);
330273
- const readPromise = reader.read();
330274
- const timeoutPromise = new Promise(
330275
- (_3, reject) => setTimeout(() => reject(new Error("Stream read timeout")), 3e4)
330276
- );
330277
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
330355
+ const { done, value } = await readProviderStreamChunk(reader, streamSignal);
330278
330356
  if (done) break;
330279
330357
  buffer += decoder.decode(value, { stream: true });
330280
330358
  const lines = buffer.split("\n");
@@ -337920,6 +337998,19 @@ var WorkspaceManager = class {
337920
337998
  this.saveState();
337921
337999
  }
337922
338000
  }
338001
+ /**
338002
+ * Re-read the registry and persisted current-workspace pointer after another
338003
+ * Newmark entrypoint updates Work/*.json. This intentionally does not create
338004
+ * a workspace: a refresh must reflect the shared on-disk state exactly.
338005
+ */
338006
+ reloadFromStorage() {
338007
+ if (this.detached) return this.current;
338008
+ this.scan();
338009
+ this.validate();
338010
+ this.current = null;
338011
+ this.restoreCurrent();
338012
+ return this.current;
338013
+ }
337923
338014
  saveInternal() {
337924
338015
  if (this.detached) return;
337925
338016
  const p = path16.join(this.rootPath, "Work", "Local.json");
@@ -340010,17 +340101,35 @@ function normalizePublicProviderError(error, secrets = []) {
340010
340101
  }
340011
340102
  return raw.slice(0, 1200);
340012
340103
  }
340104
+ function throwIfKernelAborted(signal) {
340105
+ if (!signal?.aborted) return;
340106
+ const reason = signal.reason;
340107
+ if (reason instanceof Error) {
340108
+ reason.name = "AbortError";
340109
+ throw reason;
340110
+ }
340111
+ const error = new Error(reason ? String(reason) : "Agent run aborted");
340112
+ error.name = "AbortError";
340113
+ throw error;
340114
+ }
340013
340115
  async function runAgentKernel(agent) {
340014
340116
  const stopContextTimer = performanceTimer("context_prepare", { conversationId: agent.activeConversationId });
340117
+ const processSignal = agent.activeProcessSignal();
340118
+ if (processSignal?.aborted) {
340119
+ stopContextTimer();
340120
+ throwIfKernelAborted(processSignal);
340121
+ }
340015
340122
  if (!agent.engineModel()) {
340123
+ const message = "No LLM configured. Add provider in Settings > Models.";
340016
340124
  agent.status = "error";
340017
340125
  agent.saveWorkspaceConversationState();
340018
- return [{ type: "text", text: "[Error] No LLM configured. Add provider in Settings > Models." }];
340126
+ throw new Error(message);
340019
340127
  }
340020
340128
  const [{ Agent: NativeAgent }, KernelStreamCompat] = await Promise.all([
340021
340129
  Promise.resolve().then(() => (init_agentKernel(), agentKernel_exports)),
340022
340130
  Promise.resolve().then(() => (init_stream_types(), stream_types_exports))
340023
340131
  ]);
340132
+ throwIfKernelAborted(processSignal);
340024
340133
  const toolProvisioning = new ToolProvisionSession([], []);
340025
340134
  let activeToolSurfaceIdentity = "";
340026
340135
  let activeToolSurfaceNotice = "";
@@ -340046,6 +340155,7 @@ async function runAgentKernel(agent) {
340046
340155
  const initialToolSurface = refreshToolSurface(true);
340047
340156
  const assembledContext = agent.assembleContextV2(initialToolSurface.systemPromptNotice);
340048
340157
  const systemPrompt = assembledContext.text;
340158
+ throwIfKernelAborted(processSignal);
340049
340159
  let providerRequestCount = 0;
340050
340160
  let bootstrappedCompressionAt = agent.lastCompression?.at || "";
340051
340161
  stopContextTimer();
@@ -340066,6 +340176,24 @@ async function runAgentKernel(agent) {
340066
340176
  kernel2.state.tools = toKernelTools(agent, initialToolSurface.definitions, toolProvisioning);
340067
340177
  kernel2.state.messages = toKernelMessages(agent);
340068
340178
  agent.attachAgentKernelRuntime(kernel2);
340179
+ let detachProcessAbort = () => {
340180
+ };
340181
+ if (processSignal) {
340182
+ const abortKernel = () => kernel2.abort();
340183
+ if (processSignal.aborted) {
340184
+ kernel2.abort();
340185
+ } else {
340186
+ processSignal.addEventListener("abort", abortKernel, { once: true });
340187
+ detachProcessAbort = () => processSignal.removeEventListener("abort", abortKernel);
340188
+ }
340189
+ }
340190
+ try {
340191
+ throwIfKernelAborted(processSignal);
340192
+ } catch (error) {
340193
+ detachProcessAbort();
340194
+ agent.attachAgentKernelRuntime(null);
340195
+ throw error;
340196
+ }
340069
340197
  const tokens = [];
340070
340198
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
340071
340199
  let lastAssistant = null;
@@ -340169,6 +340297,7 @@ async function runAgentKernel(agent) {
340169
340297
  else agent.pendingOptions = agent.pendingOptions.filter((question) => !isPlanExecutionQuestion(question));
340170
340298
  }
340171
340299
  } finally {
340300
+ detachProcessAbort();
340172
340301
  agent.attachAgentKernelRuntime(null);
340173
340302
  }
340174
340303
  agent.status = "idle";
@@ -340335,20 +340464,18 @@ async function transformContext(agent, messages, signal) {
340335
340464
  const provider = agent.engineModel();
340336
340465
  if (!provider || !compressionModel) return messages;
340337
340466
  const newmarkMessages = publicHistoryFromKernelMessages(messages);
340338
- const beforeCompression = JSON.stringify(newmarkMessages);
340339
340467
  const compressionAt = agent.lastCompression?.at || "";
340340
- await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
340468
+ let compressed = await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
340341
340469
  if (processSignal?.aborted) return messages;
340342
- const primaryCompressed = JSON.stringify(newmarkMessages) !== beforeCompression;
340343
- if (primaryCompressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
340344
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
340470
+ if (compressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
340471
+ compressed = await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true) || compressed;
340345
340472
  }
340346
340473
  const windowMax = agent.contextWindow(compressionModel).maxTokens;
340347
340474
  const conservativeTokens = agent.estimateContextTokens(newmarkMessages);
340348
340475
  if (conservativeTokens >= Math.floor(windowMax * 0.9) && !processSignal?.aborted) {
340349
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
340476
+ compressed = await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true) || compressed;
340350
340477
  }
340351
- if (JSON.stringify(newmarkMessages) === beforeCompression) return messages;
340478
+ if (!compressed) return messages;
340352
340479
  const durableMessages = toKernelMessagesFromHistory(newmarkMessages, agent);
340353
340480
  if (agent.lastCompression?.at && agent.lastCompression.at !== compressionAt) {
340354
340481
  agent.recordContextCompressionStep();
@@ -342239,9 +342366,11 @@ function key2(model) {
342239
342366
  }
342240
342367
  var FileModelValidationCache = class {
342241
342368
  filePath;
342369
+ readOnly;
342242
342370
  records = /* @__PURE__ */ new Map();
342243
- constructor(rootPath) {
342371
+ constructor(rootPath, options = {}) {
342244
342372
  this.filePath = path20.join(rootPath, "model-validation", "records.json");
342373
+ this.readOnly = options.readOnly === true;
342245
342374
  this.load();
342246
342375
  }
342247
342376
  get(modelKey2) {
@@ -342250,10 +342379,12 @@ var FileModelValidationCache = class {
342250
342379
  }
342251
342380
  set(record) {
342252
342381
  this.records.set(record.modelKey || key2(record.model), JSON.parse(JSON.stringify(record)));
342382
+ if (this.readOnly) return;
342253
342383
  this.save();
342254
342384
  }
342255
342385
  delete(modelKey2) {
342256
342386
  if (!this.records.delete(modelKey2)) return;
342387
+ if (this.readOnly) return;
342257
342388
  this.save();
342258
342389
  }
342259
342390
  load() {
@@ -344129,7 +344260,7 @@ var Agent4 = class _Agent {
344129
344260
  this.subagentName = options.subagentName || "";
344130
344261
  this.subagentPrompt = options.subagentPrompt || "";
344131
344262
  this.linkedPlanAccess = options.linkedPlanAccess;
344132
- this.config = new ConfigManager(rootPath);
344263
+ this.config = new ConfigManager(rootPath, { readOnly: options.readOnlyConfig === true });
344133
344264
  this.compressionHistoryArchive = new CompressionHistoryArchive(rootPath);
344134
344265
  this.contextV2 = new AgentContextManager(rootPath, this.config);
344135
344266
  this.agentRunService = this.config.contextFlag("agent_runtime_v2") ? new AgentRunService(path28.join(rootPath, ".newmark-context-v2")) : null;
@@ -344660,7 +344791,10 @@ var Agent4 = class _Agent {
344660
344791
  }
344661
344792
  const previousAuto = this.model === "auto" ? this.resolvedDeployment : null;
344662
344793
  const qualified = parseDeploymentSelectionValue2(requested);
344663
- const current = qualified ? this.config.findDeployment(qualified) : requested ? this.config.findModel(requested) : void 0;
344794
+ const legacyQualified = requested.includes("/") ? this.config.allModels().filter(
344795
+ (model2) => `${model2.provider_id}/${model2.name}` === requested || `${model2.provider}/${model2.name}` === requested
344796
+ ) : [];
344797
+ const current = qualified ? this.config.findDeployment(qualified) : legacyQualified.length === 1 ? legacyQualified[0] : requested ? this.config.findModel(requested) : void 0;
344664
344798
  this.model = current?.name || requested;
344665
344799
  this.fixedDeployment = current ? this.deploymentRef(current) : qualified;
344666
344800
  this.resolvedDeployment = null;
@@ -345777,7 +345911,7 @@ ${String(event.toolArgs || "")}`;
345777
345911
  this.saveWorkspaceConversationState(true);
345778
345912
  return true;
345779
345913
  }
345780
- finishConversationWorkRun(runId, status, endedAt = this.nowIso()) {
345914
+ finishConversationWorkRun(runId, status, endedAt = this.nowIso(), errorMessage = "") {
345781
345915
  const run = this.workRuns.find((item) => item.runId === String(runId || ""));
345782
345916
  if (!run) return false;
345783
345917
  this.syncAgentRunTerminal(run.runId, status, endedAt);
@@ -345821,7 +345955,7 @@ ${String(event.toolArgs || "")}`;
345821
345955
  this.enforceGoalTerminalInvariant(status, goalAudit);
345822
345956
  this.emitWorkEvent({
345823
345957
  type: status === "completed" ? "done" : status === "error" ? "error" : "status",
345824
- content: status === "force_interrupted" ? "Force interrupted." : status === "interrupted" ? "Interrupted." : "Response complete.",
345958
+ content: status === "error" ? String(errorMessage || "").trim() || "Agent run failed." : status === "force_interrupted" ? "Force interrupted." : status === "interrupted" ? "Interrupted." : "Response complete.",
345825
345959
  status,
345826
345960
  runId: run.runId,
345827
345961
  conversationId: run.target.conversationId,
@@ -345966,6 +346100,7 @@ ${String(event.toolArgs || "")}`;
345966
346100
  this.activeAgentKernelRuntime = runtime;
345967
346101
  this.awaitingAgentKernelRuntime = false;
345968
346102
  if (!runtime) return;
346103
+ if (this.activeProcessAbortController?.signal.aborted) runtime.abort?.();
345969
346104
  const queued = this.pendingAgentKernelQueue.splice(0);
345970
346105
  for (const item of queued) {
345971
346106
  const accepted = this.forwardAgentKernelQueueMessage(item.content, item.queueMode, item.clientMessageId, item.runId, item.images, item.hiddenUserInput);
@@ -347127,6 +347262,25 @@ Review this persisted peer result and summarize or continue the parent task as n
347127
347262
  this.loadWorkspaceConversationState();
347128
347263
  return selected;
347129
347264
  }
347265
+ refreshWorkspaceRegistryFromStorage() {
347266
+ const before = JSON.stringify({
347267
+ internal: this.workspace.internal,
347268
+ external: this.workspace.external,
347269
+ current: this.workspace.current
347270
+ });
347271
+ const selected = this.workspace.reloadFromStorage();
347272
+ const after = JSON.stringify({
347273
+ internal: this.workspace.internal,
347274
+ external: this.workspace.external,
347275
+ current: this.workspace.current
347276
+ });
347277
+ if (before === after) return selected;
347278
+ if (selected) this.config.loadWorkspaceConfig(selected.path);
347279
+ else this.config.clearWorkspaceOverrides();
347280
+ this.workspaceConversations.clear();
347281
+ this.loadWorkspaceConversationState();
347282
+ return selected;
347283
+ }
347130
347284
  setConversation(id) {
347131
347285
  const clean = this.safeConversationId(id || "default");
347132
347286
  if (this.workspaceConversationKey() === this.loadedWorkspaceConversationKey) {
@@ -348120,28 +348274,25 @@ ${summary}`, segment, "local-summarize", true);
348120
348274
  this.saveWorkspaceConversationState(true);
348121
348275
  return { text, hiddenUserInput: true, goalContinuation: true };
348122
348276
  }
348123
- writeSessionArchive(messages, mode, model) {
348277
+ buildSessionArchive(messages, mode, model, archiveDir) {
348124
348278
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "_").replace("Z", "");
348125
- const archiveDir = this.archiveDir();
348126
- fs25.mkdirSync(archiveDir, { recursive: true });
348127
- const filename = `session_${stamp}.md`;
348128
- const outPath = path28.join(archiveDir, filename);
348129
- let md = `# Newmark Session \u2014 ${stamp}
348279
+ const filename = `session_${stamp}_${crypto14.randomUUID().slice(0, 8)}.md`;
348280
+ let markdown = `# Newmark Session \u2014 ${stamp}
348130
348281
 
348131
348282
  `;
348132
- md += `**Mode**: ${mode}
348283
+ markdown += `**Mode**: ${mode}
348133
348284
  **Model**: ${model}
348134
348285
  `;
348135
- md += `**Messages**: ${messages.length}
348286
+ markdown += `**Messages**: ${messages.length}
348136
348287
 
348137
348288
  ---
348138
348289
 
348139
348290
  `;
348140
- if (this.goal) md += `**Goal**: ${this.goal.objective}
348291
+ if (this.goal) markdown += `**Goal**: ${this.goal.objective}
348141
348292
 
348142
348293
  `;
348143
348294
  for (const msg of messages) {
348144
- md += `**[${msg.role}] ${msg.timestamp}**
348295
+ markdown += `**[${msg.role}] ${msg.timestamp}**
348145
348296
 
348146
348297
  ${msg.content}
348147
348298
 
@@ -348150,13 +348301,35 @@ ${msg.content}
348150
348301
  const archived = archiveConversationImageAttachment(this.rootPath, archiveDir, attachment);
348151
348302
  if (!archived) continue;
348152
348303
  const alt = archived.name.replace(/[\]\r\n]/g, " ").trim() || "Submitted image";
348153
- md += `![${alt}](${archived.relativePath})
348304
+ markdown += `![${alt}](${archived.relativePath})
348154
348305
 
348155
348306
  `;
348156
348307
  }
348157
348308
  }
348158
- fs25.writeFileSync(outPath, md, "utf-8");
348159
- return filename;
348309
+ return { filename, markdown };
348310
+ }
348311
+ writeSessionArchive(messages, mode, model) {
348312
+ const archiveDir = this.archiveDir();
348313
+ fs25.mkdirSync(archiveDir, { recursive: true });
348314
+ const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
348315
+ fs25.writeFileSync(path28.join(archiveDir, archive.filename), archive.markdown, "utf-8");
348316
+ return archive.filename;
348317
+ }
348318
+ async writeSessionArchiveAsync(messages, mode, model, archiveDir = this.archiveDir()) {
348319
+ const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
348320
+ await fs25.promises.mkdir(archiveDir, { recursive: true });
348321
+ const outPath = path28.join(archiveDir, archive.filename);
348322
+ const tempPath = `${outPath}.${process.pid}.${crypto14.randomUUID()}.tmp`;
348323
+ try {
348324
+ await fs25.promises.writeFile(tempPath, archive.markdown, "utf-8");
348325
+ await fs25.promises.rename(tempPath, outPath);
348326
+ } finally {
348327
+ try {
348328
+ await fs25.promises.unlink(tempPath);
348329
+ } catch {
348330
+ }
348331
+ }
348332
+ return archive.filename;
348160
348333
  }
348161
348334
  archiveSession() {
348162
348335
  return this.writeSessionArchive(this.chatMessages, this.modeName(), this.model);
@@ -348224,6 +348397,93 @@ ${msg.content}
348224
348397
  }
348225
348398
  return filename;
348226
348399
  }
348400
+ /**
348401
+ * Non-blocking archive writer used by the desktop IPC path. The conversation
348402
+ * state merge remains synchronous and lock-protected, but the potentially
348403
+ * large markdown payload and manifest use promise-based filesystem I/O so
348404
+ * independent workspaces can archive in parallel without freezing Electron.
348405
+ */
348406
+ async archiveConversationAsync(conversationId) {
348407
+ return await this.archiveConversationAsyncUnlocked(conversationId);
348408
+ }
348409
+ async archiveConversationAsyncUnlocked(conversationId) {
348410
+ const ws = this.workspace.current;
348411
+ if (!ws) return null;
348412
+ const clean = this.safeConversationId(conversationId || "default");
348413
+ const stateKey2 = this.workspaceConversationStateKey(clean);
348414
+ if (!stateKey2) return null;
348415
+ const memoryKey = `${ws.isInternal ? "internal" : "external"}:${path28.resolve(ws.path)}::conversation:${clean}`;
348416
+ const archiveDir = path28.join(ws.path, "archive");
348417
+ const workspacePrefix = this.workspaceConversationPrefix() || "";
348418
+ const archiveMode = this.modeName();
348419
+ const archiveModel = this.model;
348420
+ const cachedStored = this.readStoredConversationState(ws);
348421
+ const stored = JSON.parse(JSON.stringify(cachedStored || {}));
348422
+ const persisted = stored.conversations?.[stateKey2];
348423
+ if (persisted) this.normalizeConversationTree(persisted);
348424
+ const memory = this.workspaceConversations.get(memoryKey);
348425
+ const persistedMessagesAvailable = persisted?.chatMessages !== void 0;
348426
+ const sourceMessages = persisted?.chatMessages ?? memory?.chatMessages ?? [];
348427
+ const sourceHistory = persistedMessagesAvailable ? persisted?.history ?? [] : memory?.history ?? persisted?.history ?? [];
348428
+ const messages = this.normalizeConversationChatMessages(sourceMessages, sourceHistory);
348429
+ const filename = await this.writeSessionArchiveAsync(messages, archiveMode, archiveModel, archiveDir);
348430
+ const archiveEntry = persisted ? JSON.parse(JSON.stringify(persisted)) : {
348431
+ title: this.titleFromMessages(messages, clean),
348432
+ chatMessages: messages,
348433
+ history: sourceHistory,
348434
+ plan: memory?.plan,
348435
+ linkedPlan: memory?.linkedPlan,
348436
+ subagentState: memory?.subagentState,
348437
+ workRuns: memory?.workRuns,
348438
+ continuations: memory?.continuations,
348439
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
348440
+ };
348441
+ const manifest = {
348442
+ version: 2,
348443
+ kind: "newmark-conversation-archive",
348444
+ archivedAt: (/* @__PURE__ */ new Date()).toISOString(),
348445
+ conversationId: clean,
348446
+ workspaceId: ws.id,
348447
+ workspaceName: ws.name,
348448
+ workspacePath: ws.path,
348449
+ workspaceInternal: ws.isInternal,
348450
+ statePrefix: workspacePrefix,
348451
+ entry: this.conversationEntryForDisk(archiveEntry)
348452
+ };
348453
+ const manifestPath = this.archiveManifestPath(path28.join(archiveDir, filename));
348454
+ const manifestTempPath = `${manifestPath}.${process.pid}.${crypto14.randomUUID()}.tmp`;
348455
+ try {
348456
+ await fs25.promises.writeFile(manifestTempPath, JSON.stringify(manifest, null, 2), "utf-8");
348457
+ await fs25.promises.rename(manifestTempPath, manifestPath);
348458
+ } finally {
348459
+ try {
348460
+ await fs25.promises.unlink(manifestTempPath);
348461
+ } catch {
348462
+ }
348463
+ }
348464
+ this.finalizeAsyncConversationArchive(clean, stateKey2, memoryKey, ws);
348465
+ return filename;
348466
+ }
348467
+ finalizeAsyncConversationArchive(clean, stateKey2, memoryKey, ws) {
348468
+ let nextActiveId = "";
348469
+ this.mutateStoredConversationState(ws, (latest) => {
348470
+ latest.conversations = latest.conversations || {};
348471
+ delete latest.conversations[stateKey2];
348472
+ const prefix = stateKey2.slice(0, Math.max(0, stateKey2.length - clean.length - 1)) + "-";
348473
+ const remaining = Object.keys(latest.conversations).filter((key3) => !prefix || key3.startsWith(prefix)).map((key3) => key3.slice(prefix.length)).filter(Boolean);
348474
+ const currentActiveId = this.safeConversationId(latest.activeConversationId || this.activeConversationId || "default");
348475
+ if (clean === currentActiveId) latest.activeConversationId = remaining[0] || "default";
348476
+ nextActiveId = latest.activeConversationId || remaining[0] || "default";
348477
+ return latest;
348478
+ });
348479
+ this.workspaceConversations.delete(memoryKey);
348480
+ const duplicateMemoryKey = `${ws.isInternal ? "internal" : "external"}:${path28.resolve(ws.path)}::conversation:${clean}`;
348481
+ this.workspaceConversations.delete(duplicateMemoryKey);
348482
+ if (clean === this.safeConversationId(this.activeConversationId || "default")) {
348483
+ this.activeConversationId = nextActiveId || "default";
348484
+ this.loadWorkspaceConversationState();
348485
+ }
348486
+ }
348227
348487
  listStoredConversationIds(stored) {
348228
348488
  const prefix = `${this.workspaceConversationPrefix() || ""}-`;
348229
348489
  return Object.keys(stored.conversations || {}).filter((key3) => !prefix || key3.startsWith(prefix)).map((key3) => key3.slice(prefix.length)).filter(Boolean);
@@ -348749,9 +349009,9 @@ ${msg.content}
348749
349009
  const provider = this.config.findProvider(providerId);
348750
349010
  return all.filter((m2) => m2.provider_id === (provider?.id || providerId));
348751
349011
  }
348752
- async validateModels(selectedNames) {
349012
+ async validateModels(selectedNames, options = {}) {
348753
349013
  if (this.modelValidationPromise) return this.modelValidationPromise;
348754
- const validation = this.runModelValidation(selectedNames);
349014
+ const validation = this.runModelValidation(selectedNames, options.persist !== false);
348755
349015
  this.modelValidationPromise = validation;
348756
349016
  try {
348757
349017
  return await validation;
@@ -348769,7 +349029,7 @@ ${msg.content}
348769
349029
  recentChecks: this.modelValidationProgress.recentChecks.map((item) => ({ ...item }))
348770
349030
  };
348771
349031
  }
348772
- async runModelValidation(selectedNames) {
349032
+ async runModelValidation(selectedNames, persist = true) {
348773
349033
  const selectedModels = this.config.modelsForSelections(selectedNames);
348774
349034
  if (!selectedModels.length) {
348775
349035
  this.modelValidationProgress = {
@@ -348787,7 +349047,7 @@ ${msg.content}
348787
349047
  }
348788
349048
  const results = [];
348789
349049
  const catalogByProvider = /* @__PURE__ */ new Map();
348790
- const cache = new FileModelValidationCache(this.rootPath);
349050
+ const cache = new FileModelValidationCache(this.rootPath, { readOnly: !persist });
348791
349051
  const checksPerModel = 11;
348792
349052
  let currentModel = "";
348793
349053
  let currentModelChecks = 0;
@@ -348919,7 +349179,7 @@ ${msg.content}
348919
349179
  completedModels: this.modelValidationProgress.completedModels + 1
348920
349180
  };
348921
349181
  }
348922
- this.config.save();
349182
+ if (persist) this.config.save();
348923
349183
  this.modelValidationProgress = {
348924
349184
  ...this.modelValidationProgress,
348925
349185
  running: false,
@@ -349171,7 +349431,8 @@ ${String(input.content || "").slice(0, 18e3)}`;
349171
349431
  const text = typeof input === "string" ? input : String(input.text || "");
349172
349432
  const inputEnvelope = typeof input === "string" ? null : input;
349173
349433
  const hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
349174
- this.ensureUsableModelSelection();
349434
+ const explicitFixedModel = this.model !== "" && this.model !== "auto";
349435
+ if (!explicitFixedModel) this.ensureUsableModelSelection();
349175
349436
  const clientMessageId = String(inputEnvelope?.clientMessageId || "").trim();
349176
349437
  const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || "").trim();
349177
349438
  const rawImages = typeof input === "string" ? [] : Array.isArray(input.images) ? input.images : [];
@@ -349259,7 +349520,13 @@ ${String(input.content || "").slice(0, 18e3)}`;
349259
349520
  await this.evaluateAndSwitch(displayText, inputEnvelope?.routePolicy);
349260
349521
  }
349261
349522
  if (this.model && this.modelIsUnavailable(this.model)) {
349523
+ const requestedModel = this.model;
349262
349524
  this.switchToFallbackModel();
349525
+ if (this.modelIsUnavailable(this.model)) {
349526
+ const message = `[Error] Model '${requestedModel || "unknown"}' is unavailable or not configured. Select a configured model or enable a valid provider before sending.`;
349527
+ this.status = "error";
349528
+ throw new Error(message);
349529
+ }
349263
349530
  }
349264
349531
  if (this.engine === "opencode") {
349265
349532
  if (images.length) return [{ type: "text", text: "[Vision unavailable] The OpenCode engine does not accept Newmark image attachments." }];
@@ -350252,11 +350519,11 @@ Falling back to built-in engine.` }];
350252
350519
  }
350253
350520
  }
350254
350521
  async maybeCompress(msgs, provider, signal, compressionModel, force = false) {
350255
- if (signal?.aborted) return;
350256
- if (!this.config.getBool("context", "auto_compress")) return;
350522
+ if (signal?.aborted) return false;
350523
+ if (!this.config.getBool("context", "auto_compress")) return false;
350257
350524
  const total = msgs.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content || "").length), 0);
350258
350525
  const budget = this.compressionBudget(msgs);
350259
- if (budget.estimatedTokens < budget.triggerTokens && !force) return;
350526
+ if (budget.estimatedTokens < budget.triggerTokens && !force) return false;
350260
350527
  if (!force && this.lastCompression && String(msgs[0]?.content || "").includes(this.lastCompression.summary)) {
350261
350528
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
350262
350529
  const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
@@ -350264,16 +350531,16 @@ Falling back to built-in engine.` }];
350264
350531
  const tokenGrowth = baselineTokens ? Math.max(0, budget.estimatedTokens - baselineTokens) : Number.POSITIVE_INFINITY;
350265
350532
  const minCharGrowth = Math.max(12e3, Math.floor(baselineChars * 0.25));
350266
350533
  const minTokenGrowth = Math.max(1024, Math.floor(budget.triggerTokens * 0.2));
350267
- if (charGrowth < minCharGrowth && tokenGrowth < minTokenGrowth) return;
350534
+ if (charGrowth < minCharGrowth && tokenGrowth < minTokenGrowth) return false;
350268
350535
  }
350269
350536
  const originalMessageCount = msgs.length;
350270
350537
  const configuredKeepLast = this.config.getNum("context", "keep_recent_messages") || 10;
350271
- if (msgs.length <= 1) return;
350538
+ if (msgs.length <= 1) return false;
350272
350539
  const continuationAnchorTokens = this.estimateContextTokens([this.postCompressionContinuationMessage()]);
350273
350540
  const recentBudget = Math.max(64, budget.targetTokens - budget.summaryTokens - continuationAnchorTokens);
350274
350541
  const recent = this.recentContextSuffix(msgs, configuredKeepLast, recentBudget);
350275
350542
  const recentStart = Math.max(0, msgs.length - recent.length);
350276
- if (recentStart <= 0) return;
350543
+ if (recentStart <= 0) return false;
350277
350544
  const middle = msgs.slice(0, recentStart);
350278
350545
  const currentInstruction = this.latestUserHistoryText(recent);
350279
350546
  const compression = await this.buildCompressionSummary(
@@ -350285,7 +350552,7 @@ Falling back to built-in engine.` }];
350285
350552
  compressionModel || this.activeModelName(),
350286
350553
  currentInstruction
350287
350554
  );
350288
- if (signal?.aborted) return;
350555
+ if (signal?.aborted) return false;
350289
350556
  const compressed = [{
350290
350557
  role: "system",
350291
350558
  content: compression.summary
@@ -350310,6 +350577,7 @@ Falling back to built-in engine.` }];
350310
350577
  };
350311
350578
  this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
350312
350579
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
350580
+ return true;
350313
350581
  }
350314
350582
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
350315
350583
  const workspacePath = this.workspace.current?.path || this.rootPath;
@@ -351645,7 +351913,12 @@ var ConversationKernel = class {
351645
351913
  if (runtime.runId === runId && runtime.stopRequestedRunId === runId) {
351646
351914
  stopped = true;
351647
351915
  } else {
351648
- runtime.runner.finishConversationWorkRun(runId, "error");
351916
+ runtime.runner.finishConversationWorkRun(
351917
+ runId,
351918
+ "error",
351919
+ void 0,
351920
+ error instanceof Error ? error.message : String(error)
351921
+ );
351649
351922
  throw error;
351650
351923
  }
351651
351924
  } finally {