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;
@@ -327623,8 +327628,10 @@ var ConfigManager = class {
327623
327628
  rootPath;
327624
327629
  config;
327625
327630
  workspaceOverrides;
327626
- constructor(rootPath) {
327631
+ readOnly;
327632
+ constructor(rootPath, options = {}) {
327627
327633
  this.rootPath = rootPath;
327634
+ this.readOnly = options.readOnly === true;
327628
327635
  this.workspaceOverrides = /* @__PURE__ */ new Map();
327629
327636
  this.config = this.load();
327630
327637
  }
@@ -327639,17 +327646,19 @@ var ConfigManager = class {
327639
327646
  const raw = JSON.parse(readJsonText(cp));
327640
327647
  const normalized = normalizeConfigShape(raw, true);
327641
327648
  if (isCorruptConfig(raw, normalized)) {
327649
+ if (this.readOnly) return defaultConfig();
327642
327650
  this.backupConfig(cp, "invalid-shape");
327643
327651
  return this.writeRecoveredConfig(cp);
327644
327652
  }
327645
327653
  if (migrateProviderIdsInConfig(normalized)) {
327646
327654
  try {
327647
- fs3.writeFileSync(cp, JSON.stringify(normalized, null, 2), "utf-8");
327655
+ if (!this.readOnly) fs3.writeFileSync(cp, JSON.stringify(normalized, null, 2), "utf-8");
327648
327656
  } catch {
327649
327657
  }
327650
327658
  }
327651
327659
  return normalized;
327652
327660
  } catch {
327661
+ if (this.readOnly) return defaultConfig();
327653
327662
  this.backupConfig(cp, "invalid-json");
327654
327663
  return this.writeRecoveredConfig(cp);
327655
327664
  }
@@ -327695,10 +327704,12 @@ var ConfigManager = class {
327695
327704
  this.config[section][key3] = { value: normalizedValue };
327696
327705
  }
327697
327706
  save() {
327707
+ if (this.readOnly) return;
327698
327708
  const j2 = JSON.stringify(this.config, null, 2);
327699
327709
  fs3.writeFileSync(path3.join(this.rootPath, "config.json"), j2, "utf-8");
327700
327710
  }
327701
327711
  saveTo(targetPath) {
327712
+ if (this.readOnly) return;
327702
327713
  const j2 = JSON.stringify(this.config, null, 2);
327703
327714
  fs3.writeFileSync(targetPath, j2, "utf-8");
327704
327715
  }
@@ -327923,12 +327934,14 @@ var ConfigManager = class {
327923
327934
  return providers;
327924
327935
  }
327925
327936
  writeRecoveredConfig(configPath) {
327937
+ if (this.readOnly) return defaultConfig();
327926
327938
  const config = loadExampleConfig();
327927
327939
  fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
327928
327940
  fs3.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
327929
327941
  return config;
327930
327942
  }
327931
327943
  backupConfig(configPath, reason) {
327944
+ if (this.readOnly) return;
327932
327945
  try {
327933
327946
  if (!fs3.existsSync(configPath)) return;
327934
327947
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -328273,7 +328286,10 @@ function defaultConfig() {
328273
328286
  general: {
328274
328287
  tone: { _description: "Conversation style", _type: "choice", _values: ["strict_simple", "casual_friendly"], value: "strict_simple" },
328275
328288
  language: { _description: "Default language", _type: "choice", _values: ["en", "zh", "auto"], value: "auto" },
328276
- close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "minimize" },
328289
+ // A first-run desktop window must have a deterministic close/exit
328290
+ // contract. Users who explicitly choose minimize-to-tray keep that
328291
+ // choice, but a fresh install must not hide the process on OS close.
328292
+ close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "exit" },
328277
328293
  default_input: { _description: "Default input mode", _type: "choice", _values: ["guide", "next"], value: "guide" },
328278
328294
  auto_archive_on_close: { _description: "Auto archive on close", _type: "boolean", value: true }
328279
328295
  },
@@ -328612,6 +328628,38 @@ function providerAbortError(signal) {
328612
328628
  if (!error.name || error.name === "Error") error.name = "AbortError";
328613
328629
  return error;
328614
328630
  }
328631
+ function providerStreamTimeoutError(timeoutMs) {
328632
+ const error = new Error("Stream read timeout");
328633
+ error.name = "TimeoutError";
328634
+ error.message = `Stream read timeout after ${timeoutMs}ms`;
328635
+ return error;
328636
+ }
328637
+ async function readProviderStreamChunk(reader, signal, timeoutMs = 3e4) {
328638
+ if (signal.aborted) throw providerAbortError(signal);
328639
+ let timer;
328640
+ let onAbort;
328641
+ const abortPromise = new Promise((_3, reject) => {
328642
+ onAbort = () => reject(providerAbortError(signal));
328643
+ signal.addEventListener("abort", onAbort, { once: true });
328644
+ });
328645
+ const timeoutPromise = new Promise((_3, reject) => {
328646
+ timer = setTimeout(() => reject(providerStreamTimeoutError(timeoutMs)), timeoutMs);
328647
+ });
328648
+ try {
328649
+ return await Promise.race([reader.read(), abortPromise, timeoutPromise]);
328650
+ } catch (error) {
328651
+ if (signal.aborted || error instanceof Error && error.name === "TimeoutError") {
328652
+ try {
328653
+ await reader.cancel(error);
328654
+ } catch {
328655
+ }
328656
+ }
328657
+ throw error;
328658
+ } finally {
328659
+ if (timer) clearTimeout(timer);
328660
+ if (onAbort) signal.removeEventListener("abort", onAbort);
328661
+ }
328662
+ }
328615
328663
  function parseProviderSse(raw) {
328616
328664
  const events = [];
328617
328665
  for (const block of String(raw || "").replace(/\r\n/g, "\n").split(/\n\n+/)) {
@@ -328847,12 +328895,7 @@ var ChatCompletionsAdapter = class {
328847
328895
  let emittedTool = false;
328848
328896
  try {
328849
328897
  while (true) {
328850
- if (signal.aborted) throw providerAbortError(signal);
328851
- const readPromise = reader.read();
328852
- const timeoutPromise = new Promise(
328853
- (_3, reject) => setTimeout(() => reject(new Error("Stream read timeout")), 3e4)
328854
- );
328855
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
328898
+ const { done, value } = await readProviderStreamChunk(reader, signal);
328856
328899
  if (done) break;
328857
328900
  buffer += decoder.decode(value, { stream: true });
328858
328901
  const lines = buffer.split("\n");
@@ -329090,8 +329133,7 @@ var ResponsesAdapter = class {
329090
329133
  let streamError = "";
329091
329134
  try {
329092
329135
  while (true) {
329093
- if (signal.aborted) throw providerAbortError(signal);
329094
- const { done, value } = await reader.read();
329136
+ const { done, value } = await readProviderStreamChunk(reader, signal);
329095
329137
  if (done) break;
329096
329138
  buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
329097
329139
  const blocks = buffer.split(/\n\n+/);
@@ -329301,6 +329343,16 @@ function createProviderAdapter(providerId, apiMode) {
329301
329343
  }
329302
329344
 
329303
329345
  // src/llm/provider.ts
329346
+ var DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 9e4;
329347
+ var MIN_PROVIDER_REQUEST_TIMEOUT_MS = 50;
329348
+ function providerTimeoutError(timeoutMs) {
329349
+ const error = new Error(`Provider request timed out after ${timeoutMs}ms`);
329350
+ error.name = "TimeoutError";
329351
+ return error;
329352
+ }
329353
+ function isProviderTimeoutError(error) {
329354
+ return error instanceof Error && error.name === "TimeoutError";
329355
+ }
329304
329356
  function abortFailure(signal) {
329305
329357
  const reason = signal?.reason;
329306
329358
  const error = reason instanceof Error ? reason : new Error(reason ? String(reason) : "LLM request aborted");
@@ -329330,13 +329382,14 @@ function parseProviderSse2(raw) {
329330
329382
  return events;
329331
329383
  }
329332
329384
  var LLMProvider = class _LLMProvider {
329333
- constructor(name50, baseUrl, apiKey, explicitProtocol, openAIMode = "chat_stream", useProviderAdaptersV2 = false) {
329385
+ constructor(name50, baseUrl, apiKey, explicitProtocol, openAIMode = "chat_stream", useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329334
329386
  this.name = name50;
329335
329387
  this.baseUrl = baseUrl;
329336
329388
  this.apiKey = apiKey;
329337
329389
  this.explicitProtocol = explicitProtocol;
329338
329390
  this.openAIMode = openAIMode;
329339
329391
  this.useProviderAdaptersV2 = useProviderAdaptersV2;
329392
+ this.requestTimeoutMs = requestTimeoutMs;
329340
329393
  }
329341
329394
  name;
329342
329395
  baseUrl;
@@ -329344,8 +329397,25 @@ var LLMProvider = class _LLMProvider {
329344
329397
  explicitProtocol;
329345
329398
  openAIMode;
329346
329399
  useProviderAdaptersV2;
329400
+ requestTimeoutMs;
329347
329401
  static nodeHttpTransport = null;
329348
329402
  static powershellTransport = null;
329403
+ effectiveRequestTimeout(timeoutMs) {
329404
+ const requested = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
329405
+ const configured = Number.isFinite(this.requestTimeoutMs) && this.requestTimeoutMs > 0 ? this.requestTimeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
329406
+ return Math.max(MIN_PROVIDER_REQUEST_TIMEOUT_MS, Math.min(requested, configured));
329407
+ }
329408
+ async withRequestTimeout(promise, timeoutMs, signal) {
329409
+ let timer;
329410
+ const timeoutPromise = new Promise((_3, reject) => {
329411
+ timer = setTimeout(() => reject(providerTimeoutError(timeoutMs)), timeoutMs);
329412
+ });
329413
+ try {
329414
+ return await abortable(Promise.race([promise, timeoutPromise]), signal);
329415
+ } finally {
329416
+ if (timer) clearTimeout(timer);
329417
+ }
329418
+ }
329349
329419
  intelligenceConfig(tier) {
329350
329420
  switch (tier) {
329351
329421
  case "low":
@@ -329447,6 +329517,7 @@ var LLMProvider = class _LLMProvider {
329447
329517
  };
329448
329518
  }
329449
329519
  async postJsonWithFetchFallback(url, headers, body, timeoutMs = 12e4, signal) {
329520
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329450
329521
  if (this.isPlainHttpLoopback(url)) {
329451
329522
  const pathname = (() => {
329452
329523
  try {
@@ -329456,7 +329527,7 @@ var LLMProvider = class _LLMProvider {
329456
329527
  }
329457
329528
  })();
329458
329529
  this.transportDiagnostic("loopback:start", pathname);
329459
- const local = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal);
329530
+ const local = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal, effectiveTimeout);
329460
329531
  this.transportDiagnostic("loopback:complete", `status=${local.status} bytes=${Buffer.byteLength(local.body || "")}`);
329461
329532
  return {
329462
329533
  ok: local.status >= 200 && local.status < 300,
@@ -329470,7 +329541,7 @@ var LLMProvider = class _LLMProvider {
329470
329541
  const forwardAbort = () => abort.abort(signal?.reason);
329471
329542
  if (signal?.aborted) forwardAbort();
329472
329543
  else signal?.addEventListener("abort", forwardAbort, { once: true });
329473
- const timer = setTimeout(() => abort.abort(), timeoutMs);
329544
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
329474
329545
  try {
329475
329546
  const response = await fetch(url, {
329476
329547
  method: "POST",
@@ -329481,8 +329552,9 @@ var LLMProvider = class _LLMProvider {
329481
329552
  return response;
329482
329553
  } catch (e3) {
329483
329554
  if (signal?.aborted) throw abortFailure(signal);
329555
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
329484
329556
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
329485
- const fallback = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal);
329557
+ const fallback = await this.nodeHttpJson("POST", url, headers, JSON.stringify(body), signal, effectiveTimeout);
329486
329558
  return {
329487
329559
  ok: fallback.status >= 200 && fallback.status < 300,
329488
329560
  status: fallback.status,
@@ -329496,14 +329568,16 @@ var LLMProvider = class _LLMProvider {
329496
329568
  }
329497
329569
  }
329498
329570
  async getJsonWithFetchFallback(url, headers, timeoutMs = 3e4) {
329571
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329499
329572
  const abort = new AbortController();
329500
- const timer = setTimeout(() => abort.abort(), timeoutMs);
329573
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
329501
329574
  try {
329502
329575
  const response = await fetch(url, { method: "GET", headers, signal: abort.signal });
329503
329576
  return response;
329504
329577
  } catch (e3) {
329578
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
329505
329579
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
329506
- const fallback = await this.nodeHttpJson("GET", url, headers);
329580
+ const fallback = await this.nodeHttpJson("GET", url, headers, "", void 0, effectiveTimeout);
329507
329581
  return {
329508
329582
  ok: fallback.status >= 200 && fallback.status < 300,
329509
329583
  status: fallback.status,
@@ -329516,14 +329590,16 @@ var LLMProvider = class _LLMProvider {
329516
329590
  }
329517
329591
  }
329518
329592
  shouldUseNodeHttpFallback(error) {
329519
- return error instanceof TypeError && /fetch failed/i.test(error.message) || error instanceof Error && /abort/i.test(error.name || error.message);
329593
+ return error instanceof TypeError && /fetch failed/i.test(error.message);
329520
329594
  }
329521
- nodeHttpJson(method, urlValue, headers, body = "", signal) {
329595
+ nodeHttpJson(method, urlValue, headers, body = "", signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329596
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329522
329597
  if (_LLMProvider.nodeHttpTransport) {
329523
- return abortable(_LLMProvider.nodeHttpTransport(method, urlValue, headers, body), signal).catch((error) => {
329598
+ return this.withRequestTimeout(_LLMProvider.nodeHttpTransport(method, urlValue, headers, body), effectiveTimeout, signal).catch((error) => {
329524
329599
  if (signal?.aborted) throw abortFailure(signal);
329600
+ if (isProviderTimeoutError(error)) throw error;
329525
329601
  if (process.platform === "win32") {
329526
- return this.powershellJson(method, urlValue, headers, body, signal);
329602
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
329527
329603
  }
329528
329604
  throw error;
329529
329605
  });
@@ -329568,8 +329644,8 @@ var LLMProvider = class _LLMProvider {
329568
329644
  else fail(new Error("Node HTTP response closed before completion"));
329569
329645
  });
329570
329646
  });
329571
- req.setTimeout(12e4, () => {
329572
- req.destroy(new Error("Node HTTP fallback timeout"));
329647
+ req.setTimeout(effectiveTimeout, () => {
329648
+ req.destroy(providerTimeoutError(effectiveTimeout));
329573
329649
  });
329574
329650
  req.on("error", reject);
329575
329651
  const onAbort = () => req.destroy(abortFailure(signal));
@@ -329580,15 +329656,17 @@ var LLMProvider = class _LLMProvider {
329580
329656
  req.end();
329581
329657
  }).catch((error) => {
329582
329658
  if (signal?.aborted) throw abortFailure(signal);
329659
+ if (isProviderTimeoutError(error)) throw error;
329583
329660
  if (process.platform === "win32") {
329584
- return this.powershellJson(method, urlValue, headers, body, signal);
329661
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
329585
329662
  }
329586
329663
  throw error;
329587
329664
  });
329588
329665
  }
329589
- powershellJson(method, urlValue, headers, body = "", signal) {
329666
+ powershellJson(method, urlValue, headers, body = "", signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
329667
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
329590
329668
  if (_LLMProvider.powershellTransport) {
329591
- return _LLMProvider.powershellTransport(method, urlValue, headers, body);
329669
+ return this.withRequestTimeout(_LLMProvider.powershellTransport(method, urlValue, headers, body), effectiveTimeout, signal);
329592
329670
  }
329593
329671
  return new Promise((resolve16, reject) => {
329594
329672
  const headerJson = JSON.stringify(headers);
@@ -329617,7 +329695,7 @@ var LLMProvider = class _LLMProvider {
329617
329695
  " $raw = $headerJson | ConvertFrom-Json",
329618
329696
  " foreach ($p in $raw.PSObject.Properties) { $headers[$p.Name] = [string]$p.Value }",
329619
329697
  "}",
329620
- "$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = 120 }",
329698
+ `'$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = ${Math.max(1, Math.ceil(effectiveTimeout / 1e3))} }`,
329621
329699
  'if ($method -eq "POST") { $params["Body"] = $bodyJson }',
329622
329700
  'if ($method -eq "POST") { $params["ContentType"] = "application/json; charset=utf-8" }',
329623
329701
  "$resp = Invoke-WebRequest @params",
@@ -329649,8 +329727,8 @@ var LLMProvider = class _LLMProvider {
329649
329727
  const timer = setTimeout(() => {
329650
329728
  child.kill();
329651
329729
  cleanup();
329652
- reject(new Error("PowerShell HTTP fallback timeout"));
329653
- }, 13e4);
329730
+ reject(providerTimeoutError(effectiveTimeout));
329731
+ }, effectiveTimeout + 5e3);
329654
329732
  child.stdout.setEncoding("utf8");
329655
329733
  child.stderr.setEncoding("utf8");
329656
329734
  child.stdout.on("data", (chunk) => {
@@ -330098,10 +330176,10 @@ ${responsePath}
330098
330176
  return this.shouldUseResponsesFallback(Number(match[1]), errorText);
330099
330177
  }
330100
330178
  /**
330101
- * Loopback-aware transport injected into adapter `execute`. Mirrors the
330102
- * legacy orchestration exactly: streaming requests go through fetch with a
330103
- * 120s timeout and degrade to a non-streaming node-http request on fetch
330104
- * failure; non-streaming requests reuse postJsonWithFetchFallback.
330179
+ * Loopback-aware transport injected into adapter `execute`. Streaming
330180
+ * requests retain the fetch-to-node fallback for transport failures, while
330181
+ * a local deadline is returned directly so one request cannot become a
330182
+ * second Windows fallback request.
330105
330183
  */
330106
330184
  buildProviderAdapterTransport() {
330107
330185
  return async (request, signal) => {
@@ -330110,7 +330188,8 @@ ${responsePath}
330110
330188
  const forwardAbort = () => abort.abort(signal?.reason);
330111
330189
  if (signal?.aborted) forwardAbort();
330112
330190
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330113
- const timer = setTimeout(() => abort.abort(), 12e4);
330191
+ const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330192
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
330114
330193
  try {
330115
330194
  try {
330116
330195
  return await fetch(request.url, {
@@ -330121,6 +330200,7 @@ ${responsePath}
330121
330200
  });
330122
330201
  } catch (error) {
330123
330202
  if (signal?.aborted) throw abortFailure(signal);
330203
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
330124
330204
  if (!this.shouldUseNodeHttpFallback(error)) throw error;
330125
330205
  const fallbackHeaders = { ...request.headers };
330126
330206
  delete fallbackHeaders["Accept"];
@@ -330128,7 +330208,7 @@ ${responsePath}
330128
330208
  request.url,
330129
330209
  fallbackHeaders,
330130
330210
  { ...request.body, stream: false },
330131
- 12e4,
330211
+ effectiveTimeout,
330132
330212
  signal
330133
330213
  );
330134
330214
  return this.toTransportResponse(fallback);
@@ -330238,7 +330318,8 @@ ${responsePath}
330238
330318
  const forwardAbort = () => abort.abort(signal?.reason);
330239
330319
  if (signal?.aborted) forwardAbort();
330240
330320
  else signal?.addEventListener("abort", forwardAbort, { once: true });
330241
- const timeout = setTimeout(() => abort.abort(), 12e4);
330321
+ const effectiveTimeout = this.effectiveRequestTimeout(12e4);
330322
+ const timeout = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
330242
330323
  let reader = null;
330243
330324
  try {
330244
330325
  let response;
@@ -330250,9 +330331,10 @@ ${responsePath}
330250
330331
  signal: abort.signal
330251
330332
  });
330252
330333
  } catch (e3) {
330334
+ if (signal?.aborted) throw abortFailure(signal);
330335
+ if (abort.signal.aborted) throw abortFailure(abort.signal);
330253
330336
  if (!this.shouldUseNodeHttpFallback(e3)) throw e3;
330254
330337
  clearTimeout(timeout);
330255
- if (signal?.aborted) throw abortFailure(signal);
330256
330338
  yield* this.githubModelsChatNonStreaming(url, body, signal);
330257
330339
  return;
330258
330340
  }
@@ -330272,13 +330354,9 @@ ${responsePath}
330272
330354
  let currentReasoningContent = "";
330273
330355
  let contentPolicyBlocked = false;
330274
330356
  let emittedContent = false;
330357
+ const streamSignal = signal || new AbortController().signal;
330275
330358
  while (true) {
330276
- if (signal?.aborted) throw abortFailure(signal);
330277
- const readPromise = reader.read();
330278
- const timeoutPromise = new Promise(
330279
- (_3, reject) => setTimeout(() => reject(new Error("Stream read timeout")), 3e4)
330280
- );
330281
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
330359
+ const { done, value } = await readProviderStreamChunk(reader, streamSignal);
330282
330360
  if (done) break;
330283
330361
  buffer += decoder.decode(value, { stream: true });
330284
330362
  const lines = buffer.split("\n");
@@ -337924,6 +338002,19 @@ var WorkspaceManager = class {
337924
338002
  this.saveState();
337925
338003
  }
337926
338004
  }
338005
+ /**
338006
+ * Re-read the registry and persisted current-workspace pointer after another
338007
+ * Newmark entrypoint updates Work/*.json. This intentionally does not create
338008
+ * a workspace: a refresh must reflect the shared on-disk state exactly.
338009
+ */
338010
+ reloadFromStorage() {
338011
+ if (this.detached) return this.current;
338012
+ this.scan();
338013
+ this.validate();
338014
+ this.current = null;
338015
+ this.restoreCurrent();
338016
+ return this.current;
338017
+ }
337927
338018
  saveInternal() {
337928
338019
  if (this.detached) return;
337929
338020
  const p = path16.join(this.rootPath, "Work", "Local.json");
@@ -340014,17 +340105,35 @@ function normalizePublicProviderError(error, secrets = []) {
340014
340105
  }
340015
340106
  return raw.slice(0, 1200);
340016
340107
  }
340108
+ function throwIfKernelAborted(signal) {
340109
+ if (!signal?.aborted) return;
340110
+ const reason = signal.reason;
340111
+ if (reason instanceof Error) {
340112
+ reason.name = "AbortError";
340113
+ throw reason;
340114
+ }
340115
+ const error = new Error(reason ? String(reason) : "Agent run aborted");
340116
+ error.name = "AbortError";
340117
+ throw error;
340118
+ }
340017
340119
  async function runAgentKernel(agent) {
340018
340120
  const stopContextTimer = performanceTimer("context_prepare", { conversationId: agent.activeConversationId });
340121
+ const processSignal = agent.activeProcessSignal();
340122
+ if (processSignal?.aborted) {
340123
+ stopContextTimer();
340124
+ throwIfKernelAborted(processSignal);
340125
+ }
340019
340126
  if (!agent.engineModel()) {
340127
+ const message = "No LLM configured. Add provider in Settings > Models.";
340020
340128
  agent.status = "error";
340021
340129
  agent.saveWorkspaceConversationState();
340022
- return [{ type: "text", text: "[Error] No LLM configured. Add provider in Settings > Models." }];
340130
+ throw new Error(message);
340023
340131
  }
340024
340132
  const [{ Agent: NativeAgent }, KernelStreamCompat] = await Promise.all([
340025
340133
  Promise.resolve().then(() => (init_agentKernel(), agentKernel_exports)),
340026
340134
  Promise.resolve().then(() => (init_stream_types(), stream_types_exports))
340027
340135
  ]);
340136
+ throwIfKernelAborted(processSignal);
340028
340137
  const toolProvisioning = new ToolProvisionSession([], []);
340029
340138
  let activeToolSurfaceIdentity = "";
340030
340139
  let activeToolSurfaceNotice = "";
@@ -340050,6 +340159,7 @@ async function runAgentKernel(agent) {
340050
340159
  const initialToolSurface = refreshToolSurface(true);
340051
340160
  const assembledContext = agent.assembleContextV2(initialToolSurface.systemPromptNotice);
340052
340161
  const systemPrompt = assembledContext.text;
340162
+ throwIfKernelAborted(processSignal);
340053
340163
  let providerRequestCount = 0;
340054
340164
  let bootstrappedCompressionAt = agent.lastCompression?.at || "";
340055
340165
  stopContextTimer();
@@ -340070,6 +340180,24 @@ async function runAgentKernel(agent) {
340070
340180
  kernel2.state.tools = toKernelTools(agent, initialToolSurface.definitions, toolProvisioning);
340071
340181
  kernel2.state.messages = toKernelMessages(agent);
340072
340182
  agent.attachAgentKernelRuntime(kernel2);
340183
+ let detachProcessAbort = () => {
340184
+ };
340185
+ if (processSignal) {
340186
+ const abortKernel = () => kernel2.abort();
340187
+ if (processSignal.aborted) {
340188
+ kernel2.abort();
340189
+ } else {
340190
+ processSignal.addEventListener("abort", abortKernel, { once: true });
340191
+ detachProcessAbort = () => processSignal.removeEventListener("abort", abortKernel);
340192
+ }
340193
+ }
340194
+ try {
340195
+ throwIfKernelAborted(processSignal);
340196
+ } catch (error) {
340197
+ detachProcessAbort();
340198
+ agent.attachAgentKernelRuntime(null);
340199
+ throw error;
340200
+ }
340073
340201
  const tokens = [];
340074
340202
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
340075
340203
  let lastAssistant = null;
@@ -340173,6 +340301,7 @@ async function runAgentKernel(agent) {
340173
340301
  else agent.pendingOptions = agent.pendingOptions.filter((question) => !isPlanExecutionQuestion(question));
340174
340302
  }
340175
340303
  } finally {
340304
+ detachProcessAbort();
340176
340305
  agent.attachAgentKernelRuntime(null);
340177
340306
  }
340178
340307
  agent.status = "idle";
@@ -340339,20 +340468,18 @@ async function transformContext(agent, messages, signal) {
340339
340468
  const provider = agent.engineModel();
340340
340469
  if (!provider || !compressionModel) return messages;
340341
340470
  const newmarkMessages = publicHistoryFromKernelMessages(messages);
340342
- const beforeCompression = JSON.stringify(newmarkMessages);
340343
340471
  const compressionAt = agent.lastCompression?.at || "";
340344
- await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
340472
+ let compressed = await agent.maybeCompress(newmarkMessages, provider, processSignal, compressionModel);
340345
340473
  if (processSignal?.aborted) return messages;
340346
- const primaryCompressed = JSON.stringify(newmarkMessages) !== beforeCompression;
340347
- if (primaryCompressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
340348
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
340474
+ if (compressed && agent.estimateContextTokens(newmarkMessages) >= Math.floor(agent.contextWindow(compressionModel).maxTokens * 0.82)) {
340475
+ compressed = await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true) || compressed;
340349
340476
  }
340350
340477
  const windowMax = agent.contextWindow(compressionModel).maxTokens;
340351
340478
  const conservativeTokens = agent.estimateContextTokens(newmarkMessages);
340352
340479
  if (conservativeTokens >= Math.floor(windowMax * 0.9) && !processSignal?.aborted) {
340353
- await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true);
340480
+ compressed = await agent.maybeCompress(newmarkMessages, null, processSignal, compressionModel, true) || compressed;
340354
340481
  }
340355
- if (JSON.stringify(newmarkMessages) === beforeCompression) return messages;
340482
+ if (!compressed) return messages;
340356
340483
  const durableMessages = toKernelMessagesFromHistory(newmarkMessages, agent);
340357
340484
  if (agent.lastCompression?.at && agent.lastCompression.at !== compressionAt) {
340358
340485
  agent.recordContextCompressionStep();
@@ -342243,9 +342370,11 @@ function key2(model) {
342243
342370
  }
342244
342371
  var FileModelValidationCache = class {
342245
342372
  filePath;
342373
+ readOnly;
342246
342374
  records = /* @__PURE__ */ new Map();
342247
- constructor(rootPath) {
342375
+ constructor(rootPath, options = {}) {
342248
342376
  this.filePath = path20.join(rootPath, "model-validation", "records.json");
342377
+ this.readOnly = options.readOnly === true;
342249
342378
  this.load();
342250
342379
  }
342251
342380
  get(modelKey2) {
@@ -342254,10 +342383,12 @@ var FileModelValidationCache = class {
342254
342383
  }
342255
342384
  set(record) {
342256
342385
  this.records.set(record.modelKey || key2(record.model), JSON.parse(JSON.stringify(record)));
342386
+ if (this.readOnly) return;
342257
342387
  this.save();
342258
342388
  }
342259
342389
  delete(modelKey2) {
342260
342390
  if (!this.records.delete(modelKey2)) return;
342391
+ if (this.readOnly) return;
342261
342392
  this.save();
342262
342393
  }
342263
342394
  load() {
@@ -344133,7 +344264,7 @@ var Agent4 = class _Agent {
344133
344264
  this.subagentName = options.subagentName || "";
344134
344265
  this.subagentPrompt = options.subagentPrompt || "";
344135
344266
  this.linkedPlanAccess = options.linkedPlanAccess;
344136
- this.config = new ConfigManager(rootPath);
344267
+ this.config = new ConfigManager(rootPath, { readOnly: options.readOnlyConfig === true });
344137
344268
  this.compressionHistoryArchive = new CompressionHistoryArchive(rootPath);
344138
344269
  this.contextV2 = new AgentContextManager(rootPath, this.config);
344139
344270
  this.agentRunService = this.config.contextFlag("agent_runtime_v2") ? new AgentRunService(path28.join(rootPath, ".newmark-context-v2")) : null;
@@ -344664,7 +344795,10 @@ var Agent4 = class _Agent {
344664
344795
  }
344665
344796
  const previousAuto = this.model === "auto" ? this.resolvedDeployment : null;
344666
344797
  const qualified = parseDeploymentSelectionValue2(requested);
344667
- const current = qualified ? this.config.findDeployment(qualified) : requested ? this.config.findModel(requested) : void 0;
344798
+ const legacyQualified = requested.includes("/") ? this.config.allModels().filter(
344799
+ (model2) => `${model2.provider_id}/${model2.name}` === requested || `${model2.provider}/${model2.name}` === requested
344800
+ ) : [];
344801
+ const current = qualified ? this.config.findDeployment(qualified) : legacyQualified.length === 1 ? legacyQualified[0] : requested ? this.config.findModel(requested) : void 0;
344668
344802
  this.model = current?.name || requested;
344669
344803
  this.fixedDeployment = current ? this.deploymentRef(current) : qualified;
344670
344804
  this.resolvedDeployment = null;
@@ -345781,7 +345915,7 @@ ${String(event.toolArgs || "")}`;
345781
345915
  this.saveWorkspaceConversationState(true);
345782
345916
  return true;
345783
345917
  }
345784
- finishConversationWorkRun(runId, status, endedAt = this.nowIso()) {
345918
+ finishConversationWorkRun(runId, status, endedAt = this.nowIso(), errorMessage = "") {
345785
345919
  const run = this.workRuns.find((item) => item.runId === String(runId || ""));
345786
345920
  if (!run) return false;
345787
345921
  this.syncAgentRunTerminal(run.runId, status, endedAt);
@@ -345825,7 +345959,7 @@ ${String(event.toolArgs || "")}`;
345825
345959
  this.enforceGoalTerminalInvariant(status, goalAudit);
345826
345960
  this.emitWorkEvent({
345827
345961
  type: status === "completed" ? "done" : status === "error" ? "error" : "status",
345828
- content: status === "force_interrupted" ? "Force interrupted." : status === "interrupted" ? "Interrupted." : "Response complete.",
345962
+ content: status === "error" ? String(errorMessage || "").trim() || "Agent run failed." : status === "force_interrupted" ? "Force interrupted." : status === "interrupted" ? "Interrupted." : "Response complete.",
345829
345963
  status,
345830
345964
  runId: run.runId,
345831
345965
  conversationId: run.target.conversationId,
@@ -345970,6 +346104,7 @@ ${String(event.toolArgs || "")}`;
345970
346104
  this.activeAgentKernelRuntime = runtime;
345971
346105
  this.awaitingAgentKernelRuntime = false;
345972
346106
  if (!runtime) return;
346107
+ if (this.activeProcessAbortController?.signal.aborted) runtime.abort?.();
345973
346108
  const queued = this.pendingAgentKernelQueue.splice(0);
345974
346109
  for (const item of queued) {
345975
346110
  const accepted = this.forwardAgentKernelQueueMessage(item.content, item.queueMode, item.clientMessageId, item.runId, item.images, item.hiddenUserInput);
@@ -347131,6 +347266,25 @@ Review this persisted peer result and summarize or continue the parent task as n
347131
347266
  this.loadWorkspaceConversationState();
347132
347267
  return selected;
347133
347268
  }
347269
+ refreshWorkspaceRegistryFromStorage() {
347270
+ const before = JSON.stringify({
347271
+ internal: this.workspace.internal,
347272
+ external: this.workspace.external,
347273
+ current: this.workspace.current
347274
+ });
347275
+ const selected = this.workspace.reloadFromStorage();
347276
+ const after = JSON.stringify({
347277
+ internal: this.workspace.internal,
347278
+ external: this.workspace.external,
347279
+ current: this.workspace.current
347280
+ });
347281
+ if (before === after) return selected;
347282
+ if (selected) this.config.loadWorkspaceConfig(selected.path);
347283
+ else this.config.clearWorkspaceOverrides();
347284
+ this.workspaceConversations.clear();
347285
+ this.loadWorkspaceConversationState();
347286
+ return selected;
347287
+ }
347134
347288
  setConversation(id) {
347135
347289
  const clean = this.safeConversationId(id || "default");
347136
347290
  if (this.workspaceConversationKey() === this.loadedWorkspaceConversationKey) {
@@ -348124,28 +348278,25 @@ ${summary}`, segment, "local-summarize", true);
348124
348278
  this.saveWorkspaceConversationState(true);
348125
348279
  return { text, hiddenUserInput: true, goalContinuation: true };
348126
348280
  }
348127
- writeSessionArchive(messages, mode, model) {
348281
+ buildSessionArchive(messages, mode, model, archiveDir) {
348128
348282
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace("T", "_").replace("Z", "");
348129
- const archiveDir = this.archiveDir();
348130
- fs25.mkdirSync(archiveDir, { recursive: true });
348131
- const filename = `session_${stamp}.md`;
348132
- const outPath = path28.join(archiveDir, filename);
348133
- let md = `# Newmark Session \u2014 ${stamp}
348283
+ const filename = `session_${stamp}_${crypto14.randomUUID().slice(0, 8)}.md`;
348284
+ let markdown = `# Newmark Session \u2014 ${stamp}
348134
348285
 
348135
348286
  `;
348136
- md += `**Mode**: ${mode}
348287
+ markdown += `**Mode**: ${mode}
348137
348288
  **Model**: ${model}
348138
348289
  `;
348139
- md += `**Messages**: ${messages.length}
348290
+ markdown += `**Messages**: ${messages.length}
348140
348291
 
348141
348292
  ---
348142
348293
 
348143
348294
  `;
348144
- if (this.goal) md += `**Goal**: ${this.goal.objective}
348295
+ if (this.goal) markdown += `**Goal**: ${this.goal.objective}
348145
348296
 
348146
348297
  `;
348147
348298
  for (const msg of messages) {
348148
- md += `**[${msg.role}] ${msg.timestamp}**
348299
+ markdown += `**[${msg.role}] ${msg.timestamp}**
348149
348300
 
348150
348301
  ${msg.content}
348151
348302
 
@@ -348154,13 +348305,35 @@ ${msg.content}
348154
348305
  const archived = archiveConversationImageAttachment(this.rootPath, archiveDir, attachment);
348155
348306
  if (!archived) continue;
348156
348307
  const alt = archived.name.replace(/[\]\r\n]/g, " ").trim() || "Submitted image";
348157
- md += `![${alt}](${archived.relativePath})
348308
+ markdown += `![${alt}](${archived.relativePath})
348158
348309
 
348159
348310
  `;
348160
348311
  }
348161
348312
  }
348162
- fs25.writeFileSync(outPath, md, "utf-8");
348163
- return filename;
348313
+ return { filename, markdown };
348314
+ }
348315
+ writeSessionArchive(messages, mode, model) {
348316
+ const archiveDir = this.archiveDir();
348317
+ fs25.mkdirSync(archiveDir, { recursive: true });
348318
+ const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
348319
+ fs25.writeFileSync(path28.join(archiveDir, archive.filename), archive.markdown, "utf-8");
348320
+ return archive.filename;
348321
+ }
348322
+ async writeSessionArchiveAsync(messages, mode, model, archiveDir = this.archiveDir()) {
348323
+ const archive = this.buildSessionArchive(messages, mode, model, archiveDir);
348324
+ await fs25.promises.mkdir(archiveDir, { recursive: true });
348325
+ const outPath = path28.join(archiveDir, archive.filename);
348326
+ const tempPath = `${outPath}.${process.pid}.${crypto14.randomUUID()}.tmp`;
348327
+ try {
348328
+ await fs25.promises.writeFile(tempPath, archive.markdown, "utf-8");
348329
+ await fs25.promises.rename(tempPath, outPath);
348330
+ } finally {
348331
+ try {
348332
+ await fs25.promises.unlink(tempPath);
348333
+ } catch {
348334
+ }
348335
+ }
348336
+ return archive.filename;
348164
348337
  }
348165
348338
  archiveSession() {
348166
348339
  return this.writeSessionArchive(this.chatMessages, this.modeName(), this.model);
@@ -348228,6 +348401,93 @@ ${msg.content}
348228
348401
  }
348229
348402
  return filename;
348230
348403
  }
348404
+ /**
348405
+ * Non-blocking archive writer used by the desktop IPC path. The conversation
348406
+ * state merge remains synchronous and lock-protected, but the potentially
348407
+ * large markdown payload and manifest use promise-based filesystem I/O so
348408
+ * independent workspaces can archive in parallel without freezing Electron.
348409
+ */
348410
+ async archiveConversationAsync(conversationId) {
348411
+ return await this.archiveConversationAsyncUnlocked(conversationId);
348412
+ }
348413
+ async archiveConversationAsyncUnlocked(conversationId) {
348414
+ const ws = this.workspace.current;
348415
+ if (!ws) return null;
348416
+ const clean = this.safeConversationId(conversationId || "default");
348417
+ const stateKey2 = this.workspaceConversationStateKey(clean);
348418
+ if (!stateKey2) return null;
348419
+ const memoryKey = `${ws.isInternal ? "internal" : "external"}:${path28.resolve(ws.path)}::conversation:${clean}`;
348420
+ const archiveDir = path28.join(ws.path, "archive");
348421
+ const workspacePrefix = this.workspaceConversationPrefix() || "";
348422
+ const archiveMode = this.modeName();
348423
+ const archiveModel = this.model;
348424
+ const cachedStored = this.readStoredConversationState(ws);
348425
+ const stored = JSON.parse(JSON.stringify(cachedStored || {}));
348426
+ const persisted = stored.conversations?.[stateKey2];
348427
+ if (persisted) this.normalizeConversationTree(persisted);
348428
+ const memory = this.workspaceConversations.get(memoryKey);
348429
+ const persistedMessagesAvailable = persisted?.chatMessages !== void 0;
348430
+ const sourceMessages = persisted?.chatMessages ?? memory?.chatMessages ?? [];
348431
+ const sourceHistory = persistedMessagesAvailable ? persisted?.history ?? [] : memory?.history ?? persisted?.history ?? [];
348432
+ const messages = this.normalizeConversationChatMessages(sourceMessages, sourceHistory);
348433
+ const filename = await this.writeSessionArchiveAsync(messages, archiveMode, archiveModel, archiveDir);
348434
+ const archiveEntry = persisted ? JSON.parse(JSON.stringify(persisted)) : {
348435
+ title: this.titleFromMessages(messages, clean),
348436
+ chatMessages: messages,
348437
+ history: sourceHistory,
348438
+ plan: memory?.plan,
348439
+ linkedPlan: memory?.linkedPlan,
348440
+ subagentState: memory?.subagentState,
348441
+ workRuns: memory?.workRuns,
348442
+ continuations: memory?.continuations,
348443
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
348444
+ };
348445
+ const manifest = {
348446
+ version: 2,
348447
+ kind: "newmark-conversation-archive",
348448
+ archivedAt: (/* @__PURE__ */ new Date()).toISOString(),
348449
+ conversationId: clean,
348450
+ workspaceId: ws.id,
348451
+ workspaceName: ws.name,
348452
+ workspacePath: ws.path,
348453
+ workspaceInternal: ws.isInternal,
348454
+ statePrefix: workspacePrefix,
348455
+ entry: this.conversationEntryForDisk(archiveEntry)
348456
+ };
348457
+ const manifestPath = this.archiveManifestPath(path28.join(archiveDir, filename));
348458
+ const manifestTempPath = `${manifestPath}.${process.pid}.${crypto14.randomUUID()}.tmp`;
348459
+ try {
348460
+ await fs25.promises.writeFile(manifestTempPath, JSON.stringify(manifest, null, 2), "utf-8");
348461
+ await fs25.promises.rename(manifestTempPath, manifestPath);
348462
+ } finally {
348463
+ try {
348464
+ await fs25.promises.unlink(manifestTempPath);
348465
+ } catch {
348466
+ }
348467
+ }
348468
+ this.finalizeAsyncConversationArchive(clean, stateKey2, memoryKey, ws);
348469
+ return filename;
348470
+ }
348471
+ finalizeAsyncConversationArchive(clean, stateKey2, memoryKey, ws) {
348472
+ let nextActiveId = "";
348473
+ this.mutateStoredConversationState(ws, (latest) => {
348474
+ latest.conversations = latest.conversations || {};
348475
+ delete latest.conversations[stateKey2];
348476
+ const prefix = stateKey2.slice(0, Math.max(0, stateKey2.length - clean.length - 1)) + "-";
348477
+ const remaining = Object.keys(latest.conversations).filter((key3) => !prefix || key3.startsWith(prefix)).map((key3) => key3.slice(prefix.length)).filter(Boolean);
348478
+ const currentActiveId = this.safeConversationId(latest.activeConversationId || this.activeConversationId || "default");
348479
+ if (clean === currentActiveId) latest.activeConversationId = remaining[0] || "default";
348480
+ nextActiveId = latest.activeConversationId || remaining[0] || "default";
348481
+ return latest;
348482
+ });
348483
+ this.workspaceConversations.delete(memoryKey);
348484
+ const duplicateMemoryKey = `${ws.isInternal ? "internal" : "external"}:${path28.resolve(ws.path)}::conversation:${clean}`;
348485
+ this.workspaceConversations.delete(duplicateMemoryKey);
348486
+ if (clean === this.safeConversationId(this.activeConversationId || "default")) {
348487
+ this.activeConversationId = nextActiveId || "default";
348488
+ this.loadWorkspaceConversationState();
348489
+ }
348490
+ }
348231
348491
  listStoredConversationIds(stored) {
348232
348492
  const prefix = `${this.workspaceConversationPrefix() || ""}-`;
348233
348493
  return Object.keys(stored.conversations || {}).filter((key3) => !prefix || key3.startsWith(prefix)).map((key3) => key3.slice(prefix.length)).filter(Boolean);
@@ -348753,9 +349013,9 @@ ${msg.content}
348753
349013
  const provider = this.config.findProvider(providerId);
348754
349014
  return all.filter((m2) => m2.provider_id === (provider?.id || providerId));
348755
349015
  }
348756
- async validateModels(selectedNames) {
349016
+ async validateModels(selectedNames, options = {}) {
348757
349017
  if (this.modelValidationPromise) return this.modelValidationPromise;
348758
- const validation = this.runModelValidation(selectedNames);
349018
+ const validation = this.runModelValidation(selectedNames, options.persist !== false);
348759
349019
  this.modelValidationPromise = validation;
348760
349020
  try {
348761
349021
  return await validation;
@@ -348773,7 +349033,7 @@ ${msg.content}
348773
349033
  recentChecks: this.modelValidationProgress.recentChecks.map((item) => ({ ...item }))
348774
349034
  };
348775
349035
  }
348776
- async runModelValidation(selectedNames) {
349036
+ async runModelValidation(selectedNames, persist = true) {
348777
349037
  const selectedModels = this.config.modelsForSelections(selectedNames);
348778
349038
  if (!selectedModels.length) {
348779
349039
  this.modelValidationProgress = {
@@ -348791,7 +349051,7 @@ ${msg.content}
348791
349051
  }
348792
349052
  const results = [];
348793
349053
  const catalogByProvider = /* @__PURE__ */ new Map();
348794
- const cache = new FileModelValidationCache(this.rootPath);
349054
+ const cache = new FileModelValidationCache(this.rootPath, { readOnly: !persist });
348795
349055
  const checksPerModel = 11;
348796
349056
  let currentModel = "";
348797
349057
  let currentModelChecks = 0;
@@ -348923,7 +349183,7 @@ ${msg.content}
348923
349183
  completedModels: this.modelValidationProgress.completedModels + 1
348924
349184
  };
348925
349185
  }
348926
- this.config.save();
349186
+ if (persist) this.config.save();
348927
349187
  this.modelValidationProgress = {
348928
349188
  ...this.modelValidationProgress,
348929
349189
  running: false,
@@ -349175,7 +349435,8 @@ ${String(input2.content || "").slice(0, 18e3)}`;
349175
349435
  const text = typeof input2 === "string" ? input2 : String(input2.text || "");
349176
349436
  const inputEnvelope = typeof input2 === "string" ? null : input2;
349177
349437
  const hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
349178
- this.ensureUsableModelSelection();
349438
+ const explicitFixedModel = this.model !== "" && this.model !== "auto";
349439
+ if (!explicitFixedModel) this.ensureUsableModelSelection();
349179
349440
  const clientMessageId = String(inputEnvelope?.clientMessageId || "").trim();
349180
349441
  const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || "").trim();
349181
349442
  const rawImages = typeof input2 === "string" ? [] : Array.isArray(input2.images) ? input2.images : [];
@@ -349263,7 +349524,13 @@ ${String(input2.content || "").slice(0, 18e3)}`;
349263
349524
  await this.evaluateAndSwitch(displayText, inputEnvelope?.routePolicy);
349264
349525
  }
349265
349526
  if (this.model && this.modelIsUnavailable(this.model)) {
349527
+ const requestedModel = this.model;
349266
349528
  this.switchToFallbackModel();
349529
+ if (this.modelIsUnavailable(this.model)) {
349530
+ const message = `[Error] Model '${requestedModel || "unknown"}' is unavailable or not configured. Select a configured model or enable a valid provider before sending.`;
349531
+ this.status = "error";
349532
+ throw new Error(message);
349533
+ }
349267
349534
  }
349268
349535
  if (this.engine === "opencode") {
349269
349536
  if (images.length) return [{ type: "text", text: "[Vision unavailable] The OpenCode engine does not accept Newmark image attachments." }];
@@ -350256,11 +350523,11 @@ Falling back to built-in engine.` }];
350256
350523
  }
350257
350524
  }
350258
350525
  async maybeCompress(msgs, provider, signal, compressionModel, force = false) {
350259
- if (signal?.aborted) return;
350260
- if (!this.config.getBool("context", "auto_compress")) return;
350526
+ if (signal?.aborted) return false;
350527
+ if (!this.config.getBool("context", "auto_compress")) return false;
350261
350528
  const total = msgs.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content || "").length), 0);
350262
350529
  const budget = this.compressionBudget(msgs);
350263
- if (budget.estimatedTokens < budget.triggerTokens && !force) return;
350530
+ if (budget.estimatedTokens < budget.triggerTokens && !force) return false;
350264
350531
  if (!force && this.lastCompression && String(msgs[0]?.content || "").includes(this.lastCompression.summary)) {
350265
350532
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
350266
350533
  const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
@@ -350268,16 +350535,16 @@ Falling back to built-in engine.` }];
350268
350535
  const tokenGrowth = baselineTokens ? Math.max(0, budget.estimatedTokens - baselineTokens) : Number.POSITIVE_INFINITY;
350269
350536
  const minCharGrowth = Math.max(12e3, Math.floor(baselineChars * 0.25));
350270
350537
  const minTokenGrowth = Math.max(1024, Math.floor(budget.triggerTokens * 0.2));
350271
- if (charGrowth < minCharGrowth && tokenGrowth < minTokenGrowth) return;
350538
+ if (charGrowth < minCharGrowth && tokenGrowth < minTokenGrowth) return false;
350272
350539
  }
350273
350540
  const originalMessageCount = msgs.length;
350274
350541
  const configuredKeepLast = this.config.getNum("context", "keep_recent_messages") || 10;
350275
- if (msgs.length <= 1) return;
350542
+ if (msgs.length <= 1) return false;
350276
350543
  const continuationAnchorTokens = this.estimateContextTokens([this.postCompressionContinuationMessage()]);
350277
350544
  const recentBudget = Math.max(64, budget.targetTokens - budget.summaryTokens - continuationAnchorTokens);
350278
350545
  const recent = this.recentContextSuffix(msgs, configuredKeepLast, recentBudget);
350279
350546
  const recentStart = Math.max(0, msgs.length - recent.length);
350280
- if (recentStart <= 0) return;
350547
+ if (recentStart <= 0) return false;
350281
350548
  const middle = msgs.slice(0, recentStart);
350282
350549
  const currentInstruction = this.latestUserHistoryText(recent);
350283
350550
  const compression = await this.buildCompressionSummary(
@@ -350289,7 +350556,7 @@ Falling back to built-in engine.` }];
350289
350556
  compressionModel || this.activeModelName(),
350290
350557
  currentInstruction
350291
350558
  );
350292
- if (signal?.aborted) return;
350559
+ if (signal?.aborted) return false;
350293
350560
  const compressed = [{
350294
350561
  role: "system",
350295
350562
  content: compression.summary
@@ -350314,6 +350581,7 @@ Falling back to built-in engine.` }];
350314
350581
  };
350315
350582
  this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
350316
350583
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
350584
+ return true;
350317
350585
  }
350318
350586
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
350319
350587
  const workspacePath = this.workspace.current?.path || this.rootPath;
@@ -351649,7 +351917,12 @@ var ConversationKernel = class {
351649
351917
  if (runtime.runId === runId && runtime.stopRequestedRunId === runId) {
351650
351918
  stopped = true;
351651
351919
  } else {
351652
- runtime.runner.finishConversationWorkRun(runId, "error");
351920
+ runtime.runner.finishConversationWorkRun(
351921
+ runId,
351922
+ "error",
351923
+ void 0,
351924
+ error instanceof Error ? error.message : String(error)
351925
+ );
351653
351926
  throw error;
351654
351927
  }
351655
351928
  } finally {