newmark-agent 0.5.10 → 0.5.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.
@@ -8383,7 +8383,7 @@ var require_createWorker = __commonJS({
8383
8383
  ...defaultOptions,
8384
8384
  ..._options
8385
8385
  });
8386
- const promises2 = {};
8386
+ const promises3 = {};
8387
8387
  const currentLangs = typeof langs === "string" ? langs.split("+") : langs;
8388
8388
  let currentOem = oem;
8389
8389
  let currentConfig = config;
@@ -8403,7 +8403,7 @@ var require_createWorker = __commonJS({
8403
8403
  const startJob = ({ id: jobId, action, payload }) => new Promise((resolve16, reject) => {
8404
8404
  log2(`[${id}]: Start ${jobId}, action=${action}`);
8405
8405
  const promiseId = `${action}-${jobId}`;
8406
- promises2[promiseId] = { resolve: resolve16, reject };
8406
+ promises3[promiseId] = { resolve: resolve16, reject };
8407
8407
  send(worker, {
8408
8408
  workerId: id,
8409
8409
  jobId,
@@ -8508,11 +8508,11 @@ var require_createWorker = __commonJS({
8508
8508
  const promiseId = `${action}-${jobId}`;
8509
8509
  if (status === "resolve") {
8510
8510
  log2(`[${workerId}]: Complete ${jobId}`);
8511
- promises2[promiseId].resolve({ jobId, data });
8512
- delete promises2[promiseId];
8511
+ promises3[promiseId].resolve({ jobId, data });
8512
+ delete promises3[promiseId];
8513
8513
  } else if (status === "reject") {
8514
- promises2[promiseId].reject(data);
8515
- delete promises2[promiseId];
8514
+ promises3[promiseId].reject(data);
8515
+ delete promises3[promiseId];
8516
8516
  if (action === "load") workerResReject(data);
8517
8517
  if (errorHandler) {
8518
8518
  errorHandler(data);
@@ -54006,18 +54006,18 @@ var require_proxy_agent = __commonJS({
54006
54006
  }
54007
54007
  }
54008
54008
  [kClose]() {
54009
- const promises2 = [this[kAgent].close()];
54009
+ const promises3 = [this[kAgent].close()];
54010
54010
  if (this[kClient]) {
54011
- promises2.push(this[kClient].close());
54011
+ promises3.push(this[kClient].close());
54012
54012
  }
54013
- return Promise.all(promises2);
54013
+ return Promise.all(promises3);
54014
54014
  }
54015
54015
  [kDestroy]() {
54016
- const promises2 = [this[kAgent].destroy()];
54016
+ const promises3 = [this[kAgent].destroy()];
54017
54017
  if (this[kClient]) {
54018
- promises2.push(this[kClient].destroy());
54018
+ promises3.push(this[kClient].destroy());
54019
54019
  }
54020
- return Promise.all(promises2);
54020
+ return Promise.all(promises3);
54021
54021
  }
54022
54022
  };
54023
54023
  function buildHeaders(headers) {
@@ -344923,6 +344923,7 @@ var fs24 = __toESM(require("fs"));
344923
344923
  var path27 = __toESM(require("path"));
344924
344924
  var import_crypto14 = require("crypto");
344925
344925
  var processStates = /* @__PURE__ */ new Map();
344926
+ var preparedPreviousStates = /* @__PURE__ */ new Map();
344926
344927
  function stateKey(root2, role) {
344927
344928
  return `${path27.resolve(root2)}\0${role}`;
344928
344929
  }
@@ -344968,7 +344969,8 @@ function beginRuntimeLifecycle(root2, role = "main") {
344968
344969
  const key3 = stateKey(root2, role);
344969
344970
  const existingProcessState = processStates.get(key3);
344970
344971
  if (existingProcessState) return existingProcessState;
344971
- const previousStates = readActiveStates(root2, role);
344972
+ const previousStates = preparedPreviousStates.get(key3) || readActiveStates(root2, role);
344973
+ preparedPreviousStates.delete(key3);
344972
344974
  const previousOwnerAlive = previousStates.some((previous) => isRuntimeProcessAlive(Number(previous.pid)));
344973
344975
  const state = {
344974
344976
  role,
@@ -344991,12 +344993,12 @@ function markRuntimeLifecycleClean(root2, role = "main") {
344991
344993
  const current = processStates.get(key3);
344992
344994
  if (!current) return;
344993
344995
  try {
344994
- writeState(root2, role, {
344995
- ...current,
344996
- active: false,
344997
- cleanExitAt: (/* @__PURE__ */ new Date()).toISOString()
344998
- });
344996
+ fs24.unlinkSync(statePath(root2, role, current.ownerId));
344999
344997
  } catch {
344998
+ try {
344999
+ writeState(root2, role, { ...current, active: false, cleanExitAt: (/* @__PURE__ */ new Date()).toISOString() });
345000
+ } catch {
345001
+ }
345000
345002
  }
345001
345003
  }
345002
345004
 
@@ -348522,12 +348524,15 @@ Conversation title (a few words):`;
348522
348524
  defaultInputMode() {
348523
348525
  return this.config.getStr("general", "default_input") === "next" ? "next" : "guide";
348524
348526
  }
348525
- abortActiveKernelRun() {
348527
+ abortActiveKernelRun(reason = "unspecified") {
348526
348528
  let aborted = false;
348527
348529
  this.subagents.pauseScheduling();
348528
348530
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
348529
348531
  const abortError5 = new Error("Agent run aborted");
348530
348532
  abortError5.name = "AbortError";
348533
+ if (process.env.NEWMARK_PROVIDER_DIAGNOSTICS === "1") {
348534
+ console.error(`[NewmarkKernel] abort-request reason=${String(reason).slice(0, 160)} conversation=${this.activeConversationId}`);
348535
+ }
348531
348536
  this.activeProcessAbortController.abort(abortError5);
348532
348537
  aborted = true;
348533
348538
  }
@@ -348536,7 +348541,7 @@ Conversation title (a few words):`;
348536
348541
  aborted = true;
348537
348542
  }
348538
348543
  for (const peer of this.activePeerAgents.values()) {
348539
- aborted = peer.abortActiveKernelRun() || aborted;
348544
+ aborted = peer.abortActiveKernelRun(reason) || aborted;
348540
348545
  }
348541
348546
  this.pendingAgentKernelQueue = [];
348542
348547
  return aborted;
@@ -350043,10 +350048,16 @@ ${summary}`, segment, "local-summarize", true);
350043
350048
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
350044
350049
  }
350045
350050
  compactHistoricalImages(messages) {
350051
+ const continuationIndex = messages.reduce((found, message, index) => {
350052
+ const content = String(message?.content || "");
350053
+ return content.includes("[Post-Compression Task Continuation]") ? index : found;
350054
+ }, -1);
350046
350055
  const lastParts = Array.isArray(messages.at(-1)?.content) ? messages.at(-1).content : [];
350047
- const newestImageMessage = lastParts.some((part) => part?.type === "image_url") ? messages.length - 1 : -1;
350056
+ const newestImageMessage = continuationIndex < 0 && lastParts.some((part) => part?.type === "image_url") ? messages.length - 1 : -1;
350048
350057
  return messages.map((message, index) => {
350049
- if (!Array.isArray(message.content) || index === newestImageMessage) return { ...message };
350058
+ if (!Array.isArray(message.content)) return { ...message };
350059
+ const preserveRecentImage = continuationIndex >= 0 && index > continuationIndex;
350060
+ if (preserveRecentImage || index === newestImageMessage) return { ...message };
350050
350061
  const parts = message.content.flatMap((part) => {
350051
350062
  if (part?.type !== "image_url") return [{ ...part }];
350052
350063
  return [{ type: "text", text: "[Historical image attachment omitted after context compression.]" }];
@@ -352970,12 +352981,16 @@ ${text.slice(-tailChars).trimStart()}`;
352970
352981
  }
352971
352982
  buildSystemPrompt() {
352972
352983
  const cwd = this.workspace.current?.path || this.rootPath;
352984
+ const newmarkConfigRoot = path28.resolve(this.rootPath);
352985
+ const newmarkConfigFile = path28.join(newmarkConfigRoot, "config.json");
352973
352986
  const enabledSkills = this.skills.active();
352974
352987
  const globalPromptPath = path28.join(this.rootPath, "agent.md");
352975
352988
  const globalPrompt = normalizeInjectedPrompt(fs25.existsSync(globalPromptPath) ? fs25.readFileSync(globalPromptPath, "utf-8") : "");
352976
352989
  const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
352977
352990
  const identity = JSON.stringify({
352978
352991
  cwd,
352992
+ newmarkConfigRoot,
352993
+ newmarkConfigFile,
352979
352994
  mode: this.mode,
352980
352995
  conversationId: this.activeConversationId,
352981
352996
  subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
@@ -352994,6 +353009,9 @@ ${text.slice(-tailChars).trimStart()}`;
352994
353009
  if (this.systemPromptCache?.identity === identity) return this.systemPromptCache.value;
352995
353010
  const parts = [`${CORE_SYSTEM_PROMPT}
352996
353011
 
353012
+ ## Newmark Runtime Configuration
353013
+ Newmark's own user-level configuration and runtime state are stored by default under ${newmarkConfigRoot} (the conventional user path is user/.Newmark). The primary configuration file is ${newmarkConfigFile}; provider credentials, conversation state, caches, Work/Flow data, Memory Lab, skills, and archives may also live beneath this root. Treat these files as Newmark internal state: read or modify them only when the user explicitly asks, never expose API keys or tokens, and do not confuse this runtime root with the active workspace.
353014
+
352997
353015
  ## Current Working Directory
352998
353016
  ${cwd}
352999
353017
 
@@ -353739,7 +353757,12 @@ var ConversationKernel = class {
353739
353757
  ...images?.length ? { images } : {},
353740
353758
  ...attachments?.length ? { attachments } : {},
353741
353759
  ...visible ? { visibleUserInput: visible } : {},
353742
- ...visibleMode ? { visibleMode } : {}
353760
+ ...visibleMode ? { visibleMode } : {},
353761
+ // A normal user follow-up must never inherit the identity metadata that
353762
+ // was only needed while it lived in the runtime queue. In particular,
353763
+ // do not let a stale/forwarded hiddenUserInput flag classify it as an
353764
+ // Agent-generated continuation when Agent.process persists the turn.
353765
+ hiddenUserInput: message.hiddenUserInput === true && !message.clientMessageId
353743
353766
  };
353744
353767
  }
353745
353768
  queueItems(target) {
@@ -354312,7 +354335,7 @@ ${item.goalObjective}` : item.text,
354312
354335
  this.deferOutstandingGuides(runtime);
354313
354336
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
354314
354337
  runtime.stopCheckpointed = checkpointed;
354315
- runtime.runner.abortActiveKernelRun();
354338
+ runtime.runner.abortActiveKernelRun("user_stop");
354316
354339
  runtime.runner.emitWorkEvent({
354317
354340
  type: "status",
354318
354341
  content: "Stop requested. Saving progress and interrupting this conversation.",
@@ -354446,6 +354469,7 @@ ${item.goalObjective}` : item.text,
354446
354469
  async run(runtime, message, options) {
354447
354470
  this.applyOptions(runtime.runner, options);
354448
354471
  let lastTokens = await this.runSingle(runtime, message);
354472
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
354449
354473
  if (runtime.stopRequestedRunId === runtime.runId) {
354450
354474
  this.mirrorHostIfTargetActive(runtime);
354451
354475
  return this.result(runtime, lastTokens);
@@ -354472,6 +354496,7 @@ ${item.goalObjective}` : item.text,
354472
354496
  }
354473
354497
  if (batchGuides.length === 1) {
354474
354498
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354499
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode)) break;
354475
354500
  continue;
354476
354501
  }
354477
354502
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join("\n");
@@ -354482,8 +354507,11 @@ ${batchText}`,
354482
354507
  batchGuides
354483
354508
  };
354484
354509
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354510
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, "steer")) break;
354485
354511
  } else {
354486
- lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
354512
+ const drained = this.drainQueuedFollowUpMessage(next.message);
354513
+ lastTokens = await this.runSingle(runtime, drained, next.queueMode);
354514
+ if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode)) break;
354487
354515
  }
354488
354516
  }
354489
354517
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354518,6 +354546,35 @@ Review this persisted peer result and summarize or continue the parent task as n
354518
354546
  this.mirrorHostIfTargetActive(runtime);
354519
354547
  return this.result(runtime, lastTokens);
354520
354548
  }
354549
+ automaticMessage(message, queueMode) {
354550
+ return queueMode === "steer" || typeof message !== "string" && (message.hiddenUserInput === true || message.goalContinuation === true || !!message.batchGuides?.length);
354551
+ }
354552
+ currentAssistantFingerprint(runtime) {
354553
+ const messages = runtime.runner.chatMessages || [];
354554
+ const last = messages[messages.length - 1];
354555
+ if (!last || last.role !== "assistant") return "";
354556
+ return String(last.content || "").trim().replace(/\s+/g, " ").toLowerCase().slice(0, 4e3);
354557
+ }
354558
+ rememberAutomaticAssistantFingerprint(runtime, message, queueMode) {
354559
+ if (this.automaticMessage(message, queueMode)) runtime.lastAutomaticAssistantFingerprint = this.currentAssistantFingerprint(runtime) || void 0;
354560
+ }
354561
+ repeatedAutomaticAssistant(runtime, message, queueMode) {
354562
+ if (!this.automaticMessage(message, queueMode)) return false;
354563
+ const fingerprint2 = this.currentAssistantFingerprint(runtime);
354564
+ if (!fingerprint2) return false;
354565
+ if (runtime.lastAutomaticAssistantFingerprint === fingerprint2) {
354566
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter((item) => {
354567
+ const automatic = item.queueMode === "steer" || typeof item.message !== "string" && item.message.hiddenUserInput === true;
354568
+ if (automatic) runtime.runner.consumeConversationContinuation({ content: typeof item.message === "string" ? item.message : item.message.text, queueMode: item.queueMode, clientMessageId: typeof item.message === "string" ? void 0 : item.message.clientMessageId });
354569
+ return !automatic;
354570
+ });
354571
+ runtime.runner.recordWorkStatus("Automatic continuation stopped after a repeated assistant response.");
354572
+ this.emitQueueUpdate(runtime);
354573
+ return true;
354574
+ }
354575
+ runtime.lastAutomaticAssistantFingerprint = fingerprint2;
354576
+ return false;
354577
+ }
354521
354578
  /**
354522
354579
  * Apply a model selection recorded while a Build block was running. The
354523
354580
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -354557,7 +354614,16 @@ Review this persisted peer result and summarize or continue the parent task as n
354557
354614
  tokens = await Promise.race([
354558
354615
  runtime.runner.process(message),
354559
354616
  new Promise((_3, reject) => {
354560
- timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1e3)}s)`)), timeoutMs);
354617
+ timeout = setTimeout(() => {
354618
+ runtime.runner.abortActiveKernelRun(`process_timeout_${timeoutMs}ms`);
354619
+ runtime.runner.emitWorkEvent({
354620
+ type: "error",
354621
+ content: `Process timeout (${Math.round(timeoutMs / 1e3)}s); the run was aborted to prevent a stale worker from continuing.`,
354622
+ status: "error",
354623
+ runId: runtime.runId
354624
+ });
354625
+ reject(new Error(`Process timeout (${Math.round(timeoutMs / 1e3)}s); run aborted and checkpointed`));
354626
+ }, timeoutMs);
354561
354627
  })
354562
354628
  ]);
354563
354629
  } catch (error) {
@@ -354618,7 +354684,8 @@ Review this persisted peer result and summarize or continue the parent task as n
354618
354684
  guideReceipts: /* @__PURE__ */ new Map(),
354619
354685
  guideEnvelopes: /* @__PURE__ */ new Map(),
354620
354686
  goalContinuationTimer: void 0,
354621
- pendingContinuationRunId: void 0
354687
+ pendingContinuationRunId: void 0,
354688
+ lastAutomaticAssistantFingerprint: void 0
354622
354689
  };
354623
354690
  runner.setGoalContinuationGate(() => {
354624
354691
  this.queueState(runtime);
@@ -731,7 +731,7 @@ export declare class Agent {
731
731
  persistActiveConversationSelection(id: string, ws?: WorkspaceInfo | null): string;
732
732
  setInputMode(mode: string): InputMode;
733
733
  private defaultInputMode;
734
- abortActiveKernelRun(): boolean;
734
+ abortActiveKernelRun(reason?: string): boolean;
735
735
  activeProcessSignal(): AbortSignal | undefined;
736
736
  recordWorkRunPrimaryPrompt(content: string): void;
737
737
  conversationBuildHistory(limit?: number): Array<{
@@ -4119,12 +4119,15 @@ class Agent {
4119
4119
  defaultInputMode() {
4120
4120
  return this.config.getStr('general', 'default_input') === 'next' ? 'next' : 'guide';
4121
4121
  }
4122
- abortActiveKernelRun() {
4122
+ abortActiveKernelRun(reason = 'unspecified') {
4123
4123
  let aborted = false;
4124
4124
  this.subagents.pauseScheduling();
4125
4125
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
4126
4126
  const abortError = new Error('Agent run aborted');
4127
4127
  abortError.name = 'AbortError';
4128
+ if (process.env.NEWMARK_PROVIDER_DIAGNOSTICS === '1') {
4129
+ console.error(`[NewmarkKernel] abort-request reason=${String(reason).slice(0, 160)} conversation=${this.activeConversationId}`);
4130
+ }
4128
4131
  this.activeProcessAbortController.abort(abortError);
4129
4132
  aborted = true;
4130
4133
  }
@@ -4133,7 +4136,7 @@ class Agent {
4133
4136
  aborted = true;
4134
4137
  }
4135
4138
  for (const peer of this.activePeerAgents.values()) {
4136
- aborted = peer.abortActiveKernelRun() || aborted;
4139
+ aborted = peer.abortActiveKernelRun(reason) || aborted;
4137
4140
  }
4138
4141
  this.pendingAgentKernelQueue = [];
4139
4142
  return aborted;
@@ -5780,10 +5783,22 @@ class Agent {
5780
5783
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
5781
5784
  }
5782
5785
  compactHistoricalImages(messages) {
5786
+ // The post-compression continuation anchor separates summarized history
5787
+ // from the retained recent window. Preserve every user image in that
5788
+ // recent window (not only the last image message): a burst of consecutive
5789
+ // uploads must remain available to the next vision request. Images older
5790
+ // than the retained window are still replaced by bounded text markers.
5791
+ const continuationIndex = messages.reduce((found, message, index) => {
5792
+ const content = String(message?.content || '');
5793
+ return content.includes('[Post-Compression Task Continuation]') ? index : found;
5794
+ }, -1);
5783
5795
  const lastParts = Array.isArray(messages.at(-1)?.content) ? messages.at(-1).content : [];
5784
- const newestImageMessage = lastParts.some(part => part?.type === 'image_url') ? messages.length - 1 : -1;
5796
+ const newestImageMessage = continuationIndex < 0 && lastParts.some(part => part?.type === 'image_url') ? messages.length - 1 : -1;
5785
5797
  return messages.map((message, index) => {
5786
- if (!Array.isArray(message.content) || index === newestImageMessage)
5798
+ if (!Array.isArray(message.content))
5799
+ return { ...message };
5800
+ const preserveRecentImage = continuationIndex >= 0 && index > continuationIndex;
5801
+ if (preserveRecentImage || index === newestImageMessage)
5787
5802
  return { ...message };
5788
5803
  const parts = message.content.flatMap(part => {
5789
5804
  if (part?.type !== 'image_url')
@@ -9125,12 +9140,16 @@ class Agent {
9125
9140
  }
9126
9141
  buildSystemPrompt() {
9127
9142
  const cwd = this.workspace.current?.path || this.rootPath;
9143
+ const newmarkConfigRoot = path.resolve(this.rootPath);
9144
+ const newmarkConfigFile = path.join(newmarkConfigRoot, 'config.json');
9128
9145
  const enabledSkills = this.skills.active();
9129
9146
  const globalPromptPath = path.join(this.rootPath, 'agent.md');
9130
9147
  const globalPrompt = normalizeInjectedPrompt(fs.existsSync(globalPromptPath) ? fs.readFileSync(globalPromptPath, 'utf-8') : '');
9131
9148
  const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
9132
9149
  const identity = JSON.stringify({
9133
9150
  cwd,
9151
+ newmarkConfigRoot,
9152
+ newmarkConfigFile,
9134
9153
  mode: this.mode,
9135
9154
  conversationId: this.activeConversationId,
9136
9155
  subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
@@ -9148,7 +9167,7 @@ class Agent {
9148
9167
  });
9149
9168
  if (this.systemPromptCache?.identity === identity)
9150
9169
  return this.systemPromptCache.value;
9151
- const parts = [`${CORE_SYSTEM_PROMPT}\n\n## Current Working Directory\n${cwd}\n\nWhen using file tools (read, write, edit, glob), use ABSOLUTE paths rooted at this directory. Never guess paths. First use \`pwd\` or \`bash\` to verify.`];
9170
+ const parts = [`${CORE_SYSTEM_PROMPT}\n\n## Newmark Runtime Configuration\nNewmark's own user-level configuration and runtime state are stored by default under ${newmarkConfigRoot} (the conventional user path is user/.Newmark). The primary configuration file is ${newmarkConfigFile}; provider credentials, conversation state, caches, Work/Flow data, Memory Lab, skills, and archives may also live beneath this root. Treat these files as Newmark internal state: read or modify them only when the user explicitly asks, never expose API keys or tokens, and do not confuse this runtime root with the active workspace.\n\n## Current Working Directory\n${cwd}\n\nWhen using file tools (read, write, edit, glob), use ABSOLUTE paths rooted at this directory. Never guess paths. First use \`pwd\` or \`bash\` to verify.`];
9152
9171
  if (this.isSubagentRuntime) {
9153
9172
  parts.push([
9154
9173
  '## Subagent Sandbox',
@@ -285,6 +285,10 @@ export declare class ConversationKernel {
285
285
  private settleCooperativeStop;
286
286
  private stopAutomaticContinuationAfterError;
287
287
  private run;
288
+ private automaticMessage;
289
+ private currentAssistantFingerprint;
290
+ private rememberAutomaticAssistantFingerprint;
291
+ private repeatedAutomaticAssistant;
288
292
  /**
289
293
  * Apply a model selection recorded while a Build block was running. The
290
294
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -85,6 +85,11 @@ class ConversationKernel {
85
85
  ...(attachments?.length ? { attachments } : {}),
86
86
  ...(visible ? { visibleUserInput: visible } : {}),
87
87
  ...(visibleMode ? { visibleMode } : {}),
88
+ // A normal user follow-up must never inherit the identity metadata that
89
+ // was only needed while it lived in the runtime queue. In particular,
90
+ // do not let a stale/forwarded hiddenUserInput flag classify it as an
91
+ // Agent-generated continuation when Agent.process persists the turn.
92
+ hiddenUserInput: message.hiddenUserInput === true && !message.clientMessageId,
88
93
  };
89
94
  }
90
95
  queueItems(target) {
@@ -718,7 +723,7 @@ class ConversationKernel {
718
723
  this.deferOutstandingGuides(runtime);
719
724
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
720
725
  runtime.stopCheckpointed = checkpointed;
721
- runtime.runner.abortActiveKernelRun();
726
+ runtime.runner.abortActiveKernelRun('user_stop');
722
727
  runtime.runner.emitWorkEvent({
723
728
  type: 'status',
724
729
  content: 'Stop requested. Saving progress and interrupting this conversation.',
@@ -874,6 +879,7 @@ class ConversationKernel {
874
879
  async run(runtime, message, options) {
875
880
  this.applyOptions(runtime.runner, options);
876
881
  let lastTokens = await this.runSingle(runtime, message);
882
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
877
883
  if (runtime.stopRequestedRunId === runtime.runId) {
878
884
  this.mirrorHostIfTargetActive(runtime);
879
885
  return this.result(runtime, lastTokens);
@@ -904,6 +910,8 @@ class ConversationKernel {
904
910
  }
905
911
  if (batchGuides.length === 1) {
906
912
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
913
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode))
914
+ break;
907
915
  continue;
908
916
  }
909
917
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join('\n');
@@ -913,9 +921,14 @@ class ConversationKernel {
913
921
  batchGuides,
914
922
  };
915
923
  lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
924
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, 'steer'))
925
+ break;
916
926
  }
917
927
  else {
918
- lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
928
+ const drained = this.drainQueuedFollowUpMessage(next.message);
929
+ lastTokens = await this.runSingle(runtime, drained, next.queueMode);
930
+ if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode))
931
+ break;
919
932
  }
920
933
  }
921
934
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -956,6 +969,41 @@ class ConversationKernel {
956
969
  this.mirrorHostIfTargetActive(runtime);
957
970
  return this.result(runtime, lastTokens);
958
971
  }
972
+ automaticMessage(message, queueMode) {
973
+ return queueMode === 'steer'
974
+ || (typeof message !== 'string' && (message.hiddenUserInput === true || message.goalContinuation === true || !!message.batchGuides?.length));
975
+ }
976
+ currentAssistantFingerprint(runtime) {
977
+ const messages = runtime.runner.chatMessages || [];
978
+ const last = messages[messages.length - 1];
979
+ if (!last || last.role !== 'assistant')
980
+ return '';
981
+ return String(last.content || '').trim().replace(/\s+/g, ' ').toLowerCase().slice(0, 4000);
982
+ }
983
+ rememberAutomaticAssistantFingerprint(runtime, message, queueMode) {
984
+ if (this.automaticMessage(message, queueMode))
985
+ runtime.lastAutomaticAssistantFingerprint = this.currentAssistantFingerprint(runtime) || undefined;
986
+ }
987
+ repeatedAutomaticAssistant(runtime, message, queueMode) {
988
+ if (!this.automaticMessage(message, queueMode))
989
+ return false;
990
+ const fingerprint = this.currentAssistantFingerprint(runtime);
991
+ if (!fingerprint)
992
+ return false;
993
+ if (runtime.lastAutomaticAssistantFingerprint === fingerprint) {
994
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter(item => {
995
+ const automatic = item.queueMode === 'steer' || (typeof item.message !== 'string' && item.message.hiddenUserInput === true);
996
+ if (automatic)
997
+ runtime.runner.consumeConversationContinuation({ content: typeof item.message === 'string' ? item.message : item.message.text, queueMode: item.queueMode, clientMessageId: typeof item.message === 'string' ? undefined : item.message.clientMessageId });
998
+ return !automatic;
999
+ });
1000
+ runtime.runner.recordWorkStatus('Automatic continuation stopped after a repeated assistant response.');
1001
+ this.emitQueueUpdate(runtime);
1002
+ return true;
1003
+ }
1004
+ runtime.lastAutomaticAssistantFingerprint = fingerprint;
1005
+ return false;
1006
+ }
959
1007
  /**
960
1008
  * Apply a model selection recorded while a Build block was running. The
961
1009
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -999,7 +1047,20 @@ class ConversationKernel {
999
1047
  tokens = await Promise.race([
1000
1048
  runtime.runner.process(message),
1001
1049
  new Promise((_, reject) => {
1002
- timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s)`)), timeoutMs);
1050
+ timeout = setTimeout(() => {
1051
+ // A timeout must cancel the provider/kernel operation as well as
1052
+ // reject the caller. Previously only the Promise.race rejected,
1053
+ // leaving the worker running in the background and making the
1054
+ // UI observe a later unexplained interruption.
1055
+ runtime.runner.abortActiveKernelRun(`process_timeout_${timeoutMs}ms`);
1056
+ runtime.runner.emitWorkEvent({
1057
+ type: 'error',
1058
+ content: `Process timeout (${Math.round(timeoutMs / 1000)}s); the run was aborted to prevent a stale worker from continuing.`,
1059
+ status: 'error',
1060
+ runId: runtime.runId,
1061
+ });
1062
+ reject(new Error(`Process timeout (${Math.round(timeoutMs / 1000)}s); run aborted and checkpointed`));
1063
+ }, timeoutMs);
1003
1064
  }),
1004
1065
  ]);
1005
1066
  }
@@ -1067,6 +1128,7 @@ class ConversationKernel {
1067
1128
  guideEnvelopes: new Map(),
1068
1129
  goalContinuationTimer: undefined,
1069
1130
  pendingContinuationRunId: undefined,
1131
+ lastAutomaticAssistantFingerprint: undefined,
1070
1132
  };
1071
1133
  runner.setGoalContinuationGate(() => {
1072
1134
  this.queueState(runtime);
@@ -1378,6 +1378,27 @@ class ElectronUtilityAgentClient {
1378
1378
  if (this.child !== child)
1379
1379
  return;
1380
1380
  const error = new Error(`Electron utility runtime exited (${code}): ${this.lastError || 'no stderr'}`);
1381
+ // Surface unexpected worker death as a target-scoped terminal error. A
1382
+ // silent child exit used to leave the renderer with a generic interrupted
1383
+ // state and no explanation of whether the provider, runtime, or process
1384
+ // supervisor was responsible.
1385
+ if (code !== 0 && !this.restartQuarantine) {
1386
+ const event = {
1387
+ id: `utility-runtime-exit-${process.pid}-${Date.now()}`,
1388
+ conversationId: this.target.conversationId,
1389
+ type: 'error',
1390
+ content: error.message,
1391
+ mode: 'build',
1392
+ model: '',
1393
+ timestamp: new Date().toISOString(),
1394
+ workspaceId: this.target.workspaceId,
1395
+ workspaceKey: this.target.workspaceKey,
1396
+ runtimeKey: this.target.runtimeKey,
1397
+ status: 'error',
1398
+ };
1399
+ for (const listener of this.listeners)
1400
+ listener(event);
1401
+ }
1381
1402
  this.detachChild(child, error);
1382
1403
  }
1383
1404
  detachChild(child, error) {
@@ -9,6 +9,8 @@ export interface RuntimeLifecycleState {
9
9
  active: true;
10
10
  }
11
11
  export declare function isRuntimeProcessAlive(pid: number): boolean;
12
+ /** Prepare crash-recovery markers asynchronously after the startup shell is visible. */
13
+ export declare function prepareRuntimeLifecycle(root: string, role?: RuntimeLifecycleRole): Promise<void>;
12
14
  /**
13
15
  * Claim this process/role as the current runtime owner.
14
16
  *
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.isRuntimeProcessAlive = isRuntimeProcessAlive;
37
+ exports.prepareRuntimeLifecycle = prepareRuntimeLifecycle;
37
38
  exports.beginRuntimeLifecycle = beginRuntimeLifecycle;
38
39
  exports.markRuntimeLifecycleClean = markRuntimeLifecycleClean;
39
40
  exports.runtimeLifecycleState = runtimeLifecycleState;
@@ -41,6 +42,8 @@ const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const crypto_1 = require("crypto");
43
44
  const processStates = new Map();
45
+ const preparedPreviousStates = new Map();
46
+ const lifecyclePreparations = new Map();
44
47
  function stateKey(root, role) {
45
48
  return `${path.resolve(root)}\u0000${role}`;
46
49
  }
@@ -89,6 +92,50 @@ function readActiveStates(root, role) {
89
92
  return [];
90
93
  }
91
94
  }
95
+ /** Prepare crash-recovery markers asynchronously after the startup shell is visible. */
96
+ function prepareRuntimeLifecycle(root, role = 'main') {
97
+ const key = stateKey(root, role);
98
+ if (processStates.has(key) || preparedPreviousStates.has(key))
99
+ return Promise.resolve();
100
+ const current = lifecyclePreparations.get(key);
101
+ if (current)
102
+ return current.then(() => undefined);
103
+ const preparation = (async () => {
104
+ const directory = path.join(root, '.newmark-runtime');
105
+ let files;
106
+ try {
107
+ files = (await fs.promises.readdir(directory))
108
+ .filter(file => file.startsWith(`lifecycle-${role}`) && file.endsWith('.json'));
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ const active = [];
114
+ for (let offset = 0; offset < files.length; offset += 24) {
115
+ const states = await Promise.all(files.slice(offset, offset + 24).map(async (file) => {
116
+ const filePath = path.join(directory, file);
117
+ try {
118
+ const state = JSON.parse(await fs.promises.readFile(filePath, 'utf-8'));
119
+ if (state?.active === true)
120
+ return state;
121
+ }
122
+ catch {
123
+ // A corrupt marker cannot establish a live owner.
124
+ }
125
+ await fs.promises.unlink(filePath).catch(() => undefined);
126
+ return null;
127
+ }));
128
+ for (const state of states)
129
+ if (state)
130
+ active.push(state);
131
+ }
132
+ return active;
133
+ })();
134
+ lifecyclePreparations.set(key, preparation);
135
+ return preparation.then(states => {
136
+ preparedPreviousStates.set(key, states);
137
+ }).finally(() => lifecyclePreparations.delete(key));
138
+ }
92
139
  /**
93
140
  * Claim this process/role as the current runtime owner.
94
141
  *
@@ -101,7 +148,8 @@ function beginRuntimeLifecycle(root, role = 'main') {
101
148
  const existingProcessState = processStates.get(key);
102
149
  if (existingProcessState)
103
150
  return existingProcessState;
104
- const previousStates = readActiveStates(root, role);
151
+ const previousStates = preparedPreviousStates.get(key) || readActiveStates(root, role);
152
+ preparedPreviousStates.delete(key);
105
153
  const previousOwnerAlive = previousStates.some(previous => isRuntimeProcessAlive(Number(previous.pid)));
106
154
  const state = {
107
155
  role,
@@ -129,15 +177,16 @@ function markRuntimeLifecycleClean(root, role = 'main') {
129
177
  if (!current)
130
178
  return;
131
179
  try {
132
- writeState(root, role, {
133
- ...current,
134
- active: false,
135
- cleanExitAt: new Date().toISOString(),
136
- });
180
+ fs.unlinkSync(statePath(root, role, current.ownerId));
137
181
  }
138
182
  catch {
139
- // Leaving the marker active is safer than claiming a clean exit after a
140
- // failed durable write.
183
+ try {
184
+ writeState(root, role, { ...current, active: false, cleanExitAt: new Date().toISOString() });
185
+ }
186
+ catch {
187
+ // Leaving the marker active is safer than claiming a clean exit after a
188
+ // failed durable write.
189
+ }
141
190
  }
142
191
  }
143
192
  function runtimeLifecycleState(root, role = 'main') {
package/dist/main.js CHANGED
@@ -1803,12 +1803,10 @@ else {
1803
1803
  };
1804
1804
  let firstRunInitialized = false;
1805
1805
  if (!automationWakeMode) {
1806
- // Attempt one loads the real desktop immediately. The renderer keeps a
1807
- // startup cover above it until the main-process readiness acknowledgement,
1808
- // avoiding a startup.html -> index.html renderer navigation on the hot path.
1809
- startupAttempt = 1;
1810
- startupShellLoadedAt = Date.now();
1811
- startupWindow = createDesktopWindow(true, true, startupAttempt);
1806
+ // Keep Chromium's heavy index renderer off the event loop until the
1807
+ // lightweight startup shell is painted and durable recovery is ready.
1808
+ startupAttempt = 0;
1809
+ startupWindow = createDesktopWindow(false, true);
1812
1810
  mainWindow = startupWindow;
1813
1811
  createTray();
1814
1812
  }
@@ -1901,6 +1899,8 @@ else {
1901
1899
  recordStartup('first-run-init');
1902
1900
  }
1903
1901
  if (!agent) {
1902
+ await (0, runtimeLifecycle_1.prepareRuntimeLifecycle)(root, 'main');
1903
+ recordStartup('runtime-lifecycle-prepared');
1904
1904
  agent = new agent_1.Agent(root);
1905
1905
  mcpManager = new mcpManager_1.McpManager(root);
1906
1906
  activeAgentBackendMode = process.platform === 'win32' && agent.config.getBool('agent', 'run_in_wsl') ? 'wsl' : 'windows';
@@ -1545,6 +1545,7 @@ button.left-ws-item {
1545
1545
  font-size: 12px;
1546
1546
  }
1547
1547
  .md-rendered pre {
1548
+ position: relative;
1548
1549
  margin: 8px 0;
1549
1550
  padding: 10px 12px;
1550
1551
  overflow-x: auto;
@@ -2138,6 +2139,19 @@ button.left-ws-item {
2138
2139
  /* Opaque theme-tinted foundation below the transcript, rails and glass. */
2139
2140
  background-color: var(--chat-canvas);
2140
2141
  }
2142
+ .md-code-copy {
2143
+ position: absolute;
2144
+ top: 6px;
2145
+ right: 7px;
2146
+ border: 1px solid var(--glass-border-1);
2147
+ border-radius: 4px;
2148
+ background: var(--glass-bg-2);
2149
+ color: var(--text-dim);
2150
+ padding: 2px 6px;
2151
+ font-size: 10px;
2152
+ cursor: pointer;
2153
+ }
2154
+ .md-code-copy:hover { color: var(--text); }
2141
2155
 
2142
2156
  #chat-area {
2143
2157
  flex: 1;
@@ -2512,6 +2526,33 @@ button.left-ws-item {
2512
2526
  overflow: visible;
2513
2527
  }
2514
2528
 
2529
+ /* Build Block transcript is a plain timeline, never a glass surface. Keep
2530
+ this explicit and high-priority so broad chat/panel material rules cannot
2531
+ reintroduce a rounded white compositor plane around an expanded run. */
2532
+ .work-run-message,
2533
+ .conversation-work-run,
2534
+ .conversation-work-run-head,
2535
+ .conversation-work-run-body {
2536
+ background: transparent !important;
2537
+ background-image: none !important;
2538
+ backdrop-filter: none !important;
2539
+ -webkit-backdrop-filter: none !important;
2540
+ filter: none !important;
2541
+ border: 0 !important;
2542
+ border-radius: 0 !important;
2543
+ box-shadow: none !important;
2544
+ }
2545
+
2546
+ .work-run-message::before,
2547
+ .work-run-message::after,
2548
+ .conversation-work-run-head::before,
2549
+ .conversation-work-run-head::after,
2550
+ .conversation-work-run-body::before,
2551
+ .conversation-work-run-body::after {
2552
+ content: none !important;
2553
+ display: none !important;
2554
+ }
2555
+
2515
2556
  .conversation-work-run::before {
2516
2557
  content: "";
2517
2558
  position: absolute;
@@ -2589,8 +2630,8 @@ button.left-ws-item {
2589
2630
  .conversation-work-activity-list { display: flex; flex-direction: column; gap: 4px; margin: 4px 0 5px 24px; padding: 0; border: 0; }
2590
2631
  .conversation-work-activity-item { display: grid; grid-template-columns: 17px minmax(0,1fr); gap: 7px; align-items: start; min-height: 24px; color: var(--text-dim); font: 400 11px/1.5 var(--font-ui); }
2591
2632
  .conversation-work-activity-item .nm-icon { margin-top: 2px; }
2592
- .conversation-work-display-image { display:grid; gap:6px; width:min(100%, 640px); margin:7px 0 3px 24px; padding:0; border:0; background:transparent; color:var(--text-dim); text-align:left; cursor:pointer; }
2593
- .conversation-work-display-image img { display:block; width:100%; max-height:420px; object-fit:contain; object-position:left center; border:1px solid var(--glass-border-2); border-radius:var(--radius-md); background:rgba(0,0,0,.18); }
2633
+ .conversation-work-display-image { display:inline-grid; justify-items:start; gap:6px; width:fit-content !important; max-width:min(100%, 640px); margin:7px 0 3px 24px; padding:0; border:0; border-radius:0 !important; background:transparent; color:var(--text-dim); text-align:left; cursor:pointer; }
2634
+ .conversation-work-display-image img { display:block; width:auto; max-width:100%; height:auto; max-height:420px; object-fit:contain; object-position:left center; border:1px solid var(--glass-border-2); border-radius:var(--radius-sm); background:rgba(0,0,0,.18); }
2594
2635
  .conversation-work-display-image span { font:10px/1.4 var(--font-ui); }
2595
2636
  .conversation-work-display-image:hover span, .conversation-work-display-image:focus-visible span { color:var(--accent); }
2596
2637
  .work-run-collapsed-images { display:none; flex-direction:column; gap:8px; margin:0 34px 8px 0; }
@@ -4823,20 +4864,28 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4823
4864
  }
4824
4865
 
4825
4866
  .conversation-image-attachments {
4826
- display: grid;
4827
- grid-template-columns: repeat(auto-fill, minmax(132px, 190px));
4867
+ display: flex;
4868
+ flex-wrap: wrap;
4869
+ align-items: flex-start;
4870
+ justify-content: flex-start;
4828
4871
  gap: 8px;
4829
4872
  margin-top: 9px;
4830
4873
  }
4874
+ .chat-msg.user .conversation-image-attachments { justify-content: flex-end; }
4875
+ .chat-msg.assistant .conversation-image-attachments,
4876
+ .chat-msg.workflow .conversation-image-attachments { justify-content: flex-start; }
4831
4877
 
4832
4878
  .conversation-image-attachment {
4833
4879
  min-width: 0;
4834
- display: flex;
4880
+ width: fit-content !important;
4881
+ max-width: min(100%, 280px);
4882
+ display: inline-flex;
4835
4883
  flex-direction: column;
4884
+ align-items: flex-start;
4836
4885
  gap: 5px;
4837
4886
  padding: 6px;
4838
4887
  border: 1px solid var(--glass-border-2);
4839
- border-radius: var(--radius-md);
4888
+ border-radius: var(--radius-sm) !important;
4840
4889
  background: var(--glass-bg-1);
4841
4890
  color: var(--text);
4842
4891
  font: 11px/1.35 var(--font-ui);
@@ -4851,8 +4900,11 @@ textarea.mcp-input { min-height:76px; resize:vertical; font-family:var(--font-mo
4851
4900
  }
4852
4901
 
4853
4902
  .conversation-image-attachment img {
4854
- width: 100%;
4855
- height: 112px;
4903
+ display: block;
4904
+ width: auto;
4905
+ max-width: 100%;
4906
+ height: auto;
4907
+ max-height: 180px;
4856
4908
  border-radius: var(--radius-sm);
4857
4909
  object-fit: contain;
4858
4910
  background: rgba(0,0,0,.18);
@@ -8814,7 +8866,7 @@ function renderMarkdownBlocks(text) {
8814
8866
  var code = codeLines.join('\n');
8815
8867
  var normalizedLanguage = lang ? normalizeCodeLanguage(lang) : 'text';
8816
8868
  var highlighted = normalizedLanguage === 'text' ? esc(code) : highlightCodeByLanguage(code, normalizedLanguage);
8817
- html += '<pre><code' + (lang ? ' data-lang="' + escAttr(lang) + '"' : '') + ' class="md-code-block language-' + escAttr(normalizedLanguage) + '">' + highlighted + '</code></pre>';
8869
+ html += '<pre><button class="md-code-copy" type="button" title="' + escAttr(t('common.copy')) + '" data-code="' + escAttr(code) + '" onclick="window.copyMarkdownCode(this)">' + esc(t('common.copy')) + '</button><code' + (lang ? ' data-lang="' + escAttr(lang) + '"' : '') + ' class="md-code-block language-' + escAttr(normalizedLanguage) + '">' + highlighted + '</code></pre>';
8818
8870
  continue;
8819
8871
  }
8820
8872
  if (trimmed === '$$' || trimmed === '\\[') {
@@ -9891,6 +9943,19 @@ function workReviewRunIds(runId, aliases) {
9891
9943
  return ids;
9892
9944
  }
9893
9945
 
9946
+ window.copyMarkdownCode = async function(button) {
9947
+ var value = String(button && button.getAttribute('data-code') || '');
9948
+ try {
9949
+ if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(value);
9950
+ else {
9951
+ var helper = document.createElement('textarea');
9952
+ helper.value = value; helper.style.position = 'fixed'; helper.style.opacity = '0';
9953
+ document.body.appendChild(helper); helper.select(); document.execCommand('copy'); helper.remove();
9954
+ }
9955
+ if (button) { button.textContent = t('common.copied'); setTimeout(function() { button.textContent = t('common.copy'); }, 1200); }
9956
+ } catch (e) {}
9957
+ };
9958
+
9894
9959
  function upsertWorkReviewRecord(ui, diffs, runId, aliases) {
9895
9960
  if (!ui) return null;
9896
9961
  if (!Array.isArray(ui.workReviews)) ui.workReviews = [];
@@ -12359,6 +12424,33 @@ function replayActiveAgentWorkEvents() {
12359
12424
  }
12360
12425
 
12361
12426
  var _conversationsRenderPending = false;
12427
+ var _contextWindowRefreshTimer = null;
12428
+ var _contextWindowRefreshInFlight = false;
12429
+ var _contextWindowRefreshTargetKey = '';
12430
+
12431
+ function scheduleActiveContextWindowRefresh(target) {
12432
+ if (!api.getState || !target || !isActiveConversationTarget(target)) return;
12433
+ var targetKey = runtimeKeyFor(target.workspaceId, target.conversationId);
12434
+ _contextWindowRefreshTargetKey = targetKey;
12435
+ if (_contextWindowRefreshTimer !== null) return;
12436
+ _contextWindowRefreshTimer = window.setTimeout(function() {
12437
+ _contextWindowRefreshTimer = null;
12438
+ if (_contextWindowRefreshInFlight || !isActiveConversationTarget(target)) return;
12439
+ _contextWindowRefreshInFlight = true;
12440
+ api.getState(target).then(function(snapshot) {
12441
+ if (!snapshot || !isActiveConversationTarget(target) || _contextWindowRefreshTargetKey !== targetKey) return;
12442
+ if (snapshot.contextWindow !== undefined) {
12443
+ state.contextWindow = snapshot.contextWindow;
12444
+ window.renderContextWindow();
12445
+ }
12446
+ if (snapshot.contextCompression !== undefined) state.contextCompression = snapshot.contextCompression;
12447
+ if (state.rightTab === 'status') window.renderRightStatusPanel();
12448
+ }).catch(function(){}).then(function() {
12449
+ _contextWindowRefreshInFlight = false;
12450
+ });
12451
+ }, 120);
12452
+ }
12453
+
12362
12454
  function scheduleConversationsRender() {
12363
12455
  if (_conversationsRenderPending) return;
12364
12456
  _conversationsRenderPending = true;
@@ -12426,6 +12518,15 @@ function appendAgentWorkEvent(event) {
12426
12518
  if (active) {
12427
12519
  setWorking(!!runningConversationRecord(activeConversationId()) || currentFlowRunning());
12428
12520
  updateSubmitButtonState();
12521
+ // Refresh snapshots only at semantic boundaries. Text/thought deltas can
12522
+ // arrive hundreds of times per second; issuing IPC snapshots for each
12523
+ // delta creates avoidable renderer/runtime contention and used to make a
12524
+ // healthy utility runtime look like a mysteriously interrupted run.
12525
+ var refreshableLiveEvent = event.type === 'status'
12526
+ || event.type === 'tool_result'
12527
+ || event.type === 'usage'
12528
+ || event.type === 'message_end';
12529
+ if (refreshableLiveEvent) scheduleActiveContextWindowRefresh(target);
12429
12530
  }
12430
12531
  if (active && event.type === 'tool_result' && ['linked_plan', 'task_read', 'task_create', 'task', 'subagent_create', 'SubAgent', 'subagent_send', 'subagent_read', 'subagent_close'].indexOf(String(event.toolName || '')) >= 0 && api.getState) {
12431
12532
  var eventWorkspaceKey = currentWorkspaceKey();
@@ -12441,6 +12542,12 @@ function appendAgentWorkEvent(event) {
12441
12542
  var terminalWorkspaceKey = currentWorkspaceKey();
12442
12543
  api.getState(target).then(function(snapshot) {
12443
12544
  if (!isActiveConversationTarget(target) || terminalWorkspaceKey !== currentWorkspaceKey()) return;
12545
+ if (snapshot && snapshot.contextWindow !== undefined) {
12546
+ state.contextWindow = snapshot.contextWindow;
12547
+ window.renderContextWindow();
12548
+ }
12549
+ if (snapshot && snapshot.contextCompression !== undefined) state.contextCompression = snapshot.contextCompression;
12550
+ if (state.rightTab === 'status') window.renderRightStatusPanel();
12444
12551
  // Backend-owned Goal continuations have no renderer send promise to
12445
12552
  // return the final snapshot. Refresh on the terminal work event so a
12446
12553
  // completed Goal clears its bar and restores Build immediately.
@@ -21564,6 +21671,18 @@ function initializeBrowserGuest(view) {
21564
21671
  addMsg('assistant', '[Browser] Load failed: ' + e.errorDescription, 'error', '');
21565
21672
  }
21566
21673
  });
21674
+ // Keep target=_blank/window.open inside the embedded browser. Electron's
21675
+ // webview otherwise creates an external window that has no address bar or
21676
+ // reload control, so a popup page could not be refreshed in-place.
21677
+ view.addEventListener('new-window', function(e) {
21678
+ var popupUrl = String(e && e.url || '').trim();
21679
+ if (!popupUrl || !/^(?:https?:|about:blank|newmark-preview:)/i.test(popupUrl)) return;
21680
+ if (e && typeof e.preventDefault === 'function') e.preventDefault();
21681
+ rememberBrowserUrl(popupUrl, true, target);
21682
+ var input = els['browser-url'];
21683
+ if (input && isActiveConversationTarget(target)) input.value = popupUrl;
21684
+ navigateBrowserView(view, popupUrl);
21685
+ });
21567
21686
  var current = '';
21568
21687
  try { current = view.getURL ? view.getURL() : ''; } catch { current = ''; }
21569
21688
  if (!current && !view.getAttribute('src')) view.setAttribute('src', browserRetainedUrls[targetKey] || 'about:blank');
@@ -8383,7 +8383,7 @@ var require_createWorker = __commonJS({
8383
8383
  ...defaultOptions,
8384
8384
  ..._options
8385
8385
  });
8386
- const promises2 = {};
8386
+ const promises3 = {};
8387
8387
  const currentLangs = typeof langs === "string" ? langs.split("+") : langs;
8388
8388
  let currentOem = oem;
8389
8389
  let currentConfig = config;
@@ -8403,7 +8403,7 @@ var require_createWorker = __commonJS({
8403
8403
  const startJob = ({ id: jobId, action, payload }) => new Promise((resolve16, reject) => {
8404
8404
  log2(`[${id}]: Start ${jobId}, action=${action}`);
8405
8405
  const promiseId = `${action}-${jobId}`;
8406
- promises2[promiseId] = { resolve: resolve16, reject };
8406
+ promises3[promiseId] = { resolve: resolve16, reject };
8407
8407
  send(worker, {
8408
8408
  workerId: id,
8409
8409
  jobId,
@@ -8508,11 +8508,11 @@ var require_createWorker = __commonJS({
8508
8508
  const promiseId = `${action}-${jobId}`;
8509
8509
  if (status === "resolve") {
8510
8510
  log2(`[${workerId}]: Complete ${jobId}`);
8511
- promises2[promiseId].resolve({ jobId, data });
8512
- delete promises2[promiseId];
8511
+ promises3[promiseId].resolve({ jobId, data });
8512
+ delete promises3[promiseId];
8513
8513
  } else if (status === "reject") {
8514
- promises2[promiseId].reject(data);
8515
- delete promises2[promiseId];
8514
+ promises3[promiseId].reject(data);
8515
+ delete promises3[promiseId];
8516
8516
  if (action === "load") workerResReject(data);
8517
8517
  if (errorHandler) {
8518
8518
  errorHandler(data);
@@ -54006,18 +54006,18 @@ var require_proxy_agent = __commonJS({
54006
54006
  }
54007
54007
  }
54008
54008
  [kClose]() {
54009
- const promises2 = [this[kAgent].close()];
54009
+ const promises3 = [this[kAgent].close()];
54010
54010
  if (this[kClient]) {
54011
- promises2.push(this[kClient].close());
54011
+ promises3.push(this[kClient].close());
54012
54012
  }
54013
- return Promise.all(promises2);
54013
+ return Promise.all(promises3);
54014
54014
  }
54015
54015
  [kDestroy]() {
54016
- const promises2 = [this[kAgent].destroy()];
54016
+ const promises3 = [this[kAgent].destroy()];
54017
54017
  if (this[kClient]) {
54018
- promises2.push(this[kClient].destroy());
54018
+ promises3.push(this[kClient].destroy());
54019
54019
  }
54020
- return Promise.all(promises2);
54020
+ return Promise.all(promises3);
54021
54021
  }
54022
54022
  };
54023
54023
  function buildHeaders(headers) {
@@ -344927,6 +344927,7 @@ var fs24 = __toESM(require("fs"));
344927
344927
  var path27 = __toESM(require("path"));
344928
344928
  var import_crypto14 = require("crypto");
344929
344929
  var processStates = /* @__PURE__ */ new Map();
344930
+ var preparedPreviousStates = /* @__PURE__ */ new Map();
344930
344931
  function stateKey(root2, role) {
344931
344932
  return `${path27.resolve(root2)}\0${role}`;
344932
344933
  }
@@ -344972,7 +344973,8 @@ function beginRuntimeLifecycle(root2, role = "main") {
344972
344973
  const key3 = stateKey(root2, role);
344973
344974
  const existingProcessState = processStates.get(key3);
344974
344975
  if (existingProcessState) return existingProcessState;
344975
- const previousStates = readActiveStates(root2, role);
344976
+ const previousStates = preparedPreviousStates.get(key3) || readActiveStates(root2, role);
344977
+ preparedPreviousStates.delete(key3);
344976
344978
  const previousOwnerAlive = previousStates.some((previous) => isRuntimeProcessAlive(Number(previous.pid)));
344977
344979
  const state = {
344978
344980
  role,
@@ -344995,12 +344997,12 @@ function markRuntimeLifecycleClean(root2, role = "main") {
344995
344997
  const current = processStates.get(key3);
344996
344998
  if (!current) return;
344997
344999
  try {
344998
- writeState(root2, role, {
344999
- ...current,
345000
- active: false,
345001
- cleanExitAt: (/* @__PURE__ */ new Date()).toISOString()
345002
- });
345000
+ fs24.unlinkSync(statePath(root2, role, current.ownerId));
345003
345001
  } catch {
345002
+ try {
345003
+ writeState(root2, role, { ...current, active: false, cleanExitAt: (/* @__PURE__ */ new Date()).toISOString() });
345004
+ } catch {
345005
+ }
345004
345006
  }
345005
345007
  }
345006
345008
 
@@ -348526,12 +348528,15 @@ Conversation title (a few words):`;
348526
348528
  defaultInputMode() {
348527
348529
  return this.config.getStr("general", "default_input") === "next" ? "next" : "guide";
348528
348530
  }
348529
- abortActiveKernelRun() {
348531
+ abortActiveKernelRun(reason = "unspecified") {
348530
348532
  let aborted = false;
348531
348533
  this.subagents.pauseScheduling();
348532
348534
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
348533
348535
  const abortError5 = new Error("Agent run aborted");
348534
348536
  abortError5.name = "AbortError";
348537
+ if (process.env.NEWMARK_PROVIDER_DIAGNOSTICS === "1") {
348538
+ console.error(`[NewmarkKernel] abort-request reason=${String(reason).slice(0, 160)} conversation=${this.activeConversationId}`);
348539
+ }
348535
348540
  this.activeProcessAbortController.abort(abortError5);
348536
348541
  aborted = true;
348537
348542
  }
@@ -348540,7 +348545,7 @@ Conversation title (a few words):`;
348540
348545
  aborted = true;
348541
348546
  }
348542
348547
  for (const peer of this.activePeerAgents.values()) {
348543
- aborted = peer.abortActiveKernelRun() || aborted;
348548
+ aborted = peer.abortActiveKernelRun(reason) || aborted;
348544
348549
  }
348545
348550
  this.pendingAgentKernelQueue = [];
348546
348551
  return aborted;
@@ -350047,10 +350052,16 @@ ${summary}`, segment, "local-summarize", true);
350047
350052
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
350048
350053
  }
350049
350054
  compactHistoricalImages(messages) {
350055
+ const continuationIndex = messages.reduce((found, message, index) => {
350056
+ const content = String(message?.content || "");
350057
+ return content.includes("[Post-Compression Task Continuation]") ? index : found;
350058
+ }, -1);
350050
350059
  const lastParts = Array.isArray(messages.at(-1)?.content) ? messages.at(-1).content : [];
350051
- const newestImageMessage = lastParts.some((part) => part?.type === "image_url") ? messages.length - 1 : -1;
350060
+ const newestImageMessage = continuationIndex < 0 && lastParts.some((part) => part?.type === "image_url") ? messages.length - 1 : -1;
350052
350061
  return messages.map((message, index) => {
350053
- if (!Array.isArray(message.content) || index === newestImageMessage) return { ...message };
350062
+ if (!Array.isArray(message.content)) return { ...message };
350063
+ const preserveRecentImage = continuationIndex >= 0 && index > continuationIndex;
350064
+ if (preserveRecentImage || index === newestImageMessage) return { ...message };
350054
350065
  const parts = message.content.flatMap((part) => {
350055
350066
  if (part?.type !== "image_url") return [{ ...part }];
350056
350067
  return [{ type: "text", text: "[Historical image attachment omitted after context compression.]" }];
@@ -352974,12 +352985,16 @@ ${text.slice(-tailChars).trimStart()}`;
352974
352985
  }
352975
352986
  buildSystemPrompt() {
352976
352987
  const cwd = this.workspace.current?.path || this.rootPath;
352988
+ const newmarkConfigRoot = path28.resolve(this.rootPath);
352989
+ const newmarkConfigFile = path28.join(newmarkConfigRoot, "config.json");
352977
352990
  const enabledSkills = this.skills.active();
352978
352991
  const globalPromptPath = path28.join(this.rootPath, "agent.md");
352979
352992
  const globalPrompt = normalizeInjectedPrompt(fs25.existsSync(globalPromptPath) ? fs25.readFileSync(globalPromptPath, "utf-8") : "");
352980
352993
  const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
352981
352994
  const identity = JSON.stringify({
352982
352995
  cwd,
352996
+ newmarkConfigRoot,
352997
+ newmarkConfigFile,
352983
352998
  mode: this.mode,
352984
352999
  conversationId: this.activeConversationId,
352985
353000
  subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
@@ -352998,6 +353013,9 @@ ${text.slice(-tailChars).trimStart()}`;
352998
353013
  if (this.systemPromptCache?.identity === identity) return this.systemPromptCache.value;
352999
353014
  const parts = [`${CORE_SYSTEM_PROMPT}
353000
353015
 
353016
+ ## Newmark Runtime Configuration
353017
+ Newmark's own user-level configuration and runtime state are stored by default under ${newmarkConfigRoot} (the conventional user path is user/.Newmark). The primary configuration file is ${newmarkConfigFile}; provider credentials, conversation state, caches, Work/Flow data, Memory Lab, skills, and archives may also live beneath this root. Treat these files as Newmark internal state: read or modify them only when the user explicitly asks, never expose API keys or tokens, and do not confuse this runtime root with the active workspace.
353018
+
353001
353019
  ## Current Working Directory
353002
353020
  ${cwd}
353003
353021
 
@@ -353743,7 +353761,12 @@ var ConversationKernel = class {
353743
353761
  ...images?.length ? { images } : {},
353744
353762
  ...attachments?.length ? { attachments } : {},
353745
353763
  ...visible ? { visibleUserInput: visible } : {},
353746
- ...visibleMode ? { visibleMode } : {}
353764
+ ...visibleMode ? { visibleMode } : {},
353765
+ // A normal user follow-up must never inherit the identity metadata that
353766
+ // was only needed while it lived in the runtime queue. In particular,
353767
+ // do not let a stale/forwarded hiddenUserInput flag classify it as an
353768
+ // Agent-generated continuation when Agent.process persists the turn.
353769
+ hiddenUserInput: message.hiddenUserInput === true && !message.clientMessageId
353747
353770
  };
353748
353771
  }
353749
353772
  queueItems(target) {
@@ -354316,7 +354339,7 @@ ${item.goalObjective}` : item.text,
354316
354339
  this.deferOutstandingGuides(runtime);
354317
354340
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
354318
354341
  runtime.stopCheckpointed = checkpointed;
354319
- runtime.runner.abortActiveKernelRun();
354342
+ runtime.runner.abortActiveKernelRun("user_stop");
354320
354343
  runtime.runner.emitWorkEvent({
354321
354344
  type: "status",
354322
354345
  content: "Stop requested. Saving progress and interrupting this conversation.",
@@ -354450,6 +354473,7 @@ ${item.goalObjective}` : item.text,
354450
354473
  async run(runtime, message, options) {
354451
354474
  this.applyOptions(runtime.runner, options);
354452
354475
  let lastTokens = await this.runSingle(runtime, message);
354476
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
354453
354477
  if (runtime.stopRequestedRunId === runtime.runId) {
354454
354478
  this.mirrorHostIfTargetActive(runtime);
354455
354479
  return this.result(runtime, lastTokens);
@@ -354476,6 +354500,7 @@ ${item.goalObjective}` : item.text,
354476
354500
  }
354477
354501
  if (batchGuides.length === 1) {
354478
354502
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354503
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode)) break;
354479
354504
  continue;
354480
354505
  }
354481
354506
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join("\n");
@@ -354486,8 +354511,11 @@ ${batchText}`,
354486
354511
  batchGuides
354487
354512
  };
354488
354513
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354514
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, "steer")) break;
354489
354515
  } else {
354490
- lastTokens = await this.runSingle(runtime, this.drainQueuedFollowUpMessage(next.message), next.queueMode);
354516
+ const drained = this.drainQueuedFollowUpMessage(next.message);
354517
+ lastTokens = await this.runSingle(runtime, drained, next.queueMode);
354518
+ if (this.repeatedAutomaticAssistant(runtime, drained, next.queueMode)) break;
354491
354519
  }
354492
354520
  }
354493
354521
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354522,6 +354550,35 @@ Review this persisted peer result and summarize or continue the parent task as n
354522
354550
  this.mirrorHostIfTargetActive(runtime);
354523
354551
  return this.result(runtime, lastTokens);
354524
354552
  }
354553
+ automaticMessage(message, queueMode) {
354554
+ return queueMode === "steer" || typeof message !== "string" && (message.hiddenUserInput === true || message.goalContinuation === true || !!message.batchGuides?.length);
354555
+ }
354556
+ currentAssistantFingerprint(runtime) {
354557
+ const messages = runtime.runner.chatMessages || [];
354558
+ const last = messages[messages.length - 1];
354559
+ if (!last || last.role !== "assistant") return "";
354560
+ return String(last.content || "").trim().replace(/\s+/g, " ").toLowerCase().slice(0, 4e3);
354561
+ }
354562
+ rememberAutomaticAssistantFingerprint(runtime, message, queueMode) {
354563
+ if (this.automaticMessage(message, queueMode)) runtime.lastAutomaticAssistantFingerprint = this.currentAssistantFingerprint(runtime) || void 0;
354564
+ }
354565
+ repeatedAutomaticAssistant(runtime, message, queueMode) {
354566
+ if (!this.automaticMessage(message, queueMode)) return false;
354567
+ const fingerprint2 = this.currentAssistantFingerprint(runtime);
354568
+ if (!fingerprint2) return false;
354569
+ if (runtime.lastAutomaticAssistantFingerprint === fingerprint2) {
354570
+ runtime.pendingNextTurn = runtime.pendingNextTurn.filter((item) => {
354571
+ const automatic = item.queueMode === "steer" || typeof item.message !== "string" && item.message.hiddenUserInput === true;
354572
+ if (automatic) runtime.runner.consumeConversationContinuation({ content: typeof item.message === "string" ? item.message : item.message.text, queueMode: item.queueMode, clientMessageId: typeof item.message === "string" ? void 0 : item.message.clientMessageId });
354573
+ return !automatic;
354574
+ });
354575
+ runtime.runner.recordWorkStatus("Automatic continuation stopped after a repeated assistant response.");
354576
+ this.emitQueueUpdate(runtime);
354577
+ return true;
354578
+ }
354579
+ runtime.lastAutomaticAssistantFingerprint = fingerprint2;
354580
+ return false;
354581
+ }
354525
354582
  /**
354526
354583
  * Apply a model selection recorded while a Build block was running. The
354527
354584
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -354561,7 +354618,16 @@ Review this persisted peer result and summarize or continue the parent task as n
354561
354618
  tokens = await Promise.race([
354562
354619
  runtime.runner.process(message),
354563
354620
  new Promise((_3, reject) => {
354564
- timeout = setTimeout(() => reject(new Error(`Process timeout (${Math.round(timeoutMs / 1e3)}s)`)), timeoutMs);
354621
+ timeout = setTimeout(() => {
354622
+ runtime.runner.abortActiveKernelRun(`process_timeout_${timeoutMs}ms`);
354623
+ runtime.runner.emitWorkEvent({
354624
+ type: "error",
354625
+ content: `Process timeout (${Math.round(timeoutMs / 1e3)}s); the run was aborted to prevent a stale worker from continuing.`,
354626
+ status: "error",
354627
+ runId: runtime.runId
354628
+ });
354629
+ reject(new Error(`Process timeout (${Math.round(timeoutMs / 1e3)}s); run aborted and checkpointed`));
354630
+ }, timeoutMs);
354565
354631
  })
354566
354632
  ]);
354567
354633
  } catch (error) {
@@ -354622,7 +354688,8 @@ Review this persisted peer result and summarize or continue the parent task as n
354622
354688
  guideReceipts: /* @__PURE__ */ new Map(),
354623
354689
  guideEnvelopes: /* @__PURE__ */ new Map(),
354624
354690
  goalContinuationTimer: void 0,
354625
- pendingContinuationRunId: void 0
354691
+ pendingContinuationRunId: void 0,
354692
+ lastAutomaticAssistantFingerprint: void 0
354626
354693
  };
354627
354694
  runner.setGoalContinuationGate(() => {
354628
354695
  this.queueState(runtime);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newmark-agent",
3
3
  "productName": "Newmark Agent",
4
- "version": "0.5.10",
4
+ "version": "0.5.12",
5
5
  "description": "Newmark Agent — Portable AI coding agent with rich GUI and CLI (TypeScript)",
6
6
  "homepage": "https://github.com/positer/Newmark-Agent",
7
7
  "repository": {
@@ -82,6 +82,7 @@
82
82
  "test:tool-process": "npm run build && node dist/tests/toolProcessVerify.js",
83
83
  "test:real-tool-concurrency": "npm run build && node dist/tests/realProviderToolConcurrencyStress.js",
84
84
  "test:startup-prewarm": "npm run build && node dist/tests/startupPrewarmVerify.js",
85
+ "test:startup-responsiveness": "npm run build && node dist/tests/runtimeLifecycleStartupVerify.js && node dist/tests/startupPrewarmVerify.js && node scripts/measure-startup-responsiveness.cjs",
85
86
  "test:browser-use-electron": "npm run build && electron dist/tests/browserUseElectronVerify.js",
86
87
  "test:dev009": "npm run build && node dist/tests/dev009Verify.js && node dist/tests/runtimeIsolationVerify.js && node dist/tests/guideWorkRunVerify.js && node dist/tests/guideUiReconcileVerify.js && node dist/tests/queueAttachmentIsolationVerify.js && node dist/tests/workspaceMenuVerify.js && node dist/tests/userImagePersistenceVerify.js && node dist/tests/visualPreferencesVerify.js && node dist/tests/browserUseVerify.js && node dist/tests/toolProcessVerify.js && node dist/tests/startupPrewarmVerify.js && node dist/tests/pdfPreviewServerVerify.js && node dist/tests/editorLifecycleVerify.js && node dist/tests/computerUsePerformanceVerify.js && electron dist/tests/browserUseElectronVerify.js",
87
88
  "test:dev008": "npm run build && node dist/tests/dev008-subagent.js",