newmark-agent 0.3.6 → 0.3.8

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.
@@ -327015,6 +327015,8 @@ var NATIVE_TOOL_CATALOG = [
327015
327015
  { name: "subagent_close", label: "Subagent close", description: "Close a same-conversation peer agent.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327016
327016
  { name: "linked_plan", label: "Linked plan", description: "Read or conservatively update the conversation-linked Markdown plan.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327017
327017
  { name: "build_history_query", label: "Build history query", description: "Read concrete public work details for one historical Build Block.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327018
+ { name: "context_compress", label: "Context compress", description: "Actively compress the LLM context history, leaving the displayed conversation history unchanged.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327019
+ { name: "context_history_manage", label: "Context history manage", description: "List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327018
327020
  { name: "question", label: "Ask question", description: "Ask the user for structured option feedback.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327019
327021
  { name: "skill_download", label: "Skill download", description: "Download and install a skill.", category: "agent", defaultEnabled: true },
327020
327022
  { name: "skill", label: "Skill", description: "Search enabled skill metadata or load one skill body on demand.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
@@ -327821,6 +327823,8 @@ function defaultConfig() {
327821
327823
  auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
327822
327824
  compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 8e4 },
327823
327825
  keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
327826
+ preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
327827
+ compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
327824
327828
  structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
327825
327829
  build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
327826
327830
  branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
@@ -334509,6 +334513,8 @@ var MODE_SCOPED_TOOLS = /* @__PURE__ */ new Set([
334509
334513
  "pdf_read",
334510
334514
  "linked_plan",
334511
334515
  "build_history_query",
334516
+ "context_compress",
334517
+ "context_history_manage",
334512
334518
  "question",
334513
334519
  "task",
334514
334520
  "subagent_list",
@@ -335526,14 +335532,24 @@ var ToolExecutor = class {
335526
335532
  remote_root: { type: "string" },
335527
335533
  remote_path: { type: "string" }
335528
335534
  }, ["action"]),
335529
- t3("task", "Create a same-conversation peer agent and return immediately. The peer has a nature slug, short id, and canonical UUID-qualified name. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335530
- t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335531
- t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers.", { id: { type: "string" }, name: { type: "string" }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335532
- t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent.", { id: { type: "string" }, name: { type: "string" }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335533
- t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent.", { id: { type: "string" }, name: { type: "string" } }, []),
335534
- t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself.", { id: { type: "string" }, name: { type: "string" } }, []),
335535
+ t3("task", "Create a same-conversation peer agent and return immediately. The peer has a stable human-readable name, a short id, and a canonical UUID-qualified identity. The peer name is decoupled from its id: pass name for readable references and id for exact targeting. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335536
+ t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status. Each entry exposes both the stable name and the exact id; use the id for any subsequent targeting.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335537
+ t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers. Pass the exact id returned by subagent_list, or a name for convenience lookup.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name; ambiguous names resolve to the first match." }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335538
+ t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335539
+ t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335540
+ t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335535
335541
  t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
335536
335542
  t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." } }, []),
335543
+ t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
335544
+ t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.", {
335545
+ action: { type: "string", enum: ["list", "remove", "summarize", "restore", "search", "status"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone." },
335546
+ position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
335547
+ to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
335548
+ limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list (default 200), or search matches to return (default 20)." },
335549
+ restore_id: { type: "string", description: "Cache id of a folded segment (from search or status) to restore into context." },
335550
+ query: { type: "string", description: "Case-insensitive text to search for across cached folded segments and their summaries." },
335551
+ dangerous: { type: "boolean", description: "Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message." }
335552
+ }, ["action"]),
335537
335553
  t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
335538
335554
  t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
335539
335555
  t3("skill", "Search enabled skill metadata or load one exact skill body on demand. Use query when unsure, then name to load the selected skill.", { query: { type: "string", maxLength: 200 }, name: { type: "string", maxLength: 200 } }, []),
@@ -337619,7 +337635,7 @@ var SubagentManager = class {
337619
337635
  natureSlug: slug,
337620
337636
  displayName,
337621
337637
  qualifiedName,
337622
- name: qualifiedName,
337638
+ name: slug,
337623
337639
  conversationId: this.conversationId,
337624
337640
  createdByAgentId,
337625
337641
  prompt,
@@ -337630,7 +337646,7 @@ var SubagentManager = class {
337630
337646
  flowName: flowName || void 0,
337631
337647
  flowPc: Math.max(0, Math.floor(Number(flowPc) || 0)),
337632
337648
  status: "queued",
337633
- messages: [{ role: "system", content: `Peer agent '${qualifiedName}': ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337649
+ messages: [{ role: "system", content: `Peer agent '${slug}' (${id}): ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337634
337650
  result: null,
337635
337651
  createdAt: stamp,
337636
337652
  updatedAt: stamp
@@ -337640,7 +337656,10 @@ var SubagentManager = class {
337640
337656
  return id;
337641
337657
  }
337642
337658
  get(id) {
337643
- return this.subs.get(id) || [...this.subs.values()].find((item) => item.name === id || item.qualifiedName === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337659
+ if (this.subs.has(id)) return this.subs.get(id);
337660
+ const exact = [...this.subs.values()].find((item) => item.id === id || item.qualifiedName === id);
337661
+ if (exact) return exact;
337662
+ return [...this.subs.values()].find((item) => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337644
337663
  }
337645
337664
  send(id, prompt) {
337646
337665
  const target = this.get(id);
@@ -337788,6 +337807,7 @@ var SubagentManager = class {
337788
337807
  natureSlug: record.natureSlug,
337789
337808
  displayName: record.displayName,
337790
337809
  qualifiedName: record.qualifiedName,
337810
+ name: record.name,
337791
337811
  createdByAgentId: record.createdByAgentId,
337792
337812
  status: record.status,
337793
337813
  active: record.status !== "closed",
@@ -340416,6 +340436,8 @@ async function executeNewmarkTool(agent, name50, args, inputSchema, signal) {
340416
340436
  if (name50 === "subagent_close") return agent.handleSubagentCloseEnvelope(args).output;
340417
340437
  if (name50 === "linked_plan") return agent.handleLinkedPlanTool(args);
340418
340438
  if (name50 === "build_history_query") return agent.handleBuildHistoryQuery(args);
340439
+ if (name50 === "context_compress") return (await agent.handleContextCompress(args, signal)).output;
340440
+ if (name50 === "context_history_manage") return agent.handleContextHistoryManage(args).output;
340419
340441
  if (name50 === "question") {
340420
340442
  if (agent.config.getStr("agent", "option_feedback") === "fully_autonomous") return "[question] Disabled by fully_autonomous option feedback.";
340421
340443
  if (!agent.handleQuestion(args)) return "[Question rejected: at least one question with two labeled options is required.]";
@@ -343405,6 +343427,8 @@ var Agent4 = class _Agent {
343405
343427
  continuations = [];
343406
343428
  activeConversationId = "default";
343407
343429
  lastCompression = null;
343430
+ compressionCache = [];
343431
+ nextCompressionCacheId = 1;
343408
343432
  workspaceConversations = /* @__PURE__ */ new Map();
343409
343433
  isSubagentRuntime = false;
343410
343434
  subagentName = "";
@@ -345866,6 +345890,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345866
345890
  this.workspaceConversations.set(key3, {
345867
345891
  chatMessages: [...this.chatMessages],
345868
345892
  history: [...this.history],
345893
+ compressionCache: [...this.compressionCache],
345869
345894
  plan: this.normalizeConversationPlan(this.conversationPlan),
345870
345895
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345871
345896
  subagentState: this.subagents.serialize(),
@@ -345892,6 +345917,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345892
345917
  title,
345893
345918
  chatMessages: [...this.chatMessages],
345894
345919
  history: [...this.history],
345920
+ compressionCache: [...this.compressionCache],
345895
345921
  plan: this.normalizeConversationPlan(this.conversationPlan),
345896
345922
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345897
345923
  subagentState: this.subagents.serialize(),
@@ -345919,6 +345945,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345919
345945
  if (!key3) {
345920
345946
  this.chatMessages = [];
345921
345947
  this.history = [];
345948
+ this.compressionCache = [];
345949
+ this.nextCompressionCacheId = 1;
345922
345950
  this.conversationPlan = { items: [] };
345923
345951
  this.linkedPlan = { markdown: "", revision: 0 };
345924
345952
  this.workRuns = [];
@@ -345935,6 +345963,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345935
345963
  const saved = this.workspaceConversations.get(key3);
345936
345964
  if (saved) {
345937
345965
  this.history = [...saved.history];
345966
+ this.compressionCache = saved.compressionCache ? saved.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
345967
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
345938
345968
  this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
345939
345969
  this.conversationPlan = this.normalizeConversationPlan(saved.plan);
345940
345970
  this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
@@ -345954,6 +345984,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345954
345984
  const stateKey = this.workspaceConversationStateKey();
345955
345985
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
345956
345986
  this.history = persisted?.history ? [...persisted.history] : [];
345987
+ this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
345988
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
345957
345989
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
345958
345990
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
345959
345991
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
@@ -345971,6 +346003,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345971
346003
  this.workspaceConversations.set(key3, {
345972
346004
  chatMessages: [...this.chatMessages],
345973
346005
  history: [...this.history],
346006
+ compressionCache: [...this.compressionCache],
345974
346007
  plan: this.normalizeConversationPlan(this.conversationPlan),
345975
346008
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345976
346009
  subagentState: this.subagents.serialize(),
@@ -346152,6 +346185,255 @@ Review this persisted peer result and summarize or continue the parent task as n
346152
346185
  truncatedActivities: Math.max(0, publicEvents.length - activities.length)
346153
346186
  });
346154
346187
  }
346188
+ async handleContextCompress(args, signal) {
346189
+ let input2 = {};
346190
+ try {
346191
+ input2 = JSON.parse(args || "{}");
346192
+ } catch {
346193
+ }
346194
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] No context to compress.", error: "No context to compress." };
346195
+ const previousKeepLast = this.config.getNum("context", "keep_recent_messages");
346196
+ const keepRecent = Math.max(2, Math.min(60, Math.floor(Number(input2.keep_recent) || previousKeepLast || 10)));
346197
+ const force = Boolean(input2.force);
346198
+ try {
346199
+ this.config.set("context", "keep_recent_messages", keepRecent);
346200
+ const msgs = this.history.map((message) => ({ ...message }));
346201
+ const provider = this.engineModel();
346202
+ await this.maybeCompress(msgs, provider, signal, this.activeModelName(), force);
346203
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] Compression skipped: context unchanged.", error: "Compression skipped." };
346204
+ return {
346205
+ ok: true,
346206
+ output: JSON.stringify({
346207
+ ok: true,
346208
+ compressed: true,
346209
+ at: this.lastCompression?.at,
346210
+ originalMessages: this.lastCompression?.originalMessages,
346211
+ compressedMessages: this.lastCompression?.compressedMessages,
346212
+ originalChars: this.lastCompression?.originalChars,
346213
+ compressedChars: this.lastCompression?.compressedChars,
346214
+ estimatedTokens: this.lastCompression?.compressedTokens,
346215
+ summary: this.lastCompression?.summary?.slice(0, 2e3),
346216
+ model: this.lastCompression?.model,
346217
+ fallback: this.lastCompression?.fallback,
346218
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346219
+ }, null, 2),
346220
+ metadata: { kind: "context-compress" }
346221
+ };
346222
+ } finally {
346223
+ this.config.set("context", "keep_recent_messages", previousKeepLast);
346224
+ }
346225
+ }
346226
+ handleContextHistoryManage(args) {
346227
+ let input2 = {};
346228
+ try {
346229
+ input2 = JSON.parse(args || "{}");
346230
+ } catch {
346231
+ }
346232
+ const action = String(input2.action || "").trim();
346233
+ if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize|restore|search|status).", error: "action is required." };
346234
+ if (action === "list") {
346235
+ const limit = Math.max(5, Math.min(400, Math.floor(Number(input2.limit || 200))));
346236
+ const entries = this.history.slice(0, limit).map((message, index) => ({
346237
+ position: index,
346238
+ role: String(message.role || ""),
346239
+ name: String(message.name || ""),
346240
+ chars: typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length,
346241
+ preview: this.compressionHistoryContent(message.content || "").slice(0, 160)
346242
+ }));
346243
+ return {
346244
+ ok: true,
346245
+ output: JSON.stringify({
346246
+ ok: true,
346247
+ action: "list",
346248
+ entryCount: this.history.length,
346249
+ listed: entries.length,
346250
+ entries,
346251
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346252
+ }, null, 2),
346253
+ metadata: { kind: "context-history-list" }
346254
+ };
346255
+ }
346256
+ const protectedZone = this.contextHistoryProtectedZone();
346257
+ if (action === "remove") {
346258
+ const position = Math.floor(Number(input2.position));
346259
+ if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
346260
+ return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
346261
+ }
346262
+ if (protectedZone.has(position) && !Boolean(input2.dangerous)) {
346263
+ return {
346264
+ ok: false,
346265
+ output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
346266
+ error: "remove position is in the protected context zone."
346267
+ };
346268
+ }
346269
+ const removed = this.history.splice(position, 1)[0];
346270
+ this.saveWorkspaceConversationState(true);
346271
+ return {
346272
+ ok: true,
346273
+ output: JSON.stringify({
346274
+ ok: true,
346275
+ action: "remove",
346276
+ removedPosition: position,
346277
+ removedRole: String(removed?.role || ""),
346278
+ remaining: this.history.length,
346279
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346280
+ }, null, 2),
346281
+ metadata: { kind: "context-history-remove" }
346282
+ };
346283
+ }
346284
+ if (action === "summarize") {
346285
+ const from = Math.max(0, Math.floor(Number(input2.position || 0)));
346286
+ const toRaw = Math.floor(Number(input2.to ?? input2.position));
346287
+ const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
346288
+ if (from >= this.history.length) return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: "summarize position out of range." };
346289
+ if (to - from < 1) return { ok: false, output: "[context_history_manage] summarize requires at least two entries in range.", error: "summarize requires a range of at least two entries." };
346290
+ const protectedHit = this.history.slice(from, to + 1).some((_3, index) => protectedZone.has(from + index));
346291
+ if (protectedHit && !Boolean(input2.dangerous)) {
346292
+ return {
346293
+ ok: false,
346294
+ output: "[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.",
346295
+ error: "summarize range overlaps the protected context zone."
346296
+ };
346297
+ }
346298
+ const segment = this.history.slice(from, to + 1);
346299
+ const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
346300
+ const summary = this.localCompressionSummary(
346301
+ `Workspace: ${this.workspace.current?.path || this.rootPath}
346302
+ Mode: ${this.modeName()}`,
346303
+ segment.map((message, i4) => `#${i4 + 1} [${String(message.role || "unknown")}${message.name ? ` ${String(message.name)}` : ""}]
346304
+ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0, 2e4),
346305
+ segment.length,
346306
+ chars
346307
+ );
346308
+ const replacement = { role: "system", content: `[Context History Summary]
346309
+ ${summary}` };
346310
+ this.history.splice(from, to - from + 1, replacement);
346311
+ this.pushCompressionCacheEntry(`[Context History Summary]
346312
+ ${summary}`, segment, "local-summarize", true);
346313
+ this.saveWorkspaceConversationState(true);
346314
+ return {
346315
+ ok: true,
346316
+ output: JSON.stringify({
346317
+ ok: true,
346318
+ action: "summarize",
346319
+ foldedFrom: from,
346320
+ foldedTo: to,
346321
+ foldedEntries: to - from + 1,
346322
+ foldedChars: chars,
346323
+ remaining: this.history.length,
346324
+ summary: summary.slice(0, 2e3),
346325
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346326
+ }, null, 2),
346327
+ metadata: { kind: "context-history-summarize" }
346328
+ };
346329
+ }
346330
+ if (action === "restore") {
346331
+ const restoreId = String(input2.restore_id || "").trim();
346332
+ const entry = this.compressionCache.find((item) => item.id === restoreId);
346333
+ if (!entry) return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: "restore_id not found." };
346334
+ const summaryHeader = entry.summary.startsWith("[Context Compression") ? "[Context Compression" : "[Context History Summary]";
346335
+ const markerIndex = this.history.findIndex((message) => String(message.role || "") === "system" && String(message.content || "").includes(summaryHeader) && String(message.content || "").includes(entry.summary.slice(0, 200)));
346336
+ if (markerIndex < 0) {
346337
+ return { ok: false, output: "[context_history_manage] restore failed: the folded summary is no longer present in context history (already re-folded or removed).", error: "restore target summary not found in history." };
346338
+ }
346339
+ this.history.splice(markerIndex, 1, ...entry.messages.map((message) => ({ ...message })));
346340
+ this.compressionCache = this.compressionCache.filter((item) => item.id !== entry.id);
346341
+ this.saveWorkspaceConversationState(true);
346342
+ return {
346343
+ ok: true,
346344
+ output: JSON.stringify({
346345
+ ok: true,
346346
+ action: "restore",
346347
+ restoreId: entry.id,
346348
+ restoredEntries: entry.messages.length,
346349
+ restoredChars: entry.foldedChars,
346350
+ cacheRemaining: this.compressionCache.length,
346351
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346352
+ }, null, 2),
346353
+ metadata: { kind: "context-history-restore" }
346354
+ };
346355
+ }
346356
+ if (action === "search") {
346357
+ const query = String(input2.query || "").trim().toLowerCase();
346358
+ const limit = Math.max(1, Math.min(200, Math.floor(Number(input2.limit || 20))));
346359
+ if (!query) return { ok: false, output: "[context_history_manage] search requires query.", error: "search requires query." };
346360
+ const matches = [];
346361
+ for (const entry of this.compressionCache) {
346362
+ if (matches.length >= limit) break;
346363
+ const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
346364
+ if (entry.summary.toLowerCase().includes(query)) {
346365
+ hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
346366
+ }
346367
+ entry.messages.forEach((message, index) => {
346368
+ if (matches.length >= limit || hit.matches.length >= 40) return;
346369
+ const content = this.compressionHistoryContent(message.content || message.reasoning_content || "");
346370
+ if (content.toLowerCase().includes(query)) {
346371
+ hit.matches.push({ index, snippet: this.snippetAround(content, query) });
346372
+ }
346373
+ });
346374
+ if (hit.matches.length) matches.push(hit);
346375
+ }
346376
+ return {
346377
+ ok: true,
346378
+ output: JSON.stringify({
346379
+ ok: true,
346380
+ action: "search",
346381
+ query: String(input2.query || ""),
346382
+ cacheEntries: this.compressionCache.length,
346383
+ matches,
346384
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346385
+ }, null, 2),
346386
+ metadata: { kind: "context-history-search" }
346387
+ };
346388
+ }
346389
+ if (action === "status") {
346390
+ const budget = this.compressionBudget(this.history);
346391
+ const estimatedTokens = this.estimateContextTokens(this.history);
346392
+ const maxTokens = this.contextMaxTokens();
346393
+ const protectedStartIndex = this.contextHistoryProtectedStartIndex();
346394
+ const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
346395
+ return {
346396
+ ok: true,
346397
+ output: JSON.stringify({
346398
+ ok: true,
346399
+ action: "status",
346400
+ historyLength: this.history.length,
346401
+ chatMessages: this.chatMessages.length,
346402
+ estimatedTokens,
346403
+ maxTokens,
346404
+ triggerTokens: budget.triggerTokens,
346405
+ targetTokens: budget.targetTokens,
346406
+ summaryTokens: budget.summaryTokens,
346407
+ usagePercent: maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 1e3) / 10 : 0,
346408
+ thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
346409
+ keepRecentMessages: this.config.getNum("context", "keep_recent_messages") || 10,
346410
+ lastCompression: this.lastCompression ? {
346411
+ at: this.lastCompression.at,
346412
+ originalMessages: this.lastCompression.originalMessages,
346413
+ compressedMessages: this.lastCompression.compressedMessages,
346414
+ compressedTokens: this.lastCompression.compressedTokens,
346415
+ model: this.lastCompression.model,
346416
+ fallback: this.lastCompression.fallback
346417
+ } : null,
346418
+ cache: {
346419
+ entries: this.compressionCache.length,
346420
+ totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
346421
+ totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
346422
+ ids: this.compressionCache.map((item) => item.id)
346423
+ },
346424
+ protectedZone: {
346425
+ preserveRecentMessages: this.config.getNum("context", "preserve_recent_messages") || 5,
346426
+ protectedStartIndex,
346427
+ lastUserMessageIndex: lastUserIndex,
346428
+ protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0
346429
+ },
346430
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346431
+ }, null, 2),
346432
+ metadata: { kind: "context-history-status" }
346433
+ };
346434
+ }
346435
+ return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
346436
+ }
346155
346437
  recordContextCompressionStep() {
346156
346438
  const runId = this.currentWorkRunId();
346157
346439
  if (!runId) return;
@@ -346550,6 +346832,7 @@ Review this persisted peer result and summarize or continue the parent task as n
346550
346832
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346551
346833
  fallback: fallbackUsed
346552
346834
  };
346835
+ this.pushCompressionCacheEntry(summary2, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed);
346553
346836
  this.persistCompressedHistory(summary2, recent.length, candidate2);
346554
346837
  this.saveWorkspaceConversationState(true);
346555
346838
  return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate2), maxTokens };
@@ -346583,6 +346866,7 @@ Review this persisted peer result and summarize or continue the parent task as n
346583
346866
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346584
346867
  fallback: fallbackUsed || droppedMessages > 0
346585
346868
  };
346869
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed || droppedMessages > 0);
346586
346870
  this.persistCompressedHistory(summary, recent.length, candidate);
346587
346871
  this.saveWorkspaceConversationState(true);
346588
346872
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -348019,7 +348303,7 @@ ${settled?.result || settled?.error || ""}`.trim();
348019
348303
  const transcript = sa.messages.map((m2) => `[${m2.role}] ${m2.content}`).join("\n");
348020
348304
  return this.subagents.toToolResult(
348021
348305
  sa.id,
348022
- `get.subagent("${sa.name}")
348306
+ `get.subagent("${sa.name}", id="${sa.id}")
348023
348307
  Status: ${sa.status}
348024
348308
  Model: ${sa.model}
348025
348309
  Mode: ${sa.agentMode}
@@ -348811,7 +349095,7 @@ Falling back to built-in engine.` }];
348811
349095
  if (!this.config.getBool("context", "auto_compress")) return;
348812
349096
  const total = msgs.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content || "").length), 0);
348813
349097
  const budget = this.compressionBudget(msgs);
348814
- if (budget.estimatedTokens < budget.triggerTokens) return;
349098
+ if (budget.estimatedTokens < budget.triggerTokens && !force) return;
348815
349099
  if (!force && this.lastCompression && String(msgs[0]?.content || "").includes(this.lastCompression.summary)) {
348816
349100
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
348817
349101
  const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
@@ -348863,6 +349147,7 @@ Falling back to built-in engine.` }];
348863
349147
  model: compression.model,
348864
349148
  fallback: compression.fallback
348865
349149
  };
349150
+ this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
348866
349151
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
348867
349152
  }
348868
349153
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
@@ -349011,6 +349296,48 @@ ${text.slice(-tailChars).trimStart()}`;
349011
349296
  }
349012
349297
  if (this.isSubagentRuntime) this.subagentContextPersist?.(this.history.map((message) => ({ ...message })), this.lastCompression);
349013
349298
  }
349299
+ pushCompressionCacheEntry(summary, messages, model, fallback) {
349300
+ if (!messages.length) return;
349301
+ const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
349302
+ this.compressionCache.push({
349303
+ id: `ctx-cache-${this.nextCompressionCacheId}`,
349304
+ at: (/* @__PURE__ */ new Date()).toISOString(),
349305
+ summary,
349306
+ messages: messages.map((message) => ({ ...message })),
349307
+ foldedEntries: messages.length,
349308
+ foldedChars,
349309
+ model,
349310
+ fallback
349311
+ });
349312
+ this.nextCompressionCacheId += 1;
349313
+ const maxEntries = Math.max(0, Math.floor(this.config.getNum("context", "compression_cache_max") || 8));
349314
+ if (this.compressionCache.length > maxEntries) {
349315
+ this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
349316
+ }
349317
+ this.saveWorkspaceConversationState(true);
349318
+ }
349319
+ contextHistoryProtectedStartIndex() {
349320
+ const preserve = Math.max(0, Math.floor(this.config.getNum("context", "preserve_recent_messages") || 5));
349321
+ const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
349322
+ const candidates = [];
349323
+ if (preserve > 0 && this.history.length > 0) candidates.push(Math.max(0, this.history.length - preserve));
349324
+ if (lastUserIndex >= 0) candidates.push(lastUserIndex);
349325
+ return candidates.length ? Math.min(...candidates) : -1;
349326
+ }
349327
+ contextHistoryProtectedZone() {
349328
+ const start = this.contextHistoryProtectedStartIndex();
349329
+ const zone = /* @__PURE__ */ new Set();
349330
+ if (start >= 0) for (let i4 = start; i4 < this.history.length; i4 += 1) zone.add(i4);
349331
+ return zone;
349332
+ }
349333
+ snippetAround(content, query, radius = 150) {
349334
+ const text = String(content || "");
349335
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
349336
+ if (index < 0) return text.slice(0, radius * 2);
349337
+ const from = Math.max(0, index - radius);
349338
+ const to = Math.min(text.length, index + query.length + radius);
349339
+ return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).trim()}${to < text.length ? "\u2026" : ""}`;
349340
+ }
349014
349341
  buildSystemPrompt() {
349015
349342
  const cwd = this.workspace.current?.path || this.rootPath;
349016
349343
  const enabledSkills = this.skills.active();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newmark-agent",
3
3
  "productName": "Newmark Agent",
4
- "version": "0.3.6",
4
+ "version": "0.3.8",
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": {