newmark-agent 0.3.7 → 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.
@@ -327819,6 +327819,8 @@ function defaultConfig() {
327819
327819
  auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
327820
327820
  compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 8e4 },
327821
327821
  keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
327822
+ preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
327823
+ compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
327822
327824
  structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
327823
327825
  build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
327824
327826
  branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
@@ -335535,11 +335537,14 @@ var ToolExecutor = class {
335535
335537
  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
335538
  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." } }, []),
335537
335539
  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." } }, []),
335538
- 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. The displayed conversation history (what the user sees) is never modified by any action.", {
335539
- action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
335540
+ 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.", {
335541
+ 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." },
335540
335542
  position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
335541
335543
  to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
335542
- limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list; defaults to 200." }
335544
+ limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list (default 200), or search matches to return (default 20)." },
335545
+ restore_id: { type: "string", description: "Cache id of a folded segment (from search or status) to restore into context." },
335546
+ query: { type: "string", description: "Case-insensitive text to search for across cached folded segments and their summaries." },
335547
+ 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." }
335543
335548
  }, ["action"]),
335544
335549
  t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
335545
335550
  t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
@@ -343418,6 +343423,8 @@ var Agent4 = class _Agent {
343418
343423
  continuations = [];
343419
343424
  activeConversationId = "default";
343420
343425
  lastCompression = null;
343426
+ compressionCache = [];
343427
+ nextCompressionCacheId = 1;
343421
343428
  workspaceConversations = /* @__PURE__ */ new Map();
343422
343429
  isSubagentRuntime = false;
343423
343430
  subagentName = "";
@@ -345879,6 +345886,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345879
345886
  this.workspaceConversations.set(key3, {
345880
345887
  chatMessages: [...this.chatMessages],
345881
345888
  history: [...this.history],
345889
+ compressionCache: [...this.compressionCache],
345882
345890
  plan: this.normalizeConversationPlan(this.conversationPlan),
345883
345891
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345884
345892
  subagentState: this.subagents.serialize(),
@@ -345905,6 +345913,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345905
345913
  title,
345906
345914
  chatMessages: [...this.chatMessages],
345907
345915
  history: [...this.history],
345916
+ compressionCache: [...this.compressionCache],
345908
345917
  plan: this.normalizeConversationPlan(this.conversationPlan),
345909
345918
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345910
345919
  subagentState: this.subagents.serialize(),
@@ -345932,6 +345941,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345932
345941
  if (!key3) {
345933
345942
  this.chatMessages = [];
345934
345943
  this.history = [];
345944
+ this.compressionCache = [];
345945
+ this.nextCompressionCacheId = 1;
345935
345946
  this.conversationPlan = { items: [] };
345936
345947
  this.linkedPlan = { markdown: "", revision: 0 };
345937
345948
  this.workRuns = [];
@@ -345948,6 +345959,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345948
345959
  const saved = this.workspaceConversations.get(key3);
345949
345960
  if (saved) {
345950
345961
  this.history = [...saved.history];
345962
+ this.compressionCache = saved.compressionCache ? saved.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
345963
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
345951
345964
  this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
345952
345965
  this.conversationPlan = this.normalizeConversationPlan(saved.plan);
345953
345966
  this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
@@ -345967,6 +345980,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345967
345980
  const stateKey = this.workspaceConversationStateKey();
345968
345981
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
345969
345982
  this.history = persisted?.history ? [...persisted.history] : [];
345983
+ this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
345984
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
345970
345985
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
345971
345986
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
345972
345987
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
@@ -345984,6 +345999,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345984
345999
  this.workspaceConversations.set(key3, {
345985
346000
  chatMessages: [...this.chatMessages],
345986
346001
  history: [...this.history],
346002
+ compressionCache: [...this.compressionCache],
345987
346003
  plan: this.normalizeConversationPlan(this.conversationPlan),
345988
346004
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345989
346005
  subagentState: this.subagents.serialize(),
@@ -346210,7 +346226,7 @@ Review this persisted peer result and summarize or continue the parent task as n
346210
346226
  } catch {
346211
346227
  }
346212
346228
  const action = String(input.action || "").trim();
346213
- if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
346229
+ if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize|restore|search|status).", error: "action is required." };
346214
346230
  if (action === "list") {
346215
346231
  const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
346216
346232
  const entries = this.history.slice(0, limit).map((message, index) => ({
@@ -346233,11 +346249,19 @@ Review this persisted peer result and summarize or continue the parent task as n
346233
346249
  metadata: { kind: "context-history-list" }
346234
346250
  };
346235
346251
  }
346252
+ const protectedZone = this.contextHistoryProtectedZone();
346236
346253
  if (action === "remove") {
346237
346254
  const position = Math.floor(Number(input.position));
346238
346255
  if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
346239
346256
  return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
346240
346257
  }
346258
+ if (protectedZone.has(position) && !Boolean(input.dangerous)) {
346259
+ return {
346260
+ ok: false,
346261
+ output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
346262
+ error: "remove position is in the protected context zone."
346263
+ };
346264
+ }
346241
346265
  const removed = this.history.splice(position, 1)[0];
346242
346266
  this.saveWorkspaceConversationState(true);
346243
346267
  return {
@@ -346259,6 +346283,14 @@ Review this persisted peer result and summarize or continue the parent task as n
346259
346283
  const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
346260
346284
  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." };
346261
346285
  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." };
346286
+ const protectedHit = this.history.slice(from, to + 1).some((_3, index) => protectedZone.has(from + index));
346287
+ if (protectedHit && !Boolean(input.dangerous)) {
346288
+ return {
346289
+ ok: false,
346290
+ output: "[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.",
346291
+ error: "summarize range overlaps the protected context zone."
346292
+ };
346293
+ }
346262
346294
  const segment = this.history.slice(from, to + 1);
346263
346295
  const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
346264
346296
  const summary = this.localCompressionSummary(
@@ -346272,6 +346304,8 @@ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0,
346272
346304
  const replacement = { role: "system", content: `[Context History Summary]
346273
346305
  ${summary}` };
346274
346306
  this.history.splice(from, to - from + 1, replacement);
346307
+ this.pushCompressionCacheEntry(`[Context History Summary]
346308
+ ${summary}`, segment, "local-summarize", true);
346275
346309
  this.saveWorkspaceConversationState(true);
346276
346310
  return {
346277
346311
  ok: true,
@@ -346289,6 +346323,111 @@ ${summary}` };
346289
346323
  metadata: { kind: "context-history-summarize" }
346290
346324
  };
346291
346325
  }
346326
+ if (action === "restore") {
346327
+ const restoreId = String(input.restore_id || "").trim();
346328
+ const entry = this.compressionCache.find((item) => item.id === restoreId);
346329
+ if (!entry) return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: "restore_id not found." };
346330
+ const summaryHeader = entry.summary.startsWith("[Context Compression") ? "[Context Compression" : "[Context History Summary]";
346331
+ const markerIndex = this.history.findIndex((message) => String(message.role || "") === "system" && String(message.content || "").includes(summaryHeader) && String(message.content || "").includes(entry.summary.slice(0, 200)));
346332
+ if (markerIndex < 0) {
346333
+ 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." };
346334
+ }
346335
+ this.history.splice(markerIndex, 1, ...entry.messages.map((message) => ({ ...message })));
346336
+ this.compressionCache = this.compressionCache.filter((item) => item.id !== entry.id);
346337
+ this.saveWorkspaceConversationState(true);
346338
+ return {
346339
+ ok: true,
346340
+ output: JSON.stringify({
346341
+ ok: true,
346342
+ action: "restore",
346343
+ restoreId: entry.id,
346344
+ restoredEntries: entry.messages.length,
346345
+ restoredChars: entry.foldedChars,
346346
+ cacheRemaining: this.compressionCache.length,
346347
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346348
+ }, null, 2),
346349
+ metadata: { kind: "context-history-restore" }
346350
+ };
346351
+ }
346352
+ if (action === "search") {
346353
+ const query = String(input.query || "").trim().toLowerCase();
346354
+ const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
346355
+ if (!query) return { ok: false, output: "[context_history_manage] search requires query.", error: "search requires query." };
346356
+ const matches = [];
346357
+ for (const entry of this.compressionCache) {
346358
+ if (matches.length >= limit) break;
346359
+ const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
346360
+ if (entry.summary.toLowerCase().includes(query)) {
346361
+ hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
346362
+ }
346363
+ entry.messages.forEach((message, index) => {
346364
+ if (matches.length >= limit || hit.matches.length >= 40) return;
346365
+ const content = this.compressionHistoryContent(message.content || message.reasoning_content || "");
346366
+ if (content.toLowerCase().includes(query)) {
346367
+ hit.matches.push({ index, snippet: this.snippetAround(content, query) });
346368
+ }
346369
+ });
346370
+ if (hit.matches.length) matches.push(hit);
346371
+ }
346372
+ return {
346373
+ ok: true,
346374
+ output: JSON.stringify({
346375
+ ok: true,
346376
+ action: "search",
346377
+ query: String(input.query || ""),
346378
+ cacheEntries: this.compressionCache.length,
346379
+ matches,
346380
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346381
+ }, null, 2),
346382
+ metadata: { kind: "context-history-search" }
346383
+ };
346384
+ }
346385
+ if (action === "status") {
346386
+ const budget = this.compressionBudget(this.history);
346387
+ const estimatedTokens = this.estimateContextTokens(this.history);
346388
+ const maxTokens = this.contextMaxTokens();
346389
+ const protectedStartIndex = this.contextHistoryProtectedStartIndex();
346390
+ const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
346391
+ return {
346392
+ ok: true,
346393
+ output: JSON.stringify({
346394
+ ok: true,
346395
+ action: "status",
346396
+ historyLength: this.history.length,
346397
+ chatMessages: this.chatMessages.length,
346398
+ estimatedTokens,
346399
+ maxTokens,
346400
+ triggerTokens: budget.triggerTokens,
346401
+ targetTokens: budget.targetTokens,
346402
+ summaryTokens: budget.summaryTokens,
346403
+ usagePercent: maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 1e3) / 10 : 0,
346404
+ thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
346405
+ keepRecentMessages: this.config.getNum("context", "keep_recent_messages") || 10,
346406
+ lastCompression: this.lastCompression ? {
346407
+ at: this.lastCompression.at,
346408
+ originalMessages: this.lastCompression.originalMessages,
346409
+ compressedMessages: this.lastCompression.compressedMessages,
346410
+ compressedTokens: this.lastCompression.compressedTokens,
346411
+ model: this.lastCompression.model,
346412
+ fallback: this.lastCompression.fallback
346413
+ } : null,
346414
+ cache: {
346415
+ entries: this.compressionCache.length,
346416
+ totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
346417
+ totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
346418
+ ids: this.compressionCache.map((item) => item.id)
346419
+ },
346420
+ protectedZone: {
346421
+ preserveRecentMessages: this.config.getNum("context", "preserve_recent_messages") || 5,
346422
+ protectedStartIndex,
346423
+ lastUserMessageIndex: lastUserIndex,
346424
+ protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0
346425
+ },
346426
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346427
+ }, null, 2),
346428
+ metadata: { kind: "context-history-status" }
346429
+ };
346430
+ }
346292
346431
  return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
346293
346432
  }
346294
346433
  recordContextCompressionStep() {
@@ -346689,6 +346828,7 @@ ${summary}` };
346689
346828
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346690
346829
  fallback: fallbackUsed
346691
346830
  };
346831
+ this.pushCompressionCacheEntry(summary2, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed);
346692
346832
  this.persistCompressedHistory(summary2, recent.length, candidate2);
346693
346833
  this.saveWorkspaceConversationState(true);
346694
346834
  return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate2), maxTokens };
@@ -346722,6 +346862,7 @@ ${summary}` };
346722
346862
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346723
346863
  fallback: fallbackUsed || droppedMessages > 0
346724
346864
  };
346865
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed || droppedMessages > 0);
346725
346866
  this.persistCompressedHistory(summary, recent.length, candidate);
346726
346867
  this.saveWorkspaceConversationState(true);
346727
346868
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -349002,6 +349143,7 @@ Falling back to built-in engine.` }];
349002
349143
  model: compression.model,
349003
349144
  fallback: compression.fallback
349004
349145
  };
349146
+ this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
349005
349147
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
349006
349148
  }
349007
349149
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
@@ -349150,6 +349292,48 @@ ${text.slice(-tailChars).trimStart()}`;
349150
349292
  }
349151
349293
  if (this.isSubagentRuntime) this.subagentContextPersist?.(this.history.map((message) => ({ ...message })), this.lastCompression);
349152
349294
  }
349295
+ pushCompressionCacheEntry(summary, messages, model, fallback) {
349296
+ if (!messages.length) return;
349297
+ const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
349298
+ this.compressionCache.push({
349299
+ id: `ctx-cache-${this.nextCompressionCacheId}`,
349300
+ at: (/* @__PURE__ */ new Date()).toISOString(),
349301
+ summary,
349302
+ messages: messages.map((message) => ({ ...message })),
349303
+ foldedEntries: messages.length,
349304
+ foldedChars,
349305
+ model,
349306
+ fallback
349307
+ });
349308
+ this.nextCompressionCacheId += 1;
349309
+ const maxEntries = Math.max(0, Math.floor(this.config.getNum("context", "compression_cache_max") || 8));
349310
+ if (this.compressionCache.length > maxEntries) {
349311
+ this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
349312
+ }
349313
+ this.saveWorkspaceConversationState(true);
349314
+ }
349315
+ contextHistoryProtectedStartIndex() {
349316
+ const preserve = Math.max(0, Math.floor(this.config.getNum("context", "preserve_recent_messages") || 5));
349317
+ const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
349318
+ const candidates = [];
349319
+ if (preserve > 0 && this.history.length > 0) candidates.push(Math.max(0, this.history.length - preserve));
349320
+ if (lastUserIndex >= 0) candidates.push(lastUserIndex);
349321
+ return candidates.length ? Math.min(...candidates) : -1;
349322
+ }
349323
+ contextHistoryProtectedZone() {
349324
+ const start = this.contextHistoryProtectedStartIndex();
349325
+ const zone = /* @__PURE__ */ new Set();
349326
+ if (start >= 0) for (let i4 = start; i4 < this.history.length; i4 += 1) zone.add(i4);
349327
+ return zone;
349328
+ }
349329
+ snippetAround(content, query, radius = 150) {
349330
+ const text = String(content || "");
349331
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
349332
+ if (index < 0) return text.slice(0, radius * 2);
349333
+ const from = Math.max(0, index - radius);
349334
+ const to = Math.min(text.length, index + query.length + radius);
349335
+ return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).trim()}${to < text.length ? "\u2026" : ""}`;
349336
+ }
349153
349337
  buildSystemPrompt() {
349154
349338
  const cwd = this.workspace.current?.path || this.rootPath;
349155
349339
  const enabledSkills = this.skills.active();
@@ -58,6 +58,16 @@ export interface AutoRouteRatingResult {
58
58
  }
59
59
  export declare const ROOT_AGENT_ACTOR_ID = "00000000-0000-4000-8000-000000000001";
60
60
  export declare function normalizeIntelligenceTier(value: unknown): 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
61
+ export interface CompressionCacheEntry {
62
+ id: string;
63
+ at: string;
64
+ summary: string;
65
+ messages: Array<Record<string, unknown>>;
66
+ foldedEntries: number;
67
+ foldedChars: number;
68
+ model: string;
69
+ fallback: boolean;
70
+ }
61
71
  type ConversationModelSelection = {
62
72
  kind: 'auto';
63
73
  } | {
@@ -286,6 +296,8 @@ export declare class Agent {
286
296
  model: string;
287
297
  fallback: boolean;
288
298
  } | null;
299
+ private compressionCache;
300
+ private nextCompressionCacheId;
289
301
  private workspaceConversations;
290
302
  isSubagentRuntime: boolean;
291
303
  private subagentName;
@@ -803,6 +815,10 @@ export declare class Agent {
803
815
  private compactSummaryBody;
804
816
  private formatCompressionSummary;
805
817
  private persistCompressedHistory;
818
+ private pushCompressionCacheEntry;
819
+ private contextHistoryProtectedStartIndex;
820
+ private contextHistoryProtectedZone;
821
+ private snippetAround;
806
822
  buildSystemPrompt(): string;
807
823
  /**
808
824
  * dev-0.3.0: assemble the model-request system prompt through the Context
@@ -191,6 +191,8 @@ class Agent {
191
191
  continuations = [];
192
192
  activeConversationId = 'default';
193
193
  lastCompression = null;
194
+ compressionCache = [];
195
+ nextCompressionCacheId = 1;
194
196
  workspaceConversations = new Map();
195
197
  isSubagentRuntime = false;
196
198
  subagentName = '';
@@ -3109,6 +3111,7 @@ class Agent {
3109
3111
  this.workspaceConversations.set(key, {
3110
3112
  chatMessages: [...this.chatMessages],
3111
3113
  history: [...this.history],
3114
+ compressionCache: [...this.compressionCache],
3112
3115
  plan: this.normalizeConversationPlan(this.conversationPlan),
3113
3116
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3114
3117
  subagentState: this.subagents.serialize(),
@@ -3138,6 +3141,7 @@ class Agent {
3138
3141
  title,
3139
3142
  chatMessages: [...this.chatMessages],
3140
3143
  history: [...this.history],
3144
+ compressionCache: [...this.compressionCache],
3141
3145
  plan: this.normalizeConversationPlan(this.conversationPlan),
3142
3146
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3143
3147
  subagentState: this.subagents.serialize(),
@@ -3167,6 +3171,8 @@ class Agent {
3167
3171
  if (!key) {
3168
3172
  this.chatMessages = [];
3169
3173
  this.history = [];
3174
+ this.compressionCache = [];
3175
+ this.nextCompressionCacheId = 1;
3170
3176
  this.conversationPlan = { items: [] };
3171
3177
  this.linkedPlan = { markdown: '', revision: 0 };
3172
3178
  this.workRuns = [];
@@ -3183,6 +3189,8 @@ class Agent {
3183
3189
  const saved = this.workspaceConversations.get(key);
3184
3190
  if (saved) {
3185
3191
  this.history = [...saved.history];
3192
+ this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
3193
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
3186
3194
  this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
3187
3195
  this.conversationPlan = this.normalizeConversationPlan(saved.plan);
3188
3196
  this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
@@ -3202,6 +3210,8 @@ class Agent {
3202
3210
  const stateKey = this.workspaceConversationStateKey();
3203
3211
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
3204
3212
  this.history = persisted?.history ? [...persisted.history] : [];
3213
+ this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
3214
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
3205
3215
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
3206
3216
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
3207
3217
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
@@ -3219,6 +3229,7 @@ class Agent {
3219
3229
  this.workspaceConversations.set(key, {
3220
3230
  chatMessages: [...this.chatMessages],
3221
3231
  history: [...this.history],
3232
+ compressionCache: [...this.compressionCache],
3222
3233
  plan: this.normalizeConversationPlan(this.conversationPlan),
3223
3234
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3224
3235
  subagentState: this.subagents.serialize(),
@@ -3466,7 +3477,7 @@ class Agent {
3466
3477
  catch { }
3467
3478
  const action = String(input.action || '').trim();
3468
3479
  if (!action)
3469
- return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize).', error: 'action is required.' };
3480
+ return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize|restore|search|status).', error: 'action is required.' };
3470
3481
  if (action === 'list') {
3471
3482
  const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
3472
3483
  const entries = this.history.slice(0, limit).map((message, index) => ({
@@ -3489,11 +3500,19 @@ class Agent {
3489
3500
  metadata: { kind: 'context-history-list' },
3490
3501
  };
3491
3502
  }
3503
+ const protectedZone = this.contextHistoryProtectedZone();
3492
3504
  if (action === 'remove') {
3493
3505
  const position = Math.floor(Number(input.position));
3494
3506
  if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
3495
3507
  return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: 'remove position out of range.' };
3496
3508
  }
3509
+ if (protectedZone.has(position) && !Boolean(input.dangerous)) {
3510
+ return {
3511
+ ok: false,
3512
+ output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
3513
+ error: 'remove position is in the protected context zone.',
3514
+ };
3515
+ }
3497
3516
  const removed = this.history.splice(position, 1)[0];
3498
3517
  this.saveWorkspaceConversationState(true);
3499
3518
  return {
@@ -3517,11 +3536,20 @@ class Agent {
3517
3536
  return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: 'summarize position out of range.' };
3518
3537
  if (to - from < 1)
3519
3538
  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.' };
3539
+ const protectedHit = this.history.slice(from, to + 1).some((_, index) => protectedZone.has(from + index));
3540
+ if (protectedHit && !Boolean(input.dangerous)) {
3541
+ return {
3542
+ ok: false,
3543
+ output: '[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.',
3544
+ error: 'summarize range overlaps the protected context zone.',
3545
+ };
3546
+ }
3520
3547
  const segment = this.history.slice(from, to + 1);
3521
3548
  const chars = segment.reduce((sum, message) => sum + (typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length), 0);
3522
3549
  const summary = this.localCompressionSummary(`Workspace: ${this.workspace.current?.path || this.rootPath}\nMode: ${this.modeName()}`, segment.map((message, i) => `#${i + 1} [${String(message.role || 'unknown')}${message.name ? ` ${String(message.name)}` : ''}]\n${this.compressionHistoryContent(message.content || '')}`).join('\n\n').slice(0, 20000), segment.length, chars);
3523
3550
  const replacement = { role: 'system', content: `[Context History Summary]\n${summary}` };
3524
3551
  this.history.splice(from, to - from + 1, replacement);
3552
+ this.pushCompressionCacheEntry(`[Context History Summary]\n${summary}`, segment, 'local-summarize', true);
3525
3553
  this.saveWorkspaceConversationState(true);
3526
3554
  return {
3527
3555
  ok: true,
@@ -3539,6 +3567,116 @@ class Agent {
3539
3567
  metadata: { kind: 'context-history-summarize' },
3540
3568
  };
3541
3569
  }
3570
+ if (action === 'restore') {
3571
+ const restoreId = String(input.restore_id || '').trim();
3572
+ const entry = this.compressionCache.find(item => item.id === restoreId);
3573
+ if (!entry)
3574
+ return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
3575
+ const summaryHeader = entry.summary.startsWith('[Context Compression') ? '[Context Compression' : '[Context History Summary]';
3576
+ const markerIndex = this.history.findIndex(message => String(message.role || '') === 'system' && String(message.content || '').includes(summaryHeader) && String(message.content || '').includes(entry.summary.slice(0, 200)));
3577
+ if (markerIndex < 0) {
3578
+ 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.' };
3579
+ }
3580
+ this.history.splice(markerIndex, 1, ...entry.messages.map(message => ({ ...message })));
3581
+ this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
3582
+ this.saveWorkspaceConversationState(true);
3583
+ return {
3584
+ ok: true,
3585
+ output: JSON.stringify({
3586
+ ok: true,
3587
+ action: 'restore',
3588
+ restoreId: entry.id,
3589
+ restoredEntries: entry.messages.length,
3590
+ restoredChars: entry.foldedChars,
3591
+ cacheRemaining: this.compressionCache.length,
3592
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3593
+ }, null, 2),
3594
+ metadata: { kind: 'context-history-restore' },
3595
+ };
3596
+ }
3597
+ if (action === 'search') {
3598
+ const query = String(input.query || '').trim().toLowerCase();
3599
+ const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
3600
+ if (!query)
3601
+ return { ok: false, output: '[context_history_manage] search requires query.', error: 'search requires query.' };
3602
+ const matches = [];
3603
+ for (const entry of this.compressionCache) {
3604
+ if (matches.length >= limit)
3605
+ break;
3606
+ const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
3607
+ if (entry.summary.toLowerCase().includes(query)) {
3608
+ hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
3609
+ }
3610
+ entry.messages.forEach((message, index) => {
3611
+ if (matches.length >= limit || hit.matches.length >= 40)
3612
+ return;
3613
+ const content = this.compressionHistoryContent(message.content || message.reasoning_content || '');
3614
+ if (content.toLowerCase().includes(query)) {
3615
+ hit.matches.push({ index, snippet: this.snippetAround(content, query) });
3616
+ }
3617
+ });
3618
+ if (hit.matches.length)
3619
+ matches.push(hit);
3620
+ }
3621
+ return {
3622
+ ok: true,
3623
+ output: JSON.stringify({
3624
+ ok: true,
3625
+ action: 'search',
3626
+ query: String(input.query || ''),
3627
+ cacheEntries: this.compressionCache.length,
3628
+ matches,
3629
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3630
+ }, null, 2),
3631
+ metadata: { kind: 'context-history-search' },
3632
+ };
3633
+ }
3634
+ if (action === 'status') {
3635
+ const budget = this.compressionBudget(this.history);
3636
+ const estimatedTokens = this.estimateContextTokens(this.history);
3637
+ const maxTokens = this.contextMaxTokens();
3638
+ const protectedStartIndex = this.contextHistoryProtectedStartIndex();
3639
+ const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
3640
+ return {
3641
+ ok: true,
3642
+ output: JSON.stringify({
3643
+ ok: true,
3644
+ action: 'status',
3645
+ historyLength: this.history.length,
3646
+ chatMessages: this.chatMessages.length,
3647
+ estimatedTokens,
3648
+ maxTokens,
3649
+ triggerTokens: budget.triggerTokens,
3650
+ targetTokens: budget.targetTokens,
3651
+ summaryTokens: budget.summaryTokens,
3652
+ usagePercent: maxTokens > 0 ? Math.round((estimatedTokens / maxTokens) * 1000) / 10 : 0,
3653
+ thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
3654
+ keepRecentMessages: this.config.getNum('context', 'keep_recent_messages') || 10,
3655
+ lastCompression: this.lastCompression ? {
3656
+ at: this.lastCompression.at,
3657
+ originalMessages: this.lastCompression.originalMessages,
3658
+ compressedMessages: this.lastCompression.compressedMessages,
3659
+ compressedTokens: this.lastCompression.compressedTokens,
3660
+ model: this.lastCompression.model,
3661
+ fallback: this.lastCompression.fallback,
3662
+ } : null,
3663
+ cache: {
3664
+ entries: this.compressionCache.length,
3665
+ totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
3666
+ totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
3667
+ ids: this.compressionCache.map(item => item.id),
3668
+ },
3669
+ protectedZone: {
3670
+ preserveRecentMessages: this.config.getNum('context', 'preserve_recent_messages') || 5,
3671
+ protectedStartIndex,
3672
+ lastUserMessageIndex: lastUserIndex,
3673
+ protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0,
3674
+ },
3675
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3676
+ }, null, 2),
3677
+ metadata: { kind: 'context-history-status' },
3678
+ };
3679
+ }
3542
3680
  return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
3543
3681
  }
3544
3682
  recordContextCompressionStep() {
@@ -3980,6 +4118,7 @@ class Agent {
3980
4118
  model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
3981
4119
  fallback: fallbackUsed,
3982
4120
  };
4121
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed);
3983
4122
  this.persistCompressedHistory(summary, recent.length, candidate);
3984
4123
  this.saveWorkspaceConversationState(true);
3985
4124
  return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -4016,6 +4155,7 @@ class Agent {
4016
4155
  model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
4017
4156
  fallback: fallbackUsed || droppedMessages > 0,
4018
4157
  };
4158
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed || droppedMessages > 0);
4019
4159
  this.persistCompressedHistory(summary, recent.length, candidate);
4020
4160
  this.saveWorkspaceConversationState(true);
4021
4161
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -6574,6 +6714,7 @@ class Agent {
6574
6714
  model: compression.model,
6575
6715
  fallback: compression.fallback,
6576
6716
  };
6717
+ this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
6577
6718
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
6578
6719
  }
6579
6720
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = '') {
@@ -6731,6 +6872,56 @@ class Agent {
6731
6872
  if (this.isSubagentRuntime)
6732
6873
  this.subagentContextPersist?.(this.history.map(message => ({ ...message })), this.lastCompression);
6733
6874
  }
6875
+ pushCompressionCacheEntry(summary, messages, model, fallback) {
6876
+ if (!messages.length)
6877
+ return;
6878
+ const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === 'string'
6879
+ ? message.content.length
6880
+ : JSON.stringify(message.content || '').length), 0);
6881
+ this.compressionCache.push({
6882
+ id: `ctx-cache-${this.nextCompressionCacheId}`,
6883
+ at: new Date().toISOString(),
6884
+ summary,
6885
+ messages: messages.map(message => ({ ...message })),
6886
+ foldedEntries: messages.length,
6887
+ foldedChars,
6888
+ model,
6889
+ fallback,
6890
+ });
6891
+ this.nextCompressionCacheId += 1;
6892
+ const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
6893
+ if (this.compressionCache.length > maxEntries) {
6894
+ this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
6895
+ }
6896
+ this.saveWorkspaceConversationState(true);
6897
+ }
6898
+ contextHistoryProtectedStartIndex() {
6899
+ const preserve = Math.max(0, Math.floor(this.config.getNum('context', 'preserve_recent_messages') || 5));
6900
+ const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
6901
+ const candidates = [];
6902
+ if (preserve > 0 && this.history.length > 0)
6903
+ candidates.push(Math.max(0, this.history.length - preserve));
6904
+ if (lastUserIndex >= 0)
6905
+ candidates.push(lastUserIndex);
6906
+ return candidates.length ? Math.min(...candidates) : -1;
6907
+ }
6908
+ contextHistoryProtectedZone() {
6909
+ const start = this.contextHistoryProtectedStartIndex();
6910
+ const zone = new Set();
6911
+ if (start >= 0)
6912
+ for (let i = start; i < this.history.length; i += 1)
6913
+ zone.add(i);
6914
+ return zone;
6915
+ }
6916
+ snippetAround(content, query, radius = 150) {
6917
+ const text = String(content || '');
6918
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
6919
+ if (index < 0)
6920
+ return text.slice(0, radius * 2);
6921
+ const from = Math.max(0, index - radius);
6922
+ const to = Math.min(text.length, index + query.length + radius);
6923
+ return `${from > 0 ? '…' : ''}${text.slice(from, to).trim()}${to < text.length ? '…' : ''}`;
6924
+ }
6734
6925
  buildSystemPrompt() {
6735
6926
  const cwd = this.workspace.current?.path || this.rootPath;
6736
6927
  const enabledSkills = this.skills.active();
@@ -911,6 +911,8 @@ function defaultConfig() {
911
911
  auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
912
912
  compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 80000 },
913
913
  keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
914
+ preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
915
+ compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
914
916
  structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
915
917
  build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
916
918
  branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
@@ -374,11 +374,14 @@ class ToolExecutor {
374
374
  t('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']),
375
375
  t('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.' } }, []),
376
376
  t('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.' } }, []),
377
- t('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. The displayed conversation history (what the user sees) is never modified by any action.', {
378
- action: { type: 'string', enum: ['list', 'remove', 'summarize'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry.' },
377
+ t('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.', {
378
+ 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.' },
379
379
  position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
380
380
  to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
381
- limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list; defaults to 200.' },
381
+ limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list (default 200), or search matches to return (default 20).' },
382
+ restore_id: { type: 'string', description: 'Cache id of a folded segment (from search or status) to restore into context.' },
383
+ query: { type: 'string', description: 'Case-insensitive text to search for across cached folded segments and their summaries.' },
384
+ 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.' },
382
385
  }, ['action']),
383
386
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
384
387
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
@@ -2959,6 +2959,10 @@ button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus
2959
2959
  .work-review-btn { height:28px; padding:0 10px; border:1px solid var(--glass-border-2); border-radius:6px; background:transparent; color:var(--text); cursor:pointer; font:600 11px var(--font); }
2960
2960
  .work-review-btn:hover { background:var(--control-hover-bg); border-color:var(--border-hover); }
2961
2961
  .work-review-list { border-top:1px solid var(--glass-border-1); }
2962
+ .work-review.collapsed .work-review-list { display: none; }
2963
+ .work-review-head { cursor: pointer; }
2964
+ .work-review-chevron { width:8px; height:8px; flex:0 0 auto; border-right:1px solid var(--text-dim); border-bottom:1px solid var(--text-dim); transform:rotate(-45deg); transition:transform 150ms ease; }
2965
+ .work-review.collapsed .work-review-chevron { transform:rotate(45deg); }
2962
2966
  .work-review-file { min-height:34px; display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:12px; padding:0 12px; color:var(--text); }
2963
2967
  .work-review-file:hover { background:var(--review-row-hover); }
2964
2968
  .work-review-path { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:11px var(--font-mono); }
@@ -8208,6 +8212,12 @@ function normalizeWorkReviewDiffs(diffs) {
8208
8212
  return Object.keys(byPath).map(function(key) { return byPath[key]; });
8209
8213
  }
8210
8214
 
8215
+ window.toggleWorkReview = function(head) {
8216
+ var review = head && head.closest ? head.closest('.work-review') : null;
8217
+ if (!review) return;
8218
+ review.classList.toggle('collapsed');
8219
+ };
8220
+
8211
8221
  window.toggleWorkReviewFiles = function(button) {
8212
8222
  var review = button && button.closest ? button.closest('.work-review') : null;
8213
8223
  if (!review) return;
@@ -8301,15 +8311,16 @@ function addWorkReview(diffs) {
8301
8311
  var added = files.reduce(function(total, file) { return total + file.added; }, 0);
8302
8312
  var deleted = files.reduce(function(total, file) { return total + file.deleted; }, 0);
8303
8313
  var review = document.createElement('div');
8304
- review.className = 'work-review';
8314
+ review.className = 'work-review collapsed';
8305
8315
  review.setAttribute('data-files', JSON.stringify(files));
8306
8316
  var rows = files.map(function(file, index) {
8307
8317
  return '<div class="work-review-file"' + (index >= 3 ? ' style="display:none"' : '') + '><span class="work-review-path">' + esc(file.path) + '</span><span><span class="work-review-add">+' + file.added + '</span><span class="work-review-del">-' + file.deleted + '</span></span></div>';
8308
8318
  }).join('');
8309
8319
  var editedLabel = files.length === 1 ? t('review.editedOne') : t('review.editedMany').replace('{count}', files.length);
8310
- review.innerHTML = '<div class="work-review-head"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
8320
+ review.innerHTML = '<div class="work-review-head" onclick="window.toggleWorkReview(this)"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
8311
8321
  '<div><div class="work-review-title">' + esc(editedLabel) + '</div><div class="work-review-stats"><span class="work-review-add">+' + added + '</span><span class="work-review-del">-' + deleted + '</span></div></div>' +
8312
- '<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this)">' + esc(t('review.open')) + '</button></div></div>' +
8322
+ '<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this);event.stopPropagation()">' + esc(t('review.open')) + '</button>' +
8323
+ '<span class="work-review-chevron" aria-hidden="true"></span></div></div>' +
8313
8324
  '<div class="work-review-list">' + rows + (files.length > 3 ? '<button class="work-review-more" onclick="window.toggleWorkReviewFiles(this)">' + esc(t('review.showMore').replace('{count}', files.length - 3)) + '</button>' : '') + '</div>';
8314
8325
  els['chat-area'].appendChild(review);
8315
8326
  autoScrollIfAtBottom();
@@ -327823,6 +327823,8 @@ function defaultConfig() {
327823
327823
  auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
327824
327824
  compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 8e4 },
327825
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 },
327826
327828
  structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
327827
327829
  build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
327828
327830
  branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
@@ -335539,11 +335541,14 @@ var ToolExecutor = class {
335539
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"]),
335540
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." } }, []),
335541
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." } }, []),
335542
- 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. The displayed conversation history (what the user sees) is never modified by any action.", {
335543
- action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
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." },
335544
335546
  position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
335545
335547
  to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
335546
- limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list; defaults to 200." }
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." }
335547
335552
  }, ["action"]),
335548
335553
  t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
335549
335554
  t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
@@ -343422,6 +343427,8 @@ var Agent4 = class _Agent {
343422
343427
  continuations = [];
343423
343428
  activeConversationId = "default";
343424
343429
  lastCompression = null;
343430
+ compressionCache = [];
343431
+ nextCompressionCacheId = 1;
343425
343432
  workspaceConversations = /* @__PURE__ */ new Map();
343426
343433
  isSubagentRuntime = false;
343427
343434
  subagentName = "";
@@ -345883,6 +345890,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345883
345890
  this.workspaceConversations.set(key3, {
345884
345891
  chatMessages: [...this.chatMessages],
345885
345892
  history: [...this.history],
345893
+ compressionCache: [...this.compressionCache],
345886
345894
  plan: this.normalizeConversationPlan(this.conversationPlan),
345887
345895
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345888
345896
  subagentState: this.subagents.serialize(),
@@ -345909,6 +345917,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345909
345917
  title,
345910
345918
  chatMessages: [...this.chatMessages],
345911
345919
  history: [...this.history],
345920
+ compressionCache: [...this.compressionCache],
345912
345921
  plan: this.normalizeConversationPlan(this.conversationPlan),
345913
345922
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345914
345923
  subagentState: this.subagents.serialize(),
@@ -345936,6 +345945,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345936
345945
  if (!key3) {
345937
345946
  this.chatMessages = [];
345938
345947
  this.history = [];
345948
+ this.compressionCache = [];
345949
+ this.nextCompressionCacheId = 1;
345939
345950
  this.conversationPlan = { items: [] };
345940
345951
  this.linkedPlan = { markdown: "", revision: 0 };
345941
345952
  this.workRuns = [];
@@ -345952,6 +345963,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345952
345963
  const saved = this.workspaceConversations.get(key3);
345953
345964
  if (saved) {
345954
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;
345955
345968
  this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
345956
345969
  this.conversationPlan = this.normalizeConversationPlan(saved.plan);
345957
345970
  this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
@@ -345971,6 +345984,8 @@ Review this persisted peer result and summarize or continue the parent task as n
345971
345984
  const stateKey = this.workspaceConversationStateKey();
345972
345985
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
345973
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;
345974
345989
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
345975
345990
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
345976
345991
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
@@ -345988,6 +346003,7 @@ Review this persisted peer result and summarize or continue the parent task as n
345988
346003
  this.workspaceConversations.set(key3, {
345989
346004
  chatMessages: [...this.chatMessages],
345990
346005
  history: [...this.history],
346006
+ compressionCache: [...this.compressionCache],
345991
346007
  plan: this.normalizeConversationPlan(this.conversationPlan),
345992
346008
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
345993
346009
  subagentState: this.subagents.serialize(),
@@ -346214,7 +346230,7 @@ Review this persisted peer result and summarize or continue the parent task as n
346214
346230
  } catch {
346215
346231
  }
346216
346232
  const action = String(input2.action || "").trim();
346217
- if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
346233
+ if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize|restore|search|status).", error: "action is required." };
346218
346234
  if (action === "list") {
346219
346235
  const limit = Math.max(5, Math.min(400, Math.floor(Number(input2.limit || 200))));
346220
346236
  const entries = this.history.slice(0, limit).map((message, index) => ({
@@ -346237,11 +346253,19 @@ Review this persisted peer result and summarize or continue the parent task as n
346237
346253
  metadata: { kind: "context-history-list" }
346238
346254
  };
346239
346255
  }
346256
+ const protectedZone = this.contextHistoryProtectedZone();
346240
346257
  if (action === "remove") {
346241
346258
  const position = Math.floor(Number(input2.position));
346242
346259
  if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
346243
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." };
346244
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
+ }
346245
346269
  const removed = this.history.splice(position, 1)[0];
346246
346270
  this.saveWorkspaceConversationState(true);
346247
346271
  return {
@@ -346263,6 +346287,14 @@ Review this persisted peer result and summarize or continue the parent task as n
346263
346287
  const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
346264
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." };
346265
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
+ }
346266
346298
  const segment = this.history.slice(from, to + 1);
346267
346299
  const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
346268
346300
  const summary = this.localCompressionSummary(
@@ -346276,6 +346308,8 @@ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0,
346276
346308
  const replacement = { role: "system", content: `[Context History Summary]
346277
346309
  ${summary}` };
346278
346310
  this.history.splice(from, to - from + 1, replacement);
346311
+ this.pushCompressionCacheEntry(`[Context History Summary]
346312
+ ${summary}`, segment, "local-summarize", true);
346279
346313
  this.saveWorkspaceConversationState(true);
346280
346314
  return {
346281
346315
  ok: true,
@@ -346293,6 +346327,111 @@ ${summary}` };
346293
346327
  metadata: { kind: "context-history-summarize" }
346294
346328
  };
346295
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
+ }
346296
346435
  return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
346297
346436
  }
346298
346437
  recordContextCompressionStep() {
@@ -346693,6 +346832,7 @@ ${summary}` };
346693
346832
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346694
346833
  fallback: fallbackUsed
346695
346834
  };
346835
+ this.pushCompressionCacheEntry(summary2, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed);
346696
346836
  this.persistCompressedHistory(summary2, recent.length, candidate2);
346697
346837
  this.saveWorkspaceConversationState(true);
346698
346838
  return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate2), maxTokens };
@@ -346726,6 +346866,7 @@ ${summary}` };
346726
346866
  model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
346727
346867
  fallback: fallbackUsed || droppedMessages > 0
346728
346868
  };
346869
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed || droppedMessages > 0);
346729
346870
  this.persistCompressedHistory(summary, recent.length, candidate);
346730
346871
  this.saveWorkspaceConversationState(true);
346731
346872
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -349006,6 +349147,7 @@ Falling back to built-in engine.` }];
349006
349147
  model: compression.model,
349007
349148
  fallback: compression.fallback
349008
349149
  };
349150
+ this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
349009
349151
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
349010
349152
  }
349011
349153
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
@@ -349154,6 +349296,48 @@ ${text.slice(-tailChars).trimStart()}`;
349154
349296
  }
349155
349297
  if (this.isSubagentRuntime) this.subagentContextPersist?.(this.history.map((message) => ({ ...message })), this.lastCompression);
349156
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
+ }
349157
349341
  buildSystemPrompt() {
349158
349342
  const cwd = this.workspace.current?.path || this.rootPath;
349159
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.7",
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": {