newmark-agent 0.3.7 → 0.3.10

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.
@@ -66,6 +66,8 @@ const agent_context_manager_1 = require("../context/services/agent-context-manag
66
66
  const toolchain_1 = require("../toolchain");
67
67
  const agent_runtime_1 = require("../agent-runtime");
68
68
  const performanceDiagnostics_1 = require("./performanceDiagnostics");
69
+ const compressionHistoryArchive_1 = require("./compressionHistoryArchive");
70
+ const runtimeLifecycle_1 = require("./runtimeLifecycle");
69
71
  exports.ROOT_AGENT_ACTOR_ID = '00000000-0000-4000-8000-000000000001';
70
72
  function normalizeIntelligenceTier(value) {
71
73
  const tier = String(value || '').trim().toLowerCase();
@@ -191,6 +193,9 @@ class Agent {
191
193
  continuations = [];
192
194
  activeConversationId = 'default';
193
195
  lastCompression = null;
196
+ compressionCache = [];
197
+ nextCompressionCacheId = 1;
198
+ compressionHistoryArchive;
194
199
  workspaceConversations = new Map();
195
200
  isSubagentRuntime = false;
196
201
  subagentName = '';
@@ -254,8 +259,10 @@ class Agent {
254
259
  rootInboxListener = (message) => this.deliverRootInboxMessage(message);
255
260
  agentOnly;
256
261
  runtimeActorId;
262
+ runtimeLifecycleRole;
257
263
  /** dev-0.3.0 context system facade (feature-flagged, default off). */
258
264
  contextV2;
265
+ runtimeLifecycle;
259
266
  /** dev-0.3.0 toolchain core (registry + capability catalog). Seeded lazily from cachedToolDefinitions; not consumed by the legacy path. */
260
267
  toolchainCore = null;
261
268
  /**
@@ -275,15 +282,18 @@ class Agent {
275
282
  }
276
283
  constructor(rootPath, options = {}) {
277
284
  this.rootPath = rootPath;
285
+ this.runtimeLifecycle = (0, runtimeLifecycle_1.beginRuntimeLifecycle)(rootPath, options.runtimeLifecycleRole || 'main');
278
286
  this.isSubagentRuntime = !!options.subagent;
279
287
  this.agentOnly = !!options.agentOnly;
280
288
  this.runtimeActorId = options.actorId || exports.ROOT_AGENT_ACTOR_ID;
289
+ this.runtimeLifecycleRole = options.runtimeLifecycleRole || 'main';
281
290
  if (options.conversationId)
282
291
  this.activeConversationId = this.safeConversationId(options.conversationId);
283
292
  this.subagentName = options.subagentName || '';
284
293
  this.subagentPrompt = options.subagentPrompt || '';
285
294
  this.linkedPlanAccess = options.linkedPlanAccess;
286
295
  this.config = new config_1.ConfigManager(rootPath);
296
+ this.compressionHistoryArchive = new compressionHistoryArchive_1.CompressionHistoryArchive(rootPath);
287
297
  this.contextV2 = new agent_context_manager_1.AgentContextManager(rootPath, this.config);
288
298
  this.agentRunService = this.config.contextFlag('agent_runtime_v2')
289
299
  ? new agent_runtime_1.AgentRunService(path.join(rootPath, '.newmark-context-v2'))
@@ -1462,6 +1472,11 @@ class Agent {
1462
1472
  runId,
1463
1473
  target,
1464
1474
  runtimeKey: String(raw.runtimeKey || (0, conversationTarget_1.conversationRuntimeKey)(target)),
1475
+ runtimeOwnerId: String(raw.runtimeOwnerId || '') || undefined,
1476
+ runtimeOwnerPid: Number.isFinite(Number(raw.runtimeOwnerPid)) && Number(raw.runtimeOwnerPid) > 0
1477
+ ? Math.floor(Number(raw.runtimeOwnerPid)) : undefined,
1478
+ runtimeLifecycleRole: ['main', 'utility', 'wsl'].includes(String(raw.runtimeLifecycleRole || ''))
1479
+ ? String(raw.runtimeLifecycleRole) : undefined,
1465
1480
  status,
1466
1481
  startedAt: raw.startedAt || this.nowIso(),
1467
1482
  endedAt: raw.endedAt,
@@ -1527,27 +1542,76 @@ class Agent {
1527
1542
  const sorted = [...byRun.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
1528
1543
  return sorted.length > RUN_WINDOW ? sorted.slice(-RUN_WINDOW) : sorted;
1529
1544
  }
1530
- recoverPersistedWorkRuns(runs, persistedUpdatedAt) {
1545
+ recoverPersistedWorkRuns(runs, persistedUpdatedAt, unexpectedExit = false) {
1531
1546
  const normalized = this.normalizeWorkRuns(runs);
1532
1547
  let changed = false;
1533
- // Only convert 'running' to 'interrupted' when the persisted state is
1534
- // stale enough to indicate a cold start. If the state was persisted
1535
- // recently the conversation may still be running in a background kernel
1536
- // runtime, so preserve the running status for the UI.
1548
+ // A frontend tracking window can expire while the backend process keeps
1549
+ // running. Owner PID evidence is stronger than the persistence timestamp:
1550
+ // never rewrite a live backend run merely because a cold snapshot is old.
1537
1551
  const isRecentPersist = !!persistedUpdatedAt
1538
1552
  && (Date.now() - new Date(persistedUpdatedAt).getTime()) < 120_000;
1539
- if (isRecentPersist)
1540
- return { runs: normalized, changed: false };
1553
+ let preservedLiveRun = false;
1541
1554
  for (const run of normalized) {
1542
1555
  if (run.status !== 'running')
1543
1556
  continue;
1557
+ const ownerAlive = !!run.runtimeOwnerPid && (0, runtimeLifecycle_1.isRuntimeProcessAlive)(run.runtimeOwnerPid);
1558
+ if (!unexpectedExit && (ownerAlive || (!run.runtimeOwnerPid && this.runtimeLifecycle.previousOwnerAlive))) {
1559
+ preservedLiveRun = true;
1560
+ continue;
1561
+ }
1562
+ if (!unexpectedExit && isRecentPersist) {
1563
+ preservedLiveRun = true;
1564
+ continue;
1565
+ }
1544
1566
  run.status = 'interrupted';
1545
- run.endedAt = persistedUpdatedAt || run.startedAt || this.nowIso();
1567
+ run.endedAt = unexpectedExit ? this.nowIso() : (persistedUpdatedAt || run.startedAt || this.nowIso());
1546
1568
  run.expanded = true;
1569
+ const sequence = Math.max(0, Number(run.sequence || 0)) + 1;
1570
+ run.sequence = sequence;
1571
+ run.events.push({
1572
+ id: `${run.runId}-unexpected-exit-${sequence}`,
1573
+ conversationId: run.target.conversationId,
1574
+ type: 'status',
1575
+ content: unexpectedExit
1576
+ ? 'Runtime exited unexpectedly; this Build was paused for recovery.'
1577
+ : 'Persisted running Build was recovered as interrupted.',
1578
+ mode: 'build',
1579
+ model: this.model,
1580
+ timestamp: run.endedAt,
1581
+ workspaceId: run.target.workspaceId,
1582
+ runtimeKey: run.runtimeKey,
1583
+ runId: run.runId,
1584
+ sequence,
1585
+ status: 'interrupted',
1586
+ });
1547
1587
  changed = true;
1548
1588
  }
1589
+ if (preservedLiveRun && !changed)
1590
+ return { runs: normalized, changed: false };
1549
1591
  return { runs: normalized, changed };
1550
1592
  }
1593
+ pauseFlowAfterUnexpectedExit() {
1594
+ if (this.mode !== 'flow' || !this.flow?.name)
1595
+ return false;
1596
+ if (this.getStoredFlowSuspension(this.activeConversationId)) {
1597
+ this.status = 'idle';
1598
+ return true;
1599
+ }
1600
+ const target = this.currentConversationTarget();
1601
+ this.saveStoredFlowSuspension({
1602
+ workflowName: this.flow.name,
1603
+ componentId: Math.max(0, Math.floor(Number(this.flowPc) || 0)),
1604
+ input: String([...this.chatMessages].reverse().find(message => message.role === 'user')?.content || ''),
1605
+ completedResults: [],
1606
+ previousMode: 'build',
1607
+ reason: 'interrupted',
1608
+ message: 'Flow paused after an unexpected runtime exit. Resume it explicitly.',
1609
+ target,
1610
+ updatedAt: this.nowIso(),
1611
+ }, this.activeConversationId);
1612
+ this.status = 'idle';
1613
+ return true;
1614
+ }
1551
1615
  beginConversationWorkRun(runId, target = this.currentConversationTarget(), startedAt = this.nowIso(), managed = false, runtimeKey) {
1552
1616
  const cleanRunId = String(runId || crypto.randomUUID()).trim().slice(0, 200);
1553
1617
  const existing = this.workRuns.find(run => run.runId === cleanRunId);
@@ -1565,6 +1629,9 @@ class Agent {
1565
1629
  runId: cleanRunId,
1566
1630
  target: normalizedTarget,
1567
1631
  runtimeKey: String(runtimeKey || (0, conversationTarget_1.conversationRuntimeKey)(normalizedTarget)),
1632
+ runtimeOwnerId: this.runtimeLifecycle.ownerId,
1633
+ runtimeOwnerPid: this.runtimeLifecycle.pid,
1634
+ runtimeLifecycleRole: this.runtimeLifecycleRole,
1568
1635
  status: 'running',
1569
1636
  startedAt,
1570
1637
  expanded: true,
@@ -1579,10 +1646,186 @@ class Agent {
1579
1646
  if (managed)
1580
1647
  this.managedWorkRunIds.add(run.runId);
1581
1648
  this.activeWorkRunId = run.runId;
1649
+ this.ensureBuildHistoryBlock(run);
1582
1650
  if (this.agentRunService)
1583
1651
  this.createAgentRunForWorkRun(run, normalizedTarget);
1584
1652
  return run;
1585
1653
  }
1654
+ buildHistoryStatus(status) {
1655
+ if (status === 'completed')
1656
+ return 'completed';
1657
+ if (status === 'error')
1658
+ return 'failed';
1659
+ if (status === 'force_interrupted')
1660
+ return 'cancelled';
1661
+ return 'paused';
1662
+ }
1663
+ ensureBuildHistoryBlock(run) {
1664
+ if (!this.contextV2.flags.buildHistoryPersistence)
1665
+ return null;
1666
+ try {
1667
+ const repository = this.contextV2.buildHistory;
1668
+ const existing = repository.readBlock(run.runId);
1669
+ if (existing)
1670
+ return existing;
1671
+ const block = {
1672
+ id: run.runId,
1673
+ conversationId: run.target.conversationId,
1674
+ branchId: run.branchNodeId || `conversation:${run.target.conversationId}`,
1675
+ parentBuildBlockId: null,
1676
+ startupInput: this.sanitizePublicWorkContent(run.primaryPrompt || '(user input unavailable)').slice(0, 50_000),
1677
+ status: 'active',
1678
+ createdAt: run.startedAt,
1679
+ startedAt: run.startedAt,
1680
+ completedAt: null,
1681
+ revision: 1,
1682
+ activeCheckpointId: null,
1683
+ tokenBudgetPolicyId: 'default',
1684
+ metadata: {
1685
+ runtimeKey: run.runtimeKey,
1686
+ conversationWorkRunId: run.runId,
1687
+ },
1688
+ };
1689
+ repository.saveBlock(block);
1690
+ return block;
1691
+ }
1692
+ catch (error) {
1693
+ console.error(`[NewmarkBuildHistory] unable to create Build Block ${run.runId}: ${error instanceof Error ? error.message : String(error)}`);
1694
+ return null;
1695
+ }
1696
+ }
1697
+ finalWorkRunSummary(run) {
1698
+ const finalEvent = [...run.events].reverse().find(event => event.type === 'final_response' || event.type === 'response');
1699
+ if (finalEvent?.content)
1700
+ return this.sanitizePublicWorkContent(finalEvent.content).slice(0, 4_000);
1701
+ const finalMessage = [...this.chatMessages].reverse().find(message => message.role === 'assistant' && message.runId === run.runId);
1702
+ return this.sanitizePublicWorkContent(finalMessage?.content || '(no final summary)').slice(0, 4_000);
1703
+ }
1704
+ auditGoalAtWorkRunEnd(run, status, endedAt) {
1705
+ if (!this.goal)
1706
+ return { tracked: false, checked: false, matched: false, completed: false, checkedAt: endedAt };
1707
+ const objective = this.goal.objective;
1708
+ const pausedBeforeSync = this.goal.paused;
1709
+ const matched = this.goal.checkComplete(this.finalWorkRunSummary(run));
1710
+ const completed = status === 'completed' && matched && !pausedBeforeSync;
1711
+ if (completed)
1712
+ this.markGoalComplete();
1713
+ return {
1714
+ tracked: true,
1715
+ checked: true,
1716
+ matched,
1717
+ completed,
1718
+ objective,
1719
+ paused: completed ? false : pausedBeforeSync,
1720
+ checkedAt: endedAt,
1721
+ };
1722
+ }
1723
+ enforceGoalTerminalInvariant(status, goalAudit) {
1724
+ if (!this.goal)
1725
+ return;
1726
+ const continuationIsOwned = status === 'completed' && !goalAudit.completed && !!this.goalContinuationGate;
1727
+ if (continuationIsOwned)
1728
+ return;
1729
+ if (!this.goal.paused) {
1730
+ this.goal.paused = true;
1731
+ this.status = 'goal_paused';
1732
+ this.saveWorkspaceConversationState(true);
1733
+ }
1734
+ goalAudit.paused = this.goal.paused;
1735
+ }
1736
+ persistBuildBlockWorkOverview(run, status, endedAt, goalAudit) {
1737
+ if (!this.contextV2.flags.buildHistoryPersistence)
1738
+ return;
1739
+ try {
1740
+ const repository = this.contextV2.buildHistory;
1741
+ let block = this.ensureBuildHistoryBlock(run);
1742
+ if (!block)
1743
+ return;
1744
+ const nextStatus = this.buildHistoryStatus(status);
1745
+ const nextCompletedAt = nextStatus === 'active' ? null : endedAt;
1746
+ const startupInput = this.sanitizePublicWorkContent(run.primaryPrompt || block.startupInput || '(user input unavailable)').slice(0, 50_000);
1747
+ if (block.status !== nextStatus || block.completedAt !== nextCompletedAt || block.startupInput !== startupInput) {
1748
+ const transitioned = repository.transitionBlock(block, {
1749
+ startupInput,
1750
+ status: nextStatus,
1751
+ completedAt: nextCompletedAt,
1752
+ metadata: {
1753
+ ...block.metadata,
1754
+ terminalWorkRunStatus: status,
1755
+ terminalAt: endedAt,
1756
+ },
1757
+ }, {
1758
+ expectedRevision: block.revision,
1759
+ operationId: `work-run-terminal:${run.runId}:${status}:${endedAt}`,
1760
+ });
1761
+ block = transitioned.applied ? transitioned.block : (repository.readBlock(run.runId) || block);
1762
+ }
1763
+ const eventCounts = run.events.reduce((counts, event) => {
1764
+ counts[event.type] = (counts[event.type] || 0) + 1;
1765
+ return counts;
1766
+ }, {});
1767
+ const guides = run.guides.slice(-8).map(guide => `${guide.status}: ${this.sanitizePublicWorkContent(String(guide.content || '')).slice(0, 500)}`);
1768
+ const finalSummary = this.finalWorkRunSummary(run);
1769
+ const goalLine = goalAudit.tracked
1770
+ ? `Goal: checked=${goalAudit.checked}; matched=${goalAudit.matched}; completed=${goalAudit.completed}; paused=${goalAudit.paused}; objective=${goalAudit.objective}`
1771
+ : 'Goal: not tracked for this Build Block.';
1772
+ const content = [
1773
+ 'Build Block Work Overview',
1774
+ `Run: ${run.runId}`,
1775
+ `Status: ${status}`,
1776
+ `Conversation: ${run.target.conversationId}`,
1777
+ `Startup input: ${this.sanitizePublicWorkContent(run.primaryPrompt || block.startupInput || '(user input unavailable)').slice(0, 2_000)}`,
1778
+ `Final summary: ${finalSummary}`,
1779
+ `Events: ${JSON.stringify(eventCounts)}`,
1780
+ guides.length ? `Guides: ${guides.join(' | ')}` : 'Guides: none',
1781
+ goalLine,
1782
+ ].join('\n');
1783
+ repository.appendEntry({
1784
+ buildBlockId: run.runId,
1785
+ type: 'work_overview',
1786
+ content: content.slice(0, 50_000),
1787
+ source: 'agent',
1788
+ importance: status === 'completed' ? 'normal' : 'high',
1789
+ structuredData: {
1790
+ conversationId: run.target.conversationId,
1791
+ branchId: block.branchId,
1792
+ status,
1793
+ endedAt,
1794
+ finalSummary,
1795
+ eventCounts,
1796
+ guideCount: run.guides.length,
1797
+ goal: goalAudit,
1798
+ },
1799
+ operationId: `work-run-overview:${run.runId}`,
1800
+ revision: block.revision,
1801
+ });
1802
+ }
1803
+ catch (error) {
1804
+ console.error(`[NewmarkBuildHistory] unable to write Build Block overview ${run.runId}: ${error instanceof Error ? error.message : String(error)}`);
1805
+ }
1806
+ }
1807
+ recoverMissingTerminalBuildOverviews() {
1808
+ if (!this.contextV2.flags.buildHistoryPersistence)
1809
+ return;
1810
+ const repository = this.contextV2.buildHistory;
1811
+ for (const run of this.workRuns) {
1812
+ if (run.status === 'running')
1813
+ continue;
1814
+ try {
1815
+ if (repository.readEntries(run.runId).some(entry => entry.type === 'work_overview'))
1816
+ continue;
1817
+ }
1818
+ catch {
1819
+ continue;
1820
+ }
1821
+ const endedAt = run.endedAt || this.nowIso();
1822
+ if (run.status === 'completed')
1823
+ this.ensureCompletedWorkRunFinalResult(run);
1824
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, run.status, endedAt);
1825
+ this.enforceGoalTerminalInvariant(run.status, goalAudit);
1826
+ this.persistBuildBlockWorkOverview(run, run.status, endedAt, goalAudit);
1827
+ }
1828
+ }
1586
1829
  createAgentRunForWorkRun(run, target) {
1587
1830
  if (!this.agentRunService)
1588
1831
  return;
@@ -1825,10 +2068,22 @@ class Agent {
1825
2068
  return false;
1826
2069
  this.syncAgentRunTerminal(run.runId, status, endedAt);
1827
2070
  if (run.status !== 'running') {
1828
- if (run.status !== 'interrupted' || status !== 'force_interrupted')
1829
- return run.status === status;
2071
+ if (run.status !== 'interrupted' || status !== 'force_interrupted') {
2072
+ if (run.status !== status)
2073
+ return false;
2074
+ const terminalAt = run.endedAt || endedAt;
2075
+ if (status === 'completed')
2076
+ this.ensureCompletedWorkRunFinalResult(run);
2077
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, status, terminalAt);
2078
+ this.enforceGoalTerminalInvariant(status, goalAudit);
2079
+ this.persistBuildBlockWorkOverview(run, status, terminalAt, goalAudit);
2080
+ this.saveWorkspaceConversationState();
2081
+ return true;
2082
+ }
1830
2083
  this.activeWorkRunId = run.runId;
1831
2084
  this.finalizingWorkRunId = run.runId;
2085
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, status, endedAt);
2086
+ this.enforceGoalTerminalInvariant(status, goalAudit);
1832
2087
  this.emitWorkEvent({
1833
2088
  type: 'status',
1834
2089
  content: 'Force interrupted.',
@@ -1843,6 +2098,7 @@ class Agent {
1843
2098
  this.activeWorkRunId = '';
1844
2099
  this.finalizingWorkRunId = '';
1845
2100
  this.managedWorkRunIds.delete(run.runId);
2101
+ this.persistBuildBlockWorkOverview(run, status, endedAt, goalAudit);
1846
2102
  this.saveWorkspaceConversationState();
1847
2103
  return true;
1848
2104
  }
@@ -1850,6 +2106,8 @@ class Agent {
1850
2106
  this.finalizingWorkRunId = run.runId;
1851
2107
  if (status === 'completed')
1852
2108
  this.ensureCompletedWorkRunFinalResult(run);
2109
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, status, endedAt);
2110
+ this.enforceGoalTerminalInvariant(status, goalAudit);
1853
2111
  this.emitWorkEvent({
1854
2112
  type: status === 'completed' ? 'done' : status === 'error' ? 'error' : 'status',
1855
2113
  content: status === 'force_interrupted' ? 'Force interrupted.' : status === 'interrupted' ? 'Interrupted.' : 'Response complete.',
@@ -1864,6 +2122,7 @@ class Agent {
1864
2122
  this.activeWorkRunId = '';
1865
2123
  this.finalizingWorkRunId = '';
1866
2124
  this.managedWorkRunIds.delete(run.runId);
2125
+ this.persistBuildBlockWorkOverview(run, status, endedAt, goalAudit);
1867
2126
  this.saveWorkspaceConversationState();
1868
2127
  return true;
1869
2128
  }
@@ -1970,6 +2229,14 @@ class Agent {
1970
2229
  this.saveWorkspaceConversationState(false);
1971
2230
  if (event.type === 'done' || event.type === 'error')
1972
2231
  this.saveWorkspaceConversationState(true);
2232
+ if (activeRun && this.finalizingWorkRunId !== activeRun.runId
2233
+ && (event.type === 'done' || event.type === 'error') && activeRun.status !== 'running') {
2234
+ const terminalStatus = activeRun.status;
2235
+ const terminalAt = activeRun.endedAt || this.nowIso();
2236
+ const goalAudit = this.auditGoalAtWorkRunEnd(activeRun, terminalStatus, terminalAt);
2237
+ this.enforceGoalTerminalInvariant(terminalStatus, goalAudit);
2238
+ this.persistBuildBlockWorkOverview(activeRun, terminalStatus, terminalAt, goalAudit);
2239
+ }
1973
2240
  return event;
1974
2241
  }
1975
2242
  appendWorkflowMessage(content, toolName, toolArgs, persist = true) {
@@ -3099,6 +3366,17 @@ class Agent {
3099
3366
  }
3100
3367
  return cleaned;
3101
3368
  }
3369
+ activeConversationRuntimeOwner() {
3370
+ const activeGoal = !!this.goal && !this.goal.paused;
3371
+ const activeFlow = this.mode === 'flow' && !!this.flow?.name && !this.getStoredFlowSuspension(this.activeConversationId);
3372
+ if (!activeGoal && !activeFlow)
3373
+ return {};
3374
+ return {
3375
+ runtimeOwnerId: this.runtimeLifecycle.ownerId,
3376
+ runtimeOwnerPid: this.runtimeLifecycle.pid,
3377
+ runtimeLifecycleRole: this.runtimeLifecycleRole,
3378
+ };
3379
+ }
3102
3380
  saveWorkspaceConversationState(flush = true) {
3103
3381
  if (this.isSubagentRuntime)
3104
3382
  return;
@@ -3106,9 +3384,11 @@ class Agent {
3106
3384
  if (!key)
3107
3385
  return;
3108
3386
  const updatedAt = new Date().toISOString();
3387
+ const runtimeOwner = this.activeConversationRuntimeOwner();
3109
3388
  this.workspaceConversations.set(key, {
3110
3389
  chatMessages: [...this.chatMessages],
3111
3390
  history: [...this.history],
3391
+ compressionCache: [...this.compressionCache],
3112
3392
  plan: this.normalizeConversationPlan(this.conversationPlan),
3113
3393
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3114
3394
  subagentState: this.subagents.serialize(),
@@ -3119,6 +3399,9 @@ class Agent {
3119
3399
  inputMode: this.inputMode,
3120
3400
  mode: this.mode,
3121
3401
  goal: this.serializeGoal(),
3402
+ runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3403
+ runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3404
+ runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
3122
3405
  updatedAt,
3123
3406
  });
3124
3407
  const stored = this.readStoredConversationState();
@@ -3138,6 +3421,7 @@ class Agent {
3138
3421
  title,
3139
3422
  chatMessages: [...this.chatMessages],
3140
3423
  history: [...this.history],
3424
+ compressionCache: [...this.compressionCache],
3141
3425
  plan: this.normalizeConversationPlan(this.conversationPlan),
3142
3426
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3143
3427
  subagentState: this.subagents.serialize(),
@@ -3148,6 +3432,9 @@ class Agent {
3148
3432
  inputMode: this.inputMode,
3149
3433
  mode: this.mode,
3150
3434
  goal: this.serializeGoal(),
3435
+ runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3436
+ runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3437
+ runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
3151
3438
  updatedAt,
3152
3439
  };
3153
3440
  if (nextEntry.tree || nextEntry.branches?.length) {
@@ -3167,6 +3454,8 @@ class Agent {
3167
3454
  if (!key) {
3168
3455
  this.chatMessages = [];
3169
3456
  this.history = [];
3457
+ this.compressionCache = [];
3458
+ this.nextCompressionCacheId = 1;
3170
3459
  this.conversationPlan = { items: [] };
3171
3460
  this.linkedPlan = { markdown: '', revision: 0 };
3172
3461
  this.workRuns = [];
@@ -3183,6 +3472,8 @@ class Agent {
3183
3472
  const saved = this.workspaceConversations.get(key);
3184
3473
  if (saved) {
3185
3474
  this.history = [...saved.history];
3475
+ this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
3476
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
3186
3477
  this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
3187
3478
  this.conversationPlan = this.normalizeConversationPlan(saved.plan);
3188
3479
  this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
@@ -3195,6 +3486,7 @@ class Agent {
3195
3486
  this.mode = saved.mode || 'build';
3196
3487
  this.goal = this.restoreGoal(saved.goal);
3197
3488
  this.status = this.restoreStatusFromWorkRuns(saved.goal);
3489
+ this.recoverMissingTerminalBuildOverviews();
3198
3490
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
3199
3491
  return;
3200
3492
  }
@@ -3202,10 +3494,12 @@ class Agent {
3202
3494
  const stateKey = this.workspaceConversationStateKey();
3203
3495
  const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
3204
3496
  this.history = persisted?.history ? [...persisted.history] : [];
3497
+ this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
3498
+ this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
3205
3499
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
3206
3500
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
3207
3501
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
3208
- const recoveredWorkRuns = this.recoverPersistedWorkRuns(persisted?.workRuns, persisted?.updatedAt);
3502
+ const recoveredWorkRuns = this.recoverPersistedWorkRuns(persisted?.workRuns, persisted?.updatedAt, this.runtimeLifecycle.unexpectedExit);
3209
3503
  this.workRuns = recoveredWorkRuns.runs;
3210
3504
  this.continuations = this.normalizeContinuations(persisted?.continuations);
3211
3505
  this.restoreConversationModelSelection(persisted?.modelSelection);
@@ -3213,33 +3507,64 @@ class Agent {
3213
3507
  this.inputMode = this.defaultInputMode();
3214
3508
  this.mode = persisted?.mode || 'build';
3215
3509
  this.goal = this.restoreGoal(persisted?.goal);
3216
- this.status = this.restoreStatusFromWorkRuns(persisted?.goal);
3510
+ const persistedRuntimeOwnerPid = Math.floor(Number(persisted?.runtimeOwnerPid) || 0);
3511
+ const persistedRuntimeOwnerKnown = persistedRuntimeOwnerPid > 0;
3512
+ const persistedRuntimeOwnerAlive = persistedRuntimeOwnerKnown && (0, runtimeLifecycle_1.isRuntimeProcessAlive)(persistedRuntimeOwnerPid);
3513
+ const runtimeOwnerLost = persistedRuntimeOwnerKnown ? !persistedRuntimeOwnerAlive : this.runtimeLifecycle.unexpectedExit;
3514
+ const shouldPauseRecoveredState = recoveredWorkRuns.changed || runtimeOwnerLost;
3515
+ let goalPausedByRecovery = false;
3516
+ if (shouldPauseRecoveredState && this.goal && !this.goal.paused) {
3517
+ this.goal.paused = true;
3518
+ goalPausedByRecovery = true;
3519
+ }
3520
+ this.status = this.restoreStatusFromWorkRuns(this.serializeGoal());
3521
+ const flowPausedByRecovery = shouldPauseRecoveredState && this.pauseFlowAfterUnexpectedExit();
3217
3522
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
3218
3523
  this.bindConversationSubagents(this.activeConversationId, persisted?.subagentState);
3524
+ if (recoveredWorkRuns.changed) {
3525
+ for (const run of recoveredWorkRuns.runs.filter(item => item.status === 'interrupted')) {
3526
+ const audit = this.auditGoalAtWorkRunEnd(run, 'interrupted', run.endedAt || this.nowIso());
3527
+ this.persistBuildBlockWorkOverview(run, 'interrupted', run.endedAt || this.nowIso(), audit);
3528
+ }
3529
+ }
3530
+ this.recoverMissingTerminalBuildOverviews();
3531
+ const recoveryApplied = recoveredWorkRuns.changed || runtimeOwnerLost || goalPausedByRecovery || flowPausedByRecovery;
3532
+ const recoveryAt = recoveryApplied ? this.nowIso() : persisted?.updatedAt;
3219
3533
  this.workspaceConversations.set(key, {
3220
3534
  chatMessages: [...this.chatMessages],
3221
3535
  history: [...this.history],
3536
+ compressionCache: [...this.compressionCache],
3222
3537
  plan: this.normalizeConversationPlan(this.conversationPlan),
3223
3538
  linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
3224
3539
  subagentState: this.subagents.serialize(),
3225
3540
  workRuns: this.normalizeWorkRuns(this.workRuns),
3226
3541
  continuations: this.normalizeContinuations(this.continuations),
3227
3542
  modelSelection: persisted?.modelSelection || this.currentConversationModelSelection(),
3228
- flowSelection: persisted?.flowSelection || null,
3543
+ flowSelection: this.currentConversationFlowSelection(),
3229
3544
  inputMode: this.defaultInputMode(),
3230
- mode: persisted?.mode || 'build',
3231
- goal: persisted?.goal || null,
3232
- updatedAt: persisted?.updatedAt,
3545
+ mode: this.mode,
3546
+ goal: this.serializeGoal(),
3547
+ runtimeOwnerId: this.activeConversationRuntimeOwner().runtimeOwnerId,
3548
+ runtimeOwnerPid: this.activeConversationRuntimeOwner().runtimeOwnerPid,
3549
+ runtimeLifecycleRole: this.activeConversationRuntimeOwner().runtimeLifecycleRole,
3550
+ updatedAt: recoveryAt,
3233
3551
  });
3234
- if (recoveredWorkRuns.changed && stateKey && persisted) {
3235
- const recoveredAt = this.nowIso();
3236
- stored.conversations[stateKey] = {
3237
- ...persisted,
3552
+ if (recoveryApplied && stateKey && persisted) {
3553
+ const recoveredStored = this.readStoredConversationState();
3554
+ recoveredStored.conversations = recoveredStored.conversations || {};
3555
+ recoveredStored.conversations[stateKey] = {
3556
+ ...(recoveredStored.conversations[stateKey] || persisted),
3238
3557
  workRuns: this.normalizeWorkRuns(this.workRuns),
3239
- updatedAt: recoveredAt,
3558
+ flowSelection: this.currentConversationFlowSelection(),
3559
+ mode: this.mode,
3560
+ goal: this.serializeGoal(),
3561
+ runtimeOwnerId: this.activeConversationRuntimeOwner().runtimeOwnerId,
3562
+ runtimeOwnerPid: this.activeConversationRuntimeOwner().runtimeOwnerPid,
3563
+ runtimeLifecycleRole: this.activeConversationRuntimeOwner().runtimeLifecycleRole,
3564
+ updatedAt: recoveryAt,
3240
3565
  };
3241
- this.workspaceConversations.get(key).updatedAt = recoveredAt;
3242
- this.writeStoredConversationStateNow(stored);
3566
+ this.workspaceConversations.get(key).updatedAt = recoveryAt;
3567
+ this.writeStoredConversationStateNow(recoveredStored);
3243
3568
  }
3244
3569
  }
3245
3570
  applyWorkspaceContext(ws) {
@@ -3466,7 +3791,7 @@ class Agent {
3466
3791
  catch { }
3467
3792
  const action = String(input.action || '').trim();
3468
3793
  if (!action)
3469
- return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize).', error: 'action is required.' };
3794
+ return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize|restore|search|read|status).', error: 'action is required.' };
3470
3795
  if (action === 'list') {
3471
3796
  const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
3472
3797
  const entries = this.history.slice(0, limit).map((message, index) => ({
@@ -3489,11 +3814,19 @@ class Agent {
3489
3814
  metadata: { kind: 'context-history-list' },
3490
3815
  };
3491
3816
  }
3817
+ const protectedZone = this.contextHistoryProtectedZone();
3492
3818
  if (action === 'remove') {
3493
3819
  const position = Math.floor(Number(input.position));
3494
3820
  if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
3495
3821
  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
3822
  }
3823
+ if (protectedZone.has(position) && !Boolean(input.dangerous)) {
3824
+ return {
3825
+ ok: false,
3826
+ output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
3827
+ error: 'remove position is in the protected context zone.',
3828
+ };
3829
+ }
3497
3830
  const removed = this.history.splice(position, 1)[0];
3498
3831
  this.saveWorkspaceConversationState(true);
3499
3832
  return {
@@ -3517,11 +3850,20 @@ class Agent {
3517
3850
  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
3851
  if (to - from < 1)
3519
3852
  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.' };
3853
+ const protectedHit = this.history.slice(from, to + 1).some((_, index) => protectedZone.has(from + index));
3854
+ if (protectedHit && !Boolean(input.dangerous)) {
3855
+ return {
3856
+ ok: false,
3857
+ output: '[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.',
3858
+ error: 'summarize range overlaps the protected context zone.',
3859
+ };
3860
+ }
3520
3861
  const segment = this.history.slice(from, to + 1);
3521
3862
  const chars = segment.reduce((sum, message) => sum + (typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length), 0);
3522
3863
  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
3864
  const replacement = { role: 'system', content: `[Context History Summary]\n${summary}` };
3524
3865
  this.history.splice(from, to - from + 1, replacement);
3866
+ this.pushCompressionCacheEntry(`[Context History Summary]\n${summary}`, segment, 'local-summarize', true);
3525
3867
  this.saveWorkspaceConversationState(true);
3526
3868
  return {
3527
3869
  ok: true,
@@ -3539,6 +3881,196 @@ class Agent {
3539
3881
  metadata: { kind: 'context-history-summarize' },
3540
3882
  };
3541
3883
  }
3884
+ if (action === 'restore') {
3885
+ const restoreId = String(input.restore_id || '').trim();
3886
+ const hotEntry = this.compressionCache.find(item => item.id === restoreId);
3887
+ const coldEntry = hotEntry ? undefined : this.coldCompressionEntries().find(item => item.id === restoreId);
3888
+ const entry = hotEntry || coldEntry;
3889
+ if (!entry)
3890
+ return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
3891
+ const summaryHeader = entry.summary.startsWith('[Context Compression') ? '[Context Compression' : '[Context History Summary]';
3892
+ const markerIndex = this.history.findIndex(message => String(message.role || '') === 'system' && String(message.content || '').includes(summaryHeader) && String(message.content || '').includes(entry.summary.slice(0, 200)));
3893
+ if (markerIndex < 0) {
3894
+ 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.' };
3895
+ }
3896
+ this.history.splice(markerIndex, 1, ...entry.messages.map(message => ({ ...message })));
3897
+ if (hotEntry)
3898
+ this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
3899
+ if (coldEntry)
3900
+ this.markColdCompressionRestored(entry.id);
3901
+ this.saveWorkspaceConversationState(true);
3902
+ return {
3903
+ ok: true,
3904
+ output: JSON.stringify({
3905
+ ok: true,
3906
+ action: 'restore',
3907
+ restoreId: entry.id,
3908
+ restoredEntries: entry.messages.length,
3909
+ restoredChars: entry.foldedChars,
3910
+ source: hotEntry ? 'hot-cache' : 'cold-archive',
3911
+ cacheRemaining: this.compressionCache.length,
3912
+ archiveRemaining: this.coldCompressionEntries().length,
3913
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3914
+ }, null, 2),
3915
+ metadata: { kind: 'context-history-restore' },
3916
+ };
3917
+ }
3918
+ if (action === 'search') {
3919
+ const query = String(input.query || '').trim().toLowerCase();
3920
+ const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
3921
+ if (!query)
3922
+ return { ok: false, output: '[context_history_manage] search requires query.', error: 'search requires query.' };
3923
+ const matches = [];
3924
+ const coldEntries = this.coldCompressionEntries();
3925
+ const searchable = [
3926
+ ...this.compressionCache.map(entry => ({ entry, source: 'hot-cache' })),
3927
+ ...coldEntries.map(entry => ({ entry, source: 'cold-archive' })),
3928
+ ];
3929
+ for (const item of searchable) {
3930
+ if (matches.length >= limit)
3931
+ break;
3932
+ const { entry } = item;
3933
+ const hit = { cacheId: entry.id, source: item.source, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
3934
+ if (entry.summary.toLowerCase().includes(query)) {
3935
+ hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
3936
+ }
3937
+ entry.messages.forEach((message, index) => {
3938
+ if (matches.length >= limit || hit.matches.length >= 40)
3939
+ return;
3940
+ const content = this.compressionHistoryContent(message.content || message.reasoning_content || '');
3941
+ if (content.toLowerCase().includes(query)) {
3942
+ hit.matches.push({ index, snippet: this.snippetAround(content, query) });
3943
+ }
3944
+ });
3945
+ if (hit.matches.length)
3946
+ matches.push(hit);
3947
+ }
3948
+ return {
3949
+ ok: true,
3950
+ output: JSON.stringify({
3951
+ ok: true,
3952
+ action: 'search',
3953
+ query: String(input.query || ''),
3954
+ cacheEntries: this.compressionCache.length,
3955
+ archiveEntries: coldEntries.length,
3956
+ matches,
3957
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3958
+ }, null, 2),
3959
+ metadata: { kind: 'context-history-search' },
3960
+ };
3961
+ }
3962
+ if (action === 'read') {
3963
+ const restoreId = String(input.restore_id || '').trim();
3964
+ if (!restoreId)
3965
+ return { ok: false, output: '[context_history_manage] read requires restore_id.', error: 'read requires restore_id.' };
3966
+ const hotEntry = this.compressionCache.find(item => item.id === restoreId);
3967
+ const coldEntry = hotEntry ? undefined : this.coldCompressionEntries().find(item => item.id === restoreId);
3968
+ const entry = hotEntry || coldEntry;
3969
+ if (!entry)
3970
+ return { ok: false, output: `[context_history_manage] read unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
3971
+ const offset = Math.max(0, Math.floor(Number(input.offset || 0)));
3972
+ const contentOffset = Math.max(0, Math.floor(Number(input.content_offset || 0)));
3973
+ const limit = Math.max(1, Math.min(100, Math.floor(Number(input.limit || 20))));
3974
+ const maxChars = Math.max(1_000, Math.min(60_000, Math.floor(Number(input.max_chars || 12_000))));
3975
+ const messages = [];
3976
+ let emittedChars = 0;
3977
+ let nextOffset = null;
3978
+ let nextContentOffset = 0;
3979
+ for (let index = offset; index < entry.messages.length && messages.length < limit; index += 1) {
3980
+ const message = entry.messages[index];
3981
+ const remaining = maxChars - emittedChars;
3982
+ if (remaining <= 0)
3983
+ break;
3984
+ const fullContent = this.compressionHistoryContent(message.content || message.reasoning_content || '');
3985
+ const start = index === offset ? Math.min(contentOffset, fullContent.length) : 0;
3986
+ const content = fullContent.slice(start, start + remaining);
3987
+ const truncated = start + content.length < fullContent.length;
3988
+ messages.push({ index, role: String(message.role || ''), name: String(message.name || ''), contentOffset: start, content, truncated });
3989
+ emittedChars += content.length;
3990
+ if (truncated) {
3991
+ nextOffset = index;
3992
+ nextContentOffset = start + content.length;
3993
+ break;
3994
+ }
3995
+ }
3996
+ if (nextOffset === null && offset + messages.length < entry.messages.length)
3997
+ nextOffset = offset + messages.length;
3998
+ return {
3999
+ ok: true,
4000
+ output: JSON.stringify({
4001
+ ok: true,
4002
+ action: 'read',
4003
+ restoreId: entry.id,
4004
+ source: hotEntry ? 'hot-cache' : 'cold-archive',
4005
+ at: entry.at,
4006
+ foldedEntries: entry.foldedEntries,
4007
+ foldedChars: entry.foldedChars,
4008
+ offset,
4009
+ contentOffset,
4010
+ returned: messages.length,
4011
+ nextOffset,
4012
+ nextContentOffset: nextOffset === null ? null : nextContentOffset,
4013
+ summary: entry.summary.slice(0, 2_000),
4014
+ messages,
4015
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
4016
+ }, null, 2),
4017
+ metadata: { kind: 'context-history-read' },
4018
+ };
4019
+ }
4020
+ if (action === 'status') {
4021
+ const budget = this.compressionBudget(this.history);
4022
+ const estimatedTokens = this.estimateContextTokens(this.history);
4023
+ const maxTokens = this.contextMaxTokens();
4024
+ const protectedStartIndex = this.contextHistoryProtectedStartIndex();
4025
+ const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
4026
+ const coldEntries = this.coldCompressionEntries();
4027
+ return {
4028
+ ok: true,
4029
+ output: JSON.stringify({
4030
+ ok: true,
4031
+ action: 'status',
4032
+ historyLength: this.history.length,
4033
+ chatMessages: this.chatMessages.length,
4034
+ estimatedTokens,
4035
+ maxTokens,
4036
+ triggerTokens: budget.triggerTokens,
4037
+ targetTokens: budget.targetTokens,
4038
+ summaryTokens: budget.summaryTokens,
4039
+ usagePercent: maxTokens > 0 ? Math.round((estimatedTokens / maxTokens) * 1000) / 10 : 0,
4040
+ thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
4041
+ keepRecentMessages: this.config.getNum('context', 'keep_recent_messages') || 10,
4042
+ lastCompression: this.lastCompression ? {
4043
+ at: this.lastCompression.at,
4044
+ originalMessages: this.lastCompression.originalMessages,
4045
+ compressedMessages: this.lastCompression.compressedMessages,
4046
+ compressedTokens: this.lastCompression.compressedTokens,
4047
+ model: this.lastCompression.model,
4048
+ fallback: this.lastCompression.fallback,
4049
+ } : null,
4050
+ cache: {
4051
+ entries: this.compressionCache.length,
4052
+ totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
4053
+ totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
4054
+ ids: this.compressionCache.map(item => item.id),
4055
+ },
4056
+ archive: {
4057
+ enabled: this.config.getBool('context', 'compression_archive_enabled'),
4058
+ entries: coldEntries.length,
4059
+ totalFoldedEntries: coldEntries.reduce((sum, item) => sum + item.foldedEntries, 0),
4060
+ totalFoldedChars: coldEntries.reduce((sum, item) => sum + item.foldedChars, 0),
4061
+ recentIds: coldEntries.slice(-50).map(item => item.id),
4062
+ },
4063
+ protectedZone: {
4064
+ preserveRecentMessages: this.config.getNum('context', 'preserve_recent_messages') || 5,
4065
+ protectedStartIndex,
4066
+ lastUserMessageIndex: lastUserIndex,
4067
+ protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0,
4068
+ },
4069
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
4070
+ }, null, 2),
4071
+ metadata: { kind: 'context-history-status' },
4072
+ };
4073
+ }
3542
4074
  return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
3543
4075
  }
3544
4076
  recordContextCompressionStep() {
@@ -3980,6 +4512,7 @@ class Agent {
3980
4512
  model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
3981
4513
  fallback: fallbackUsed,
3982
4514
  };
4515
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed);
3983
4516
  this.persistCompressedHistory(summary, recent.length, candidate);
3984
4517
  this.saveWorkspaceConversationState(true);
3985
4518
  return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -4016,6 +4549,7 @@ class Agent {
4016
4549
  model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
4017
4550
  fallback: fallbackUsed || droppedMessages > 0,
4018
4551
  };
4552
+ this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed || droppedMessages > 0);
4019
4553
  this.persistCompressedHistory(summary, recent.length, candidate);
4020
4554
  this.saveWorkspaceConversationState(true);
4021
4555
  return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
@@ -4046,6 +4580,7 @@ class Agent {
4046
4580
  };
4047
4581
  }
4048
4582
  updateGoal(newGoal) {
4583
+ const existingGoalPaused = this.goal?.paused || false;
4049
4584
  if (this.goal) {
4050
4585
  this.goal.update(newGoal);
4051
4586
  }
@@ -4055,9 +4590,10 @@ class Agent {
4055
4590
  }
4056
4591
  if (this.goal) {
4057
4592
  this.goal.verified = false;
4058
- this.goal.paused = false;
4593
+ // Replacing the objective must not implicitly resume an explicitly paused Goal.
4594
+ this.goal.paused = existingGoalPaused;
4059
4595
  this.goal.goalRounds = 0;
4060
- this.status = 'idle';
4596
+ this.status = this.goal.paused ? 'goal_paused' : 'idle';
4061
4597
  }
4062
4598
  this.mode = 'goal';
4063
4599
  this.saveWorkspaceConversationState(true);
@@ -4070,6 +4606,17 @@ class Agent {
4070
4606
  this.saveWorkspaceConversationState(true);
4071
4607
  return this.goal.paused;
4072
4608
  }
4609
+ /** Pause only in response to the user's explicit Stop action. */
4610
+ pauseGoalForUserInterrupt() {
4611
+ if (!this.goal)
4612
+ return false;
4613
+ if (!this.goal.paused) {
4614
+ this.goal.paused = true;
4615
+ this.status = 'goal_paused';
4616
+ this.saveWorkspaceConversationState(true);
4617
+ }
4618
+ return true;
4619
+ }
4073
4620
  clearGoal() {
4074
4621
  this.goal = null;
4075
4622
  if (this.mode === 'goal')
@@ -4094,8 +4641,8 @@ class Agent {
4094
4641
  canAutoContinueGoal() {
4095
4642
  return this.goalContinuationGate ? this.goalContinuationGate() : true;
4096
4643
  }
4097
- claimGoalContinuationMessage() {
4098
- if (this.mode !== 'goal' || !this.goal || this.goal.paused || !this.canAutoContinueGoal())
4644
+ claimGoalContinuationMessage(options) {
4645
+ if (this.mode !== 'goal' || !this.goal || this.goal.paused || (!options?.force && !this.canAutoContinueGoal()))
4099
4646
  return null;
4100
4647
  const maxGoalContinuations = Math.max(0, Math.floor(this.config.getNum('agent', 'goal_max_continuations') || 0));
4101
4648
  if (maxGoalContinuations > 0 && this.goal.goalRounds >= maxGoalContinuations) {
@@ -6574,6 +7121,7 @@ class Agent {
6574
7121
  model: compression.model,
6575
7122
  fallback: compression.fallback,
6576
7123
  };
7124
+ this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
6577
7125
  this.persistCompressedHistory(compression.summary, recent.length, msgs);
6578
7126
  }
6579
7127
  async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = '') {
@@ -6731,6 +7279,111 @@ class Agent {
6731
7279
  if (this.isSubagentRuntime)
6732
7280
  this.subagentContextPersist?.(this.history.map(message => ({ ...message })), this.lastCompression);
6733
7281
  }
7282
+ compressionArchiveScopeKey() {
7283
+ if (this.isSubagentRuntime || !this.config.getBool('context', 'compression_archive_enabled'))
7284
+ return null;
7285
+ return this.workspaceConversationKey();
7286
+ }
7287
+ coldCompressionEntries() {
7288
+ const scopeKey = this.compressionArchiveScopeKey();
7289
+ if (!scopeKey)
7290
+ return [];
7291
+ try {
7292
+ const hotIds = new Set(this.compressionCache.map(entry => entry.id));
7293
+ return this.compressionHistoryArchive.activeEntries(scopeKey).filter(entry => !hotIds.has(entry.id));
7294
+ }
7295
+ catch {
7296
+ return [];
7297
+ }
7298
+ }
7299
+ archiveColdCompressionEntries(entries) {
7300
+ const scopeKey = this.compressionArchiveScopeKey();
7301
+ if (!scopeKey)
7302
+ return [];
7303
+ const failed = [];
7304
+ for (const entry of entries) {
7305
+ try {
7306
+ this.compressionHistoryArchive.archive(scopeKey, entry);
7307
+ }
7308
+ catch {
7309
+ // Prefer a temporarily oversized hot cache over irreversible history loss.
7310
+ failed.push(entry);
7311
+ }
7312
+ }
7313
+ return failed;
7314
+ }
7315
+ markColdCompressionRestored(id) {
7316
+ const scopeKey = this.compressionArchiveScopeKey();
7317
+ if (!scopeKey)
7318
+ return;
7319
+ try {
7320
+ this.compressionHistoryArchive.markRestored(scopeKey, id);
7321
+ }
7322
+ catch {
7323
+ // The restored context is already authoritative; archive bookkeeping is best-effort.
7324
+ }
7325
+ }
7326
+ pushCompressionCacheEntry(summary, messages, model, fallback) {
7327
+ if (!messages.length)
7328
+ return;
7329
+ const scopeKey = this.compressionArchiveScopeKey();
7330
+ let archivedMaxId = 0;
7331
+ if (scopeKey) {
7332
+ try {
7333
+ archivedMaxId = this.compressionHistoryArchive.maxNumericId(scopeKey);
7334
+ }
7335
+ catch { }
7336
+ }
7337
+ this.nextCompressionCacheId = Math.max(this.nextCompressionCacheId, archivedMaxId + 1);
7338
+ const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === 'string'
7339
+ ? message.content.length
7340
+ : JSON.stringify(message.content || '').length), 0);
7341
+ this.compressionCache.push({
7342
+ id: `ctx-cache-${this.nextCompressionCacheId}`,
7343
+ at: new Date().toISOString(),
7344
+ summary,
7345
+ messages: messages.map(message => ({ ...message })),
7346
+ foldedEntries: messages.length,
7347
+ foldedChars,
7348
+ model,
7349
+ fallback,
7350
+ });
7351
+ this.nextCompressionCacheId += 1;
7352
+ const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
7353
+ if (this.compressionCache.length > maxEntries) {
7354
+ const evicted = this.compressionCache.slice(0, this.compressionCache.length - maxEntries);
7355
+ const failed = this.archiveColdCompressionEntries(evicted);
7356
+ this.compressionCache = [...failed, ...this.compressionCache.slice(this.compressionCache.length - maxEntries)];
7357
+ }
7358
+ this.saveWorkspaceConversationState(true);
7359
+ }
7360
+ contextHistoryProtectedStartIndex() {
7361
+ const preserve = Math.max(0, Math.floor(this.config.getNum('context', 'preserve_recent_messages') || 5));
7362
+ const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
7363
+ const candidates = [];
7364
+ if (preserve > 0 && this.history.length > 0)
7365
+ candidates.push(Math.max(0, this.history.length - preserve));
7366
+ if (lastUserIndex >= 0)
7367
+ candidates.push(lastUserIndex);
7368
+ return candidates.length ? Math.min(...candidates) : -1;
7369
+ }
7370
+ contextHistoryProtectedZone() {
7371
+ const start = this.contextHistoryProtectedStartIndex();
7372
+ const zone = new Set();
7373
+ if (start >= 0)
7374
+ for (let i = start; i < this.history.length; i += 1)
7375
+ zone.add(i);
7376
+ return zone;
7377
+ }
7378
+ snippetAround(content, query, radius = 150) {
7379
+ const text = String(content || '');
7380
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
7381
+ if (index < 0)
7382
+ return text.slice(0, radius * 2);
7383
+ const from = Math.max(0, index - radius);
7384
+ const to = Math.min(text.length, index + query.length + radius);
7385
+ return `${from > 0 ? '…' : ''}${text.slice(from, to).trim()}${to < text.length ? '…' : ''}`;
7386
+ }
6734
7387
  buildSystemPrompt() {
6735
7388
  const cwd = this.workspace.current?.path || this.rootPath;
6736
7389
  const enabledSkills = this.skills.active();