newmark-agent 0.5.11 → 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.
@@ -348524,12 +348524,15 @@ Conversation title (a few words):`;
348524
348524
  defaultInputMode() {
348525
348525
  return this.config.getStr("general", "default_input") === "next" ? "next" : "guide";
348526
348526
  }
348527
- abortActiveKernelRun() {
348527
+ abortActiveKernelRun(reason = "unspecified") {
348528
348528
  let aborted = false;
348529
348529
  this.subagents.pauseScheduling();
348530
348530
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
348531
348531
  const abortError5 = new Error("Agent run aborted");
348532
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
+ }
348533
348536
  this.activeProcessAbortController.abort(abortError5);
348534
348537
  aborted = true;
348535
348538
  }
@@ -348538,7 +348541,7 @@ Conversation title (a few words):`;
348538
348541
  aborted = true;
348539
348542
  }
348540
348543
  for (const peer of this.activePeerAgents.values()) {
348541
- aborted = peer.abortActiveKernelRun() || aborted;
348544
+ aborted = peer.abortActiveKernelRun(reason) || aborted;
348542
348545
  }
348543
348546
  this.pendingAgentKernelQueue = [];
348544
348547
  return aborted;
@@ -350045,10 +350048,16 @@ ${summary}`, segment, "local-summarize", true);
350045
350048
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
350046
350049
  }
350047
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);
350048
350055
  const lastParts = Array.isArray(messages.at(-1)?.content) ? messages.at(-1).content : [];
350049
- 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;
350050
350057
  return messages.map((message, index) => {
350051
- 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 };
350052
350061
  const parts = message.content.flatMap((part) => {
350053
350062
  if (part?.type !== "image_url") return [{ ...part }];
350054
350063
  return [{ type: "text", text: "[Historical image attachment omitted after context compression.]" }];
@@ -352972,12 +352981,16 @@ ${text.slice(-tailChars).trimStart()}`;
352972
352981
  }
352973
352982
  buildSystemPrompt() {
352974
352983
  const cwd = this.workspace.current?.path || this.rootPath;
352984
+ const newmarkConfigRoot = path28.resolve(this.rootPath);
352985
+ const newmarkConfigFile = path28.join(newmarkConfigRoot, "config.json");
352975
352986
  const enabledSkills = this.skills.active();
352976
352987
  const globalPromptPath = path28.join(this.rootPath, "agent.md");
352977
352988
  const globalPrompt = normalizeInjectedPrompt(fs25.existsSync(globalPromptPath) ? fs25.readFileSync(globalPromptPath, "utf-8") : "");
352978
352989
  const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
352979
352990
  const identity = JSON.stringify({
352980
352991
  cwd,
352992
+ newmarkConfigRoot,
352993
+ newmarkConfigFile,
352981
352994
  mode: this.mode,
352982
352995
  conversationId: this.activeConversationId,
352983
352996
  subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
@@ -352996,6 +353009,9 @@ ${text.slice(-tailChars).trimStart()}`;
352996
353009
  if (this.systemPromptCache?.identity === identity) return this.systemPromptCache.value;
352997
353010
  const parts = [`${CORE_SYSTEM_PROMPT}
352998
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
+
352999
353015
  ## Current Working Directory
353000
353016
  ${cwd}
353001
353017
 
@@ -353741,7 +353757,12 @@ var ConversationKernel = class {
353741
353757
  ...images?.length ? { images } : {},
353742
353758
  ...attachments?.length ? { attachments } : {},
353743
353759
  ...visible ? { visibleUserInput: visible } : {},
353744
- ...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
353745
353766
  };
353746
353767
  }
353747
353768
  queueItems(target) {
@@ -354314,7 +354335,7 @@ ${item.goalObjective}` : item.text,
354314
354335
  this.deferOutstandingGuides(runtime);
354315
354336
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
354316
354337
  runtime.stopCheckpointed = checkpointed;
354317
- runtime.runner.abortActiveKernelRun();
354338
+ runtime.runner.abortActiveKernelRun("user_stop");
354318
354339
  runtime.runner.emitWorkEvent({
354319
354340
  type: "status",
354320
354341
  content: "Stop requested. Saving progress and interrupting this conversation.",
@@ -354448,6 +354469,7 @@ ${item.goalObjective}` : item.text,
354448
354469
  async run(runtime, message, options) {
354449
354470
  this.applyOptions(runtime.runner, options);
354450
354471
  let lastTokens = await this.runSingle(runtime, message);
354472
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
354451
354473
  if (runtime.stopRequestedRunId === runtime.runId) {
354452
354474
  this.mirrorHostIfTargetActive(runtime);
354453
354475
  return this.result(runtime, lastTokens);
@@ -354474,6 +354496,7 @@ ${item.goalObjective}` : item.text,
354474
354496
  }
354475
354497
  if (batchGuides.length === 1) {
354476
354498
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354499
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode)) break;
354477
354500
  continue;
354478
354501
  }
354479
354502
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join("\n");
@@ -354484,8 +354507,11 @@ ${batchText}`,
354484
354507
  batchGuides
354485
354508
  };
354486
354509
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354510
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, "steer")) break;
354487
354511
  } else {
354488
- 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;
354489
354515
  }
354490
354516
  }
