newmark-agent 0.3.8 → 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();
@@ -193,6 +195,7 @@ class Agent {
193
195
  lastCompression = null;
194
196
  compressionCache = [];
195
197
  nextCompressionCacheId = 1;
198
+ compressionHistoryArchive;
196
199
  workspaceConversations = new Map();
197
200
  isSubagentRuntime = false;
198
201
  subagentName = '';
@@ -256,8 +259,10 @@ class Agent {
256
259
  rootInboxListener = (message) => this.deliverRootInboxMessage(message);
257
260
  agentOnly;
258
261
  runtimeActorId;
262
+ runtimeLifecycleRole;
259
263
  /** dev-0.3.0 context system facade (feature-flagged, default off). */
260
264
  contextV2;
265
+ runtimeLifecycle;
261
266
  /** dev-0.3.0 toolchain core (registry + capability catalog). Seeded lazily from cachedToolDefinitions; not consumed by the legacy path. */
262
267
  toolchainCore = null;
263
268
  /**
@@ -277,15 +282,18 @@ class Agent {
277
282
  }
278
283
  constructor(rootPath, options = {}) {
279
284
  this.rootPath = rootPath;
285
+ this.runtimeLifecycle = (0, runtimeLifecycle_1.beginRuntimeLifecycle)(rootPath, options.runtimeLifecycleRole || 'main');
280
286
  this.isSubagentRuntime = !!options.subagent;
281
287
  this.agentOnly = !!options.agentOnly;
282
288
  this.runtimeActorId = options.actorId || exports.ROOT_AGENT_ACTOR_ID;
289
+ this.runtimeLifecycleRole = options.runtimeLifecycleRole || 'main';
283
290
  if (options.conversationId)
284
291
  this.activeConversationId = this.safeConversationId(options.conversationId);
285
292
  this.subagentName = options.subagentName || '';
286
293
  this.subagentPrompt = options.subagentPrompt || '';
287
294
  this.linkedPlanAccess = options.linkedPlanAccess;
288
295
  this.config = new config_1.ConfigManager(rootPath);
296
+ this.compressionHistoryArchive = new compressionHistoryArchive_1.CompressionHistoryArchive(rootPath);
289
297
  this.contextV2 = new agent_context_manager_1.AgentContextManager(rootPath, this.config);
290
298
  this.agentRunService = this.config.contextFlag('agent_runtime_v2')
291
299
  ? new agent_runtime_1.AgentRunService(path.join(rootPath, '.newmark-context-v2'))
@@ -1464,6 +1472,11 @@ class Agent {
1464
1472
  runId,
1465
1473
  target,
1466
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,
1467
1480
  status,
1468
1481
  startedAt: raw.startedAt || this.nowIso(),
1469
1482
  endedAt: raw.endedAt,
@@ -1529,27 +1542,76 @@ class Agent {
1529
1542
  const sorted = [...byRun.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
1530
1543
  return sorted.length > RUN_WINDOW ? sorted.slice(-RUN_WINDOW) : sorted;
1531
1544
  }
1532
- recoverPersistedWorkRuns(runs, persistedUpdatedAt) {
1545
+ recoverPersistedWorkRuns(runs, persistedUpdatedAt, unexpectedExit = false) {
1533
1546
  const normalized = this.normalizeWorkRuns(runs);
1534
1547
  let changed = false;
1535
- // Only convert 'running' to 'interrupted' when the persisted state is
1536
- // stale enough to indicate a cold start. If the state was persisted
1537
- // recently the conversation may still be running in a background kernel
1538
- // 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.
1539
1551
  const isRecentPersist = !!persistedUpdatedAt
1540
1552
  && (Date.now() - new Date(persistedUpdatedAt).getTime()) < 120_000;
1541
- if (isRecentPersist)
1542
- return { runs: normalized, changed: false };
1553
+ let preservedLiveRun = false;
1543
1554
  for (const run of normalized) {
1544
1555
  if (run.status !== 'running')
1545
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
+ }
1546
1566
  run.status = 'interrupted';
1547
- run.endedAt = persistedUpdatedAt || run.startedAt || this.nowIso();
1567
+ run.endedAt = unexpectedExit ? this.nowIso() : (persistedUpdatedAt || run.startedAt || this.nowIso());
1548
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
+ });
1549
1587
  changed = true;
1550
1588
  }
1589
+ if (preservedLiveRun && !changed)
1590
+ return { runs: normalized, changed: false };
1551
1591
  return { runs: normalized, changed };
1552
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
+ }
1553
1615
  beginConversationWorkRun(runId, target = this.currentConversationTarget(), startedAt = this.nowIso(), managed = false, runtimeKey) {
1554
1616
  const cleanRunId = String(runId || crypto.randomUUID()).trim().slice(0, 200);
1555
1617
  const existing = this.workRuns.find(run => run.runId === cleanRunId);
@@ -1567,6 +1629,9 @@ class Agent {
1567
1629
  runId: cleanRunId,
1568
1630
  target: normalizedTarget,
1569
1631
  runtimeKey: String(runtimeKey || (0, conversationTarget_1.conversationRuntimeKey)(normalizedTarget)),
1632
+ runtimeOwnerId: this.runtimeLifecycle.ownerId,
1633
+ runtimeOwnerPid: this.runtimeLifecycle.pid,
1634
+ runtimeLifecycleRole: this.runtimeLifecycleRole,
1570
1635
  status: 'running',
1571
1636
  startedAt,
1572
1637
  expanded: true,
@@ -1581,10 +1646,186 @@ class Agent {
1581
1646
  if (managed)
1582
1647
  this.managedWorkRunIds.add(run.runId);
1583
1648
  this.activeWorkRunId = run.runId;
1649
+ this.ensureBuildHistoryBlock(run);
1584
1650
  if (this.agentRunService)
1585
1651
  this.createAgentRunForWorkRun(run, normalizedTarget);
1586
1652
  return run;
1587
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
+ }
1588
1829
  createAgentRunForWorkRun(run, target) {
1589
1830
  if (!this.agentRunService)
1590
1831
  return;
@@ -1827,10 +2068,22 @@ class Agent {
1827
2068
  return false;
1828
2069
  this.syncAgentRunTerminal(run.runId, status, endedAt);
1829
2070
  if (run.status !== 'running') {
1830
- if (run.status !== 'interrupted' || status !== 'force_interrupted')
1831
- 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
+ }
1832
2083
  this.activeWorkRunId = run.runId;
1833
2084
  this.finalizingWorkRunId = run.runId;
2085
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, status, endedAt);
2086
+ this.enforceGoalTerminalInvariant(status, goalAudit);
1834
2087
  this.emitWorkEvent({
1835
2088
  type: 'status',
1836
2089
  content: 'Force interrupted.',
@@ -1845,6 +2098,7 @@ class Agent {
1845
2098
  this.activeWorkRunId = '';
1846
2099
  this.finalizingWorkRunId = '';
1847
2100
  this.managedWorkRunIds.delete(run.runId);
2101
+ this.persistBuildBlockWorkOverview(run, status, endedAt, goalAudit);
1848
2102
  this.saveWorkspaceConversationState();
1849
2103
  return true;
1850
2104
  }
@@ -1852,6 +2106,8 @@ class Agent {
1852
2106
  this.finalizingWorkRunId = run.runId;
1853
2107
  if (status === 'completed')
1854
2108
  this.ensureCompletedWorkRunFinalResult(run);
2109
+ const goalAudit = this.auditGoalAtWorkRunEnd(run, status, endedAt);
2110
+ this.enforceGoalTerminalInvariant(status, goalAudit);
1855
2111
  this.emitWorkEvent({
1856
2112
  type: status === 'completed' ? 'done' : status === 'error' ? 'error' : 'status',
1857
2113
  content: status === 'force_interrupted' ? 'Force interrupted.' : status === 'interrupted' ? 'Interrupted.' : 'Response complete.',
@@ -1866,6 +2122,7 @@ class Agent {
1866
2122
  this.activeWorkRunId = '';
1867
2123
  this.finalizingWorkRunId = '';
1868
2124
  this.managedWorkRunIds.delete(run.runId);
2125
+ this.persistBuildBlockWorkOverview(run, status, endedAt, goalAudit);
1869
2126
  this.saveWorkspaceConversationState();
1870
2127
  return true;
1871
2128
  }
@@ -1972,6 +2229,14 @@ class Agent {
1972
2229
  this.saveWorkspaceConversationState(false);
1973
2230
  if (event.type === 'done' || event.type === 'error')
1974
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
+ }
1975
2240
  return event;
1976
2241
  }
1977
2242
  appendWorkflowMessage(content, toolName, toolArgs, persist = true) {
@@ -3101,6 +3366,17 @@ class Agent {
3101
3366
  }
3102
3367
  return cleaned;
3103
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
+ }
3104
3380
  saveWorkspaceConversationState(flush = true) {
3105
3381
  if (this.isSubagentRuntime)
3106
3382
  return;
@@ -3108,6 +3384,7 @@ class Agent {
3108
3384
  if (!key)
3109
3385
  return;
3110
3386
  const updatedAt = new Date().toISOString();
3387
+ const runtimeOwner = this.activeConversationRuntimeOwner();
3111
3388
  this.workspaceConversations.set(key, {
3112
3389
  chatMessages: [...this.chatMessages],
3113
3390
  history: [...this.history],
@@ -3122,6 +3399,9 @@ class Agent {
3122
3399
  inputMode: this.inputMode,
3123
3400
  mode: this.mode,
3124
3401
  goal: this.serializeGoal(),
3402
+ runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3403
+ runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3404
+ runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
3125
3405
  updatedAt,
3126
3406
  });
3127
3407
  const stored = this.readStoredConversationState();
@@ -3152,6 +3432,9 @@ class Agent {
3152
3432
  inputMode: this.inputMode,
3153
3433
  mode: this.mode,
3154
3434
  goal: this.serializeGoal(),
3435
+ runtimeOwnerId: runtimeOwner.runtimeOwnerId,
3436
+ runtimeOwnerPid: runtimeOwner.runtimeOwnerPid,
3437
+ runtimeLifecycleRole: runtimeOwner.runtimeLifecycleRole,
3155
3438
  updatedAt,
3156
3439
  };
3157
3440
  if (nextEntry.tree || nextEntry.branches?.length) {
@@ -3203,6 +3486,7 @@ class Agent {
3203
3486
  this.mode = saved.mode || 'build';
3204
3487
  this.goal = this.restoreGoal(saved.goal);
3205
3488
  this.status = this.restoreStatusFromWorkRuns(saved.goal);
3489
+ this.recoverMissingTerminalBuildOverviews();
3206
3490
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
3207
3491
  return;
3208
3492
  }
@@ -3215,7 +3499,7 @@ class Agent {
3215
3499
  this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
3216
3500
  this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
3217
3501
  this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
3218
- const recoveredWorkRuns = this.recoverPersistedWorkRuns(persisted?.workRuns, persisted?.updatedAt);
3502
+ const recoveredWorkRuns = this.recoverPersistedWorkRuns(persisted?.workRuns, persisted?.updatedAt, this.runtimeLifecycle.unexpectedExit);
3219
3503
  this.workRuns = recoveredWorkRuns.runs;
3220
3504
  this.continuations = this.normalizeContinuations(persisted?.continuations);
3221
3505
  this.restoreConversationModelSelection(persisted?.modelSelection);
@@ -3223,9 +3507,29 @@ class Agent {
3223
3507
  this.inputMode = this.defaultInputMode();
3224
3508
  this.mode = persisted?.mode || 'build';
3225
3509
  this.goal = this.restoreGoal(persisted?.goal);
3226
- 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();
3227
3522
  this.activeWorkRunId = this.workRuns.find(run => run.status === 'running')?.runId || '';
3228
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;
3229
3533
  this.workspaceConversations.set(key, {
3230
3534
  chatMessages: [...this.chatMessages],
3231
3535
  history: [...this.history],
@@ -3236,21 +3540,31 @@ class Agent {
3236
3540
  workRuns: this.normalizeWorkRuns(this.workRuns),
3237
3541
  continuations: this.normalizeContinuations(this.continuations),
3238
3542
  modelSelection: persisted?.modelSelection || this.currentConversationModelSelection(),
3239
- flowSelection: persisted?.flowSelection || null,
3543
+ flowSelection: this.currentConversationFlowSelection(),
3240
3544
  inputMode: this.defaultInputMode(),
3241
- mode: persisted?.mode || 'build',
3242
- goal: persisted?.goal || null,
3243
- 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,
3244
3551
  });
3245
- if (recoveredWorkRuns.changed && stateKey && persisted) {
3246
- const recoveredAt = this.nowIso();
3247
- stored.conversations[stateKey] = {
3248
- ...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),
3249
3557
  workRuns: this.normalizeWorkRuns(this.workRuns),
3250
- 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,
3251
3565
  };
3252
- this.workspaceConversations.get(key).updatedAt = recoveredAt;
3253
- this.writeStoredConversationStateNow(stored);
3566
+ this.workspaceConversations.get(key).updatedAt = recoveryAt;
3567
+ this.writeStoredConversationStateNow(recoveredStored);
3254
3568
  }
3255
3569
  }
3256
3570
  applyWorkspaceContext(ws) {
@@ -3477,7 +3791,7 @@ class Agent {
3477
3791
  catch { }
3478
3792
  const action = String(input.action || '').trim();
3479
3793
  if (!action)
3480
- return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize|restore|search|status).', 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.' };
3481
3795
  if (action === 'list') {
3482
3796
  const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
3483
3797
  const entries = this.history.slice(0, limit).map((message, index) => ({
@@ -3569,7 +3883,9 @@ class Agent {
3569
3883
  }
3570
3884
  if (action === 'restore') {
3571
3885
  const restoreId = String(input.restore_id || '').trim();
3572
- const entry = this.compressionCache.find(item => item.id === restoreId);
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;
3573
3889
  if (!entry)
3574
3890
  return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
3575
3891
  const summaryHeader = entry.summary.startsWith('[Context Compression') ? '[Context Compression' : '[Context History Summary]';
@@ -3578,7 +3894,10 @@ class Agent {
3578
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.' };
3579
3895
  }
3580
3896
  this.history.splice(markerIndex, 1, ...entry.messages.map(message => ({ ...message })));
3581
- this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
3897
+ if (hotEntry)
3898
+ this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
3899
+ if (coldEntry)
3900
+ this.markColdCompressionRestored(entry.id);
3582
3901
  this.saveWorkspaceConversationState(true);
3583
3902
  return {
3584
3903
  ok: true,
@@ -3588,7 +3907,9 @@ class Agent {
3588
3907
  restoreId: entry.id,
3589
3908
  restoredEntries: entry.messages.length,
3590
3909
  restoredChars: entry.foldedChars,
3910
+ source: hotEntry ? 'hot-cache' : 'cold-archive',
3591
3911
  cacheRemaining: this.compressionCache.length,
3912
+ archiveRemaining: this.coldCompressionEntries().length,
3592
3913
  displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3593
3914
  }, null, 2),
3594
3915
  metadata: { kind: 'context-history-restore' },
@@ -3600,10 +3921,16 @@ class Agent {
3600
3921
  if (!query)
3601
3922
  return { ok: false, output: '[context_history_manage] search requires query.', error: 'search requires query.' };
3602
3923
  const matches = [];
3603
- for (const entry of this.compressionCache) {
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) {
3604
3930
  if (matches.length >= limit)
3605
3931
  break;
3606
- const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
3932
+ const { entry } = item;
3933
+ const hit = { cacheId: entry.id, source: item.source, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
3607
3934
  if (entry.summary.toLowerCase().includes(query)) {
3608
3935
  hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
3609
3936
  }
@@ -3625,18 +3952,78 @@ class Agent {
3625
3952
  action: 'search',
3626
3953
  query: String(input.query || ''),
3627
3954
  cacheEntries: this.compressionCache.length,
3955
+ archiveEntries: coldEntries.length,
3628
3956
  matches,
3629
3957
  displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3630
3958
  }, null, 2),
3631
3959
  metadata: { kind: 'context-history-search' },
3632
3960
  };
3633
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
+ }
3634
4020
  if (action === 'status') {
3635
4021
  const budget = this.compressionBudget(this.history);
3636
4022
  const estimatedTokens = this.estimateContextTokens(this.history);
3637
4023
  const maxTokens = this.contextMaxTokens();
3638
4024
  const protectedStartIndex = this.contextHistoryProtectedStartIndex();
3639
4025
  const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
4026
+ const coldEntries = this.coldCompressionEntries();
3640
4027
  return {
3641
4028
  ok: true,
3642
4029
  output: JSON.stringify({
@@ -3666,6 +4053,13 @@ class Agent {
3666
4053
  totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
3667
4054
  ids: this.compressionCache.map(item => item.id),
3668
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
+ },
3669
4063
  protectedZone: {
3670
4064
  preserveRecentMessages: this.config.getNum('context', 'preserve_recent_messages') || 5,
3671
4065
  protectedStartIndex,
@@ -4186,6 +4580,7 @@ class Agent {
4186
4580
  };
4187
4581
  }
4188
4582
  updateGoal(newGoal) {
4583
+ const existingGoalPaused = this.goal?.paused || false;
4189
4584
  if (this.goal) {
4190
4585
  this.goal.update(newGoal);
4191
4586
  }
@@ -4195,9 +4590,10 @@ class Agent {
4195
4590
  }
4196
4591
  if (this.goal) {
4197
4592
  this.goal.verified = false;
4198
- this.goal.paused = false;
4593
+ // Replacing the objective must not implicitly resume an explicitly paused Goal.
4594
+ this.goal.paused = existingGoalPaused;
4199
4595
  this.goal.goalRounds = 0;
4200
- this.status = 'idle';
4596
+ this.status = this.goal.paused ? 'goal_paused' : 'idle';
4201
4597
  }
4202
4598
  this.mode = 'goal';
4203
4599
  this.saveWorkspaceConversationState(true);
@@ -4210,6 +4606,17 @@ class Agent {
4210
4606
  this.saveWorkspaceConversationState(true);
4211
4607
  return this.goal.paused;
4212
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
+ }
4213
4620
  clearGoal() {
4214
4621
  this.goal = null;
4215
4622
  if (this.mode === 'goal')
@@ -4234,8 +4641,8 @@ class Agent {
4234
4641
  canAutoContinueGoal() {
4235
4642
  return this.goalContinuationGate ? this.goalContinuationGate() : true;
4236
4643
  }
4237
- claimGoalContinuationMessage() {
4238
- 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()))
4239
4646
  return null;
4240
4647
  const maxGoalContinuations = Math.max(0, Math.floor(this.config.getNum('agent', 'goal_max_continuations') || 0));
4241
4648
  if (maxGoalContinuations > 0 && this.goal.goalRounds >= maxGoalContinuations) {
@@ -6872,9 +7279,62 @@ class Agent {
6872
7279
  if (this.isSubagentRuntime)
6873
7280
  this.subagentContextPersist?.(this.history.map(message => ({ ...message })), this.lastCompression);
6874
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
+ }
6875
7326
  pushCompressionCacheEntry(summary, messages, model, fallback) {
6876
7327
  if (!messages.length)
6877
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);
6878
7338
  const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === 'string'
6879
7339
  ? message.content.length
6880
7340
  : JSON.stringify(message.content || '').length), 0);
@@ -6891,7 +7351,9 @@ class Agent {
6891
7351
  this.nextCompressionCacheId += 1;
6892
7352
  const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
6893
7353
  if (this.compressionCache.length > maxEntries) {
6894
- this.compressionCache = this.compressionCache.slice(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)];
6895
7357
  }
6896
7358
  this.saveWorkspaceConversationState(true);
6897
7359
  }