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.
- package/dist/context/domain/types.d.ts +1 -1
- package/dist/conversation-utility-host.bundle.cjs +2048 -716
- package/dist/conversation-utility-host.js +4 -1
- package/dist/core/agent.d.ts +39 -1
- package/dist/core/agent.js +681 -28
- package/dist/core/agentKernelRunner.js +1 -1
- package/dist/core/compressionHistoryArchive.d.ts +28 -0
- package/dist/core/compressionHistoryArchive.js +131 -0
- package/dist/core/config.js +3 -0
- package/dist/core/conversationKernel.d.ts +2 -1
- package/dist/core/conversationKernel.js +38 -7
- package/dist/core/electronBrowserUseHost.js +9 -0
- package/dist/core/electronUtilityRuntimePool.js +6 -7
- package/dist/core/runtimeLifecycle.d.ts +23 -0
- package/dist/core/runtimeLifecycle.js +146 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/wslAgentRuntimePool.js +9 -10
- package/dist/main.js +15 -0
- package/dist/tools/index.js +9 -3
- package/dist/tools/nativeTools.js +1 -1
- package/dist/ui/index.html +14 -4
- package/dist/wsl-agent-host.bundle.cjs +2051 -718
- package/dist/wsl-agent-host.js +4 -0
- package/package.json +2 -2
package/dist/core/agent.js
CHANGED
|
@@ -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
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3543
|
+
flowSelection: this.currentConversationFlowSelection(),
|
|
3229
3544
|
inputMode: this.defaultInputMode(),
|
|
3230
|
-
mode:
|
|
3231
|
-
goal:
|
|
3232
|
-
|
|
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 (
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
|
|
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
|
-
|
|
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 =
|
|
3242
|
-
this.writeStoredConversationStateNow(
|
|
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
|
-
|
|
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();
|