354491
354517
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354520,6 +354546,35 @@ Review this persisted peer result and summarize or continue the parent task as n
354520
354546
  this.mirrorHostIfTargetActive(runtime);
354521
354547
  return this.result(runtime, lastTokens);
354522
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
+ }
354523
354578
  /**
354524
354579
  * Apply a model selection recorded while a Build block was running. The
354525
354580
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -354559,7 +354614,16 @@ Review this persisted peer result and summarize or continue the parent task as n
354559
354614
  tokens = await Promise.race([
354560
354615
  runtime.runner.process(message),
354561
354616
  new Promise((_3, reject) => {
354562
- 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);
354563
354627
  })
354564
354628
  ]);
354565
354629
  } catch (error) {
@@ -354620,7 +354684,8 @@ Review this persisted peer result and summarize or continue the parent task as n
354620
354684
  guideReceipts: /* @__PURE__ */ new Map(),
354621
354685
  guideEnvelopes: /* @__PURE__ */ new Map(),
354622
354686
  goalContinuationTimer: void 0,
354623
- pendingContinuationRunId: void 0
354687
+ pendingContinuationRunId: void 0,
354688
+ lastAutomaticAssistantFingerprint: void 0
354624
354689
  };
354625
354690
  runner.setGoalContinuationGate(() => {
354626
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) {
@@ -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');
@@ -348528,12 +348528,15 @@ Conversation title (a few words):`;
348528
348528
  defaultInputMode() {
348529
348529
  return this.config.getStr("general", "default_input") === "next" ? "next" : "guide";
348530
348530
  }
348531
- abortActiveKernelRun() {
348531
+ abortActiveKernelRun(reason = "unspecified") {
348532
348532
  let aborted = false;
348533
348533
  this.subagents.pauseScheduling();
348534
348534
  if (this.activeProcessAbortController && !this.activeProcessAbortController.signal.aborted) {
348535
348535
  const abortError5 = new Error("Agent run aborted");
348536
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
+ }
348537
348540
  this.activeProcessAbortController.abort(abortError5);
348538
348541
  aborted = true;
348539
348542
  }
@@ -348542,7 +348545,7 @@ Conversation title (a few words):`;
348542
348545
  aborted = true;
348543
348546
  }
348544
348547
  for (const peer of this.activePeerAgents.values()) {
348545
- aborted = peer.abortActiveKernelRun() || aborted;
348548
+ aborted = peer.abortActiveKernelRun(reason) || aborted;
348546
348549
  }
348547
348550
  this.pendingAgentKernelQueue = [];
348548
348551
  return aborted;
@@ -350049,10 +350052,16 @@ ${summary}`, segment, "local-summarize", true);
350049
350052
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
350050
350053
  }
350051
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);
350052
350059
  const lastParts = Array.isArray(messages.at(-1)?.content) ? messages.at(-1).content : [];
350053
- 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;
350054
350061
  return messages.map((message, index) => {
350055
- 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 };
350056
350065
  const parts = message.content.flatMap((part) => {
350057
350066
  if (part?.type !== "image_url") return [{ ...part }];
350058
350067
  return [{ type: "text", text: "[Historical image attachment omitted after context compression.]" }];
@@ -352976,12 +352985,16 @@ ${text.slice(-tailChars).trimStart()}`;
352976
352985
  }
352977
352986
  buildSystemPrompt() {
352978
352987
  const cwd = this.workspace.current?.path || this.rootPath;
352988
+ const newmarkConfigRoot = path28.resolve(this.rootPath);
352989
+ const newmarkConfigFile = path28.join(newmarkConfigRoot, "config.json");
352979
352990
  const enabledSkills = this.skills.active();
352980
352991
  const globalPromptPath = path28.join(this.rootPath, "agent.md");
352981
352992
  const globalPrompt = normalizeInjectedPrompt(fs25.existsSync(globalPromptPath) ? fs25.readFileSync(globalPromptPath, "utf-8") : "");
352982
352993
  const workspacePrompt = normalizeInjectedPrompt(this.workspace.currentAgentPrompt());
352983
352994
  const identity = JSON.stringify({
352984
352995
  cwd,
352996
+ newmarkConfigRoot,
352997
+ newmarkConfigFile,
352985
352998
  mode: this.mode,
352986
352999
  conversationId: this.activeConversationId,
352987
353000
  subagent: this.isSubagentRuntime ? [this.subagentName, this.subagentPrompt] : null,
@@ -353000,6 +353013,9 @@ ${text.slice(-tailChars).trimStart()}`;
353000
353013
  if (this.systemPromptCache?.identity === identity) return this.systemPromptCache.value;
353001
353014
  const parts = [`${CORE_SYSTEM_PROMPT}
353002
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
+
353003
353019
  ## Current Working Directory
353004
353020
  ${cwd}
353005
353021
 
@@ -353745,7 +353761,12 @@ var ConversationKernel = class {
353745
353761
  ...images?.length ? { images } : {},
353746
353762
  ...attachments?.length ? { attachments } : {},
353747
353763
  ...visible ? { visibleUserInput: visible } : {},
353748
- ...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
353749
353770
  };
353750
353771
  }
353751
353772
  queueItems(target) {
@@ -354318,7 +354339,7 @@ ${item.goalObjective}` : item.text,
354318
354339
  this.deferOutstandingGuides(runtime);
354319
354340
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
354320
354341
  runtime.stopCheckpointed = checkpointed;
354321
- runtime.runner.abortActiveKernelRun();
354342
+ runtime.runner.abortActiveKernelRun("user_stop");
354322
354343
  runtime.runner.emitWorkEvent({
354323
354344
  type: "status",
354324
354345
  content: "Stop requested. Saving progress and interrupting this conversation.",
@@ -354452,6 +354473,7 @@ ${item.goalObjective}` : item.text,
354452
354473
  async run(runtime, message, options) {
354453
354474
  this.applyOptions(runtime.runner, options);
354454
354475
  let lastTokens = await this.runSingle(runtime, message);
354476
+ this.rememberAutomaticAssistantFingerprint(runtime, message);
354455
354477
  if (runtime.stopRequestedRunId === runtime.runId) {
354456
354478
  this.mirrorHostIfTargetActive(runtime);
354457
354479
  return this.result(runtime, lastTokens);
@@ -354478,6 +354500,7 @@ ${item.goalObjective}` : item.text,
354478
354500
  }
354479
354501
  if (batchGuides.length === 1) {
354480
354502
  lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
354503
+ if (this.repeatedAutomaticAssistant(runtime, next.message, next.queueMode)) break;
354481
354504
  continue;
354482
354505
  }
354483
354506
  const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join("\n");
@@ -354488,8 +354511,11 @@ ${batchText}`,
354488
354511
  batchGuides
354489
354512
  };
354490
354513
  lastTokens = await this.runSingle(runtime, batchMessage, "steer");
354514
+ if (this.repeatedAutomaticAssistant(runtime, batchMessage, "steer")) break;
354491
354515
  } else {
354492
- 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;
354493
354519
  }
354494
354520
  }
354495
354521
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
@@ -354524,6 +354550,35 @@ Review this persisted peer result and summarize or continue the parent task as n
354524
354550
  this.mirrorHostIfTargetActive(runtime);
354525
354551
  return this.result(runtime, lastTokens);
354526
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
+ }
354527
354582
  /**
354528
354583
  * Apply a model selection recorded while a Build block was running. The
354529
354584
  * in-flight block never switches mid-block; the switch takes effect the next
@@ -354563,7 +354618,16 @@ Review this persisted peer result and summarize or continue the parent task as n
354563
354618
  tokens = await Promise.race([
354564
354619
  runtime.runner.process(message),
354565
354620
  new Promise((_3, reject) => {
354566
- 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);
354567
354631
  })
354568
354632
  ]);
354569
354633
  } catch (error) {
@@ -354624,7 +354688,8 @@ Review this persisted peer result and summarize or continue the parent task as n
354624
354688
  guideReceipts: /* @__PURE__ */ new Map(),
354625
354689
  guideEnvelopes: /* @__PURE__ */ new Map(),
354626
354690
  goalContinuationTimer: void 0,
354627
- pendingContinuationRunId: void 0
354691
+ pendingContinuationRunId: void 0,
354692
+ lastAutomaticAssistantFingerprint: void 0
354628
354693
  };
354629
354694
  runner.setGoalContinuationGate(() => {
354630
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.11",
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": {