@curie-agent/daemon 0.4.2 → 0.4.4
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/src/channel-manager.d.ts.map +1 -1
- package/dist/src/channel-manager.js +1 -0
- package/dist/src/channel-manager.js.map +1 -1
- package/dist/src/daemon-app.d.ts +13 -0
- package/dist/src/daemon-app.d.ts.map +1 -1
- package/dist/src/daemon-app.js +109 -29
- package/dist/src/daemon-app.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsonrpc-handler.d.ts +18 -1
- package/dist/src/jsonrpc-handler.d.ts.map +1 -1
- package/dist/src/jsonrpc-handler.js +199 -261
- package/dist/src/jsonrpc-handler.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +2 -1
- package/dist/src/server.js.map +1 -1
- package/dist/src/version.d.ts +9 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +21 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/ws-handler.d.ts.map +1 -1
- package/dist/src/ws-handler.js +3 -1
- package/dist/src/ws-handler.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/web/dist/assets/index-BNHnn_LR.js +234 -0
- package/web/dist/assets/index-ulOOFddN.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DV8ePK4N.css +0 -1
- package/web/dist/assets/index-De5KT5pu.js +0 -165
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import os, { homedir } from 'node:os';
|
|
2
2
|
import path, { join } from 'node:path';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import { Method, renderSlashCommandHelp, findSlashCommand } from '@curie-agent/protocol';
|
|
6
5
|
import { TurnLoop, parseReminderTime, listSnapshots, revertTo, createIdentityFilesAuto, PURE_TOOLS } from '@curie-agent/core';
|
|
6
|
+
import { compactMessages, reconstructMessagesFromEvents, resolveBudget, estimateRequestTokens, breakdownChars, fillPct, formatTokens, DEFAULT_CALIBRATION, DEFAULT_SETTINGS, } from '@curie-agent/core';
|
|
7
7
|
import { listSkills, discoverAllSkills } from '@curie-agent/tools';
|
|
8
8
|
import { executeCd } from './slash-cd.js';
|
|
9
|
-
|
|
10
|
-
const VERSION = (() => {
|
|
11
|
-
try {
|
|
12
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8'));
|
|
14
|
-
return pkg.version ?? '0.0.0';
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return '0.0.0';
|
|
18
|
-
}
|
|
19
|
-
})();
|
|
9
|
+
import { VERSION } from './version.js';
|
|
20
10
|
export class JsonRpcHandler {
|
|
21
11
|
sessionStore;
|
|
22
12
|
settingsManager;
|
|
@@ -305,10 +295,11 @@ export class JsonRpcHandler {
|
|
|
305
295
|
}
|
|
306
296
|
case Method.CONFIG_GET: {
|
|
307
297
|
const key = this.getStringParam(params, 'key');
|
|
308
|
-
if (!key)
|
|
309
|
-
return this.paramError('key');
|
|
310
298
|
const settings = this.settingsManager.get();
|
|
311
|
-
|
|
299
|
+
// '*' or omitted → the whole tree, in one consistent snapshot. Reading
|
|
300
|
+
// ~23 top-level keys one at a time can tear if another client writes
|
|
301
|
+
// mid-fetch, yielding a snapshot that never existed on disk.
|
|
302
|
+
result = !key || key === '*' ? settings : this.getNestedValue(settings, key);
|
|
312
303
|
break;
|
|
313
304
|
}
|
|
314
305
|
case Method.CONFIG_SET: {
|
|
@@ -340,14 +331,7 @@ export class JsonRpcHandler {
|
|
|
340
331
|
else {
|
|
341
332
|
this.settingsManager.update({ [key]: value });
|
|
342
333
|
}
|
|
343
|
-
this.
|
|
344
|
-
this.sharedEventBus?.emit({
|
|
345
|
-
type: 'config-changed',
|
|
346
|
-
id: Math.random().toString(36).substring(7),
|
|
347
|
-
timestamp: Date.now(),
|
|
348
|
-
key,
|
|
349
|
-
value,
|
|
350
|
-
});
|
|
334
|
+
this.emitConfigChanged(key, value);
|
|
351
335
|
if (key.startsWith('heartbeat') && this.daemonApp) {
|
|
352
336
|
const updatedSettings = this.settingsManager.get();
|
|
353
337
|
if (updatedSettings.heartbeat?.schedule === 'on') {
|
|
@@ -363,6 +347,17 @@ export class JsonRpcHandler {
|
|
|
363
347
|
this.daemonApp.taskManager.cancelAllHeartbeats();
|
|
364
348
|
}
|
|
365
349
|
}
|
|
350
|
+
// Keep the derived top-level `model` in sync with the active provider.
|
|
351
|
+
// SettingsManager.load() recomputes it from providers[current_provider].model,
|
|
352
|
+
// so a stale top-level value silently reverts on the next daemon start.
|
|
353
|
+
if (key === 'current_provider' || /^providers\.[^.]+\.model$/.test(key)) {
|
|
354
|
+
const synced = this.settingsManager.get();
|
|
355
|
+
const active = synced.providers[synced.current_provider];
|
|
356
|
+
if (active && synced.model !== active.model) {
|
|
357
|
+
this.settingsManager.update({ model: active.model });
|
|
358
|
+
this.emitConfigChanged('model', active.model);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
366
361
|
result = { status: 'ok', key, value };
|
|
367
362
|
break;
|
|
368
363
|
}
|
|
@@ -824,21 +819,26 @@ export class JsonRpcHandler {
|
|
|
824
819
|
if (typeof p.status === 'string') {
|
|
825
820
|
this.daemonApp.taskManager.updateTaskStatus(taskId, p.status);
|
|
826
821
|
}
|
|
822
|
+
// Patch through updateTask() rather than mutating the object in place —
|
|
823
|
+
// an in-place edit followed by save() writes a stale array.
|
|
824
|
+
const patch = {};
|
|
827
825
|
if (typeof p.priority === 'string')
|
|
828
|
-
|
|
826
|
+
patch.priority = p.priority;
|
|
829
827
|
if (typeof p.title === 'string')
|
|
830
|
-
|
|
828
|
+
patch.title = p.title;
|
|
831
829
|
if (typeof p.description === 'string')
|
|
832
|
-
|
|
830
|
+
patch.description = p.description;
|
|
833
831
|
if (Array.isArray(p.tags))
|
|
834
|
-
|
|
832
|
+
patch.tags = p.tags;
|
|
835
833
|
if (typeof p.mode === 'string')
|
|
836
|
-
|
|
834
|
+
patch.mode = p.mode;
|
|
837
835
|
if (typeof p.scope === 'string')
|
|
838
|
-
|
|
836
|
+
patch.scope = p.scope;
|
|
839
837
|
if (typeof p.scheduled_at === 'number')
|
|
840
|
-
|
|
841
|
-
|
|
838
|
+
patch.scheduled_at = p.scheduled_at;
|
|
839
|
+
if (Object.keys(patch).length > 0) {
|
|
840
|
+
this.daemonApp.taskManager.updateTask(task.id, patch);
|
|
841
|
+
}
|
|
842
842
|
this.sharedEventBus?.emit({ type: 'todo-changed', id: crypto.randomUUID(), timestamp: Date.now(), action: 'updated', taskId });
|
|
843
843
|
result = { ok: true, task: this.daemonApp.taskManager.findTask(taskId) };
|
|
844
844
|
break;
|
|
@@ -905,6 +905,7 @@ export class JsonRpcHandler {
|
|
|
905
905
|
'tool-result', 'approval-decision', 'usage',
|
|
906
906
|
'error', 'session-start', 'session-stop', 'hook', 'status',
|
|
907
907
|
'session-resumed', 'context-warning', 'thinking-delta',
|
|
908
|
+
'compaction', 'context-report',
|
|
908
909
|
// Subagent events
|
|
909
910
|
'agent-start', 'agent-text-delta', 'agent-thinking-delta',
|
|
910
911
|
'agent-tool-call', 'agent-tool-result', 'agent-usage',
|
|
@@ -918,7 +919,9 @@ export class JsonRpcHandler {
|
|
|
918
919
|
}
|
|
919
920
|
try {
|
|
920
921
|
const result = await loop.run(text);
|
|
921
|
-
|
|
922
|
+
// No post-run threshold check: TurnLoop enforces the budget before every
|
|
923
|
+
// provider call, on every code path (channels, tasks, heartbeat,
|
|
924
|
+
// subagents) — not just between prompts on this one.
|
|
922
925
|
return { status: 'completed', sessionId: result.sessionId, events: result.events.length };
|
|
923
926
|
}
|
|
924
927
|
catch (err) {
|
|
@@ -936,6 +939,21 @@ export class JsonRpcHandler {
|
|
|
936
939
|
paramError(key) {
|
|
937
940
|
return { jsonrpc: '2.0', id: 0, error: { code: -32602, message: `Missing required parameter: ${key}` } };
|
|
938
941
|
}
|
|
942
|
+
/**
|
|
943
|
+
* Broadcast a settings change so every connected client re-reads config.
|
|
944
|
+
* 'config-changed' is already in ws-handler's newEventTypes, so this reaches
|
|
945
|
+
* the browser. Every settings write must emit — a write that doesn't leaves
|
|
946
|
+
* the web dashboard showing a stale value until the user reloads.
|
|
947
|
+
*/
|
|
948
|
+
emitConfigChanged(key, value) {
|
|
949
|
+
this.sharedEventBus?.emit({
|
|
950
|
+
type: 'config-changed',
|
|
951
|
+
id: Math.random().toString(36).substring(7),
|
|
952
|
+
timestamp: Date.now(),
|
|
953
|
+
key,
|
|
954
|
+
value,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
939
957
|
/** Get a nested value from an object using dot notation (e.g. "providers.anthropic.model"). */
|
|
940
958
|
getNestedValue(obj, path) {
|
|
941
959
|
const parts = path.split('.');
|
|
@@ -1008,6 +1026,7 @@ export class JsonRpcHandler {
|
|
|
1008
1026
|
const pricing = pConfig?.model_cost
|
|
1009
1027
|
? `\`${pConfig.model_cost}\` (per million)`
|
|
1010
1028
|
: 'Not configured';
|
|
1029
|
+
const statusBudget = resolveBudget(settings);
|
|
1011
1030
|
const statusText = `### Curie Agent Status
|
|
1012
1031
|
* **Version:** \`${VERSION}\`
|
|
1013
1032
|
* **Active Model:** \`${settings.model}\`
|
|
@@ -1015,11 +1034,11 @@ export class JsonRpcHandler {
|
|
|
1015
1034
|
* **Approval Mode:** \`${settings.mode || 'auto'}\`
|
|
1016
1035
|
* **Reasoning Effort:** \`${settings.effort || 'auto'}\`
|
|
1017
1036
|
* **Workspace CWD:** \`${this.getSessionCwd(sessionId) || process.cwd()}\`
|
|
1018
|
-
* **Tools per Turn
|
|
1019
|
-
* **Web Search per Turn
|
|
1020
|
-
* **Model Context Window:** \`${(
|
|
1037
|
+
* **Tools per Turn / Run:** \`${String(settings.tools_per_call || DEFAULT_SETTINGS.tools_per_call)}\` / \`${String(settings.tools_per_run || DEFAULT_SETTINGS.tools_per_run)}\`
|
|
1038
|
+
* **Web Search per Turn / Run:** \`${String(settings.websearch_per_call || DEFAULT_SETTINGS.websearch_per_call)}\` / \`${String(settings.websearch_per_run || DEFAULT_SETTINGS.websearch_per_run)}\`
|
|
1039
|
+
* **Model Context Window:** \`${statusBudget.windowTokens.toLocaleString()} tokens\` (\`${statusBudget.usableTokens.toLocaleString()}\` usable, \`${statusBudget.reservedOutput.toLocaleString()}\` reserved for output)
|
|
1021
1040
|
* **Model Pricing:** ${pricing}
|
|
1022
|
-
* **Auto-Compaction:** \`${settings.auto_compact?.enabled
|
|
1041
|
+
* **Auto-Compaction:** \`${settings.auto_compact?.enabled ?? DEFAULT_SETTINGS.auto_compact.enabled}\` (warn \`${String(settings.auto_compact?.warn_threshold ?? DEFAULT_SETTINGS.auto_compact.warn_threshold)}%\`, suggest \`${String(settings.auto_compact?.threshold ?? DEFAULT_SETTINGS.auto_compact.threshold)}%\`, compact \`${String(settings.auto_compact?.forced_threshold ?? DEFAULT_SETTINGS.auto_compact.forced_threshold)}%\`)`;
|
|
1023
1042
|
emitDelta(statusText);
|
|
1024
1043
|
break;
|
|
1025
1044
|
}
|
|
@@ -1126,13 +1145,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1126
1145
|
else {
|
|
1127
1146
|
settings.theme = theme;
|
|
1128
1147
|
this.settingsManager.update(settings);
|
|
1129
|
-
this.
|
|
1130
|
-
type: 'config-changed',
|
|
1131
|
-
id: Math.random().toString(36).substring(7),
|
|
1132
|
-
timestamp: Date.now(),
|
|
1133
|
-
key: 'theme',
|
|
1134
|
-
value: theme,
|
|
1135
|
-
});
|
|
1148
|
+
this.emitConfigChanged('theme', theme);
|
|
1136
1149
|
emitDelta(`Successfully switched theme to: **${theme}**`);
|
|
1137
1150
|
}
|
|
1138
1151
|
break;
|
|
@@ -1149,6 +1162,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1149
1162
|
else {
|
|
1150
1163
|
settings.mode = args.toLowerCase();
|
|
1151
1164
|
this.settingsManager.update(settings);
|
|
1165
|
+
this.emitConfigChanged('mode', settings.mode);
|
|
1152
1166
|
emitDelta(`Successfully switched approval mode to: **${args.toLowerCase()}**`);
|
|
1153
1167
|
}
|
|
1154
1168
|
break;
|
|
@@ -1165,6 +1179,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1165
1179
|
else {
|
|
1166
1180
|
settings.effort = args.toLowerCase();
|
|
1167
1181
|
this.settingsManager.update(settings);
|
|
1182
|
+
this.emitConfigChanged('effort', settings.effort);
|
|
1168
1183
|
emitDelta(`Successfully switched reasoning effort to: **${args.toLowerCase()}**`);
|
|
1169
1184
|
}
|
|
1170
1185
|
break;
|
|
@@ -1192,8 +1207,11 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1192
1207
|
settings.websearch_per_call = wsVal;
|
|
1193
1208
|
}
|
|
1194
1209
|
}
|
|
1195
|
-
this.settingsManager.
|
|
1196
|
-
|
|
1210
|
+
this.settingsManager.update({
|
|
1211
|
+
tools_per_call: settings.tools_per_call,
|
|
1212
|
+
websearch_per_call: settings.websearch_per_call,
|
|
1213
|
+
});
|
|
1214
|
+
emitDelta(`Tool limits updated! Tools per turn: **${String(settings.tools_per_call)}**, WebSearch per turn: **${String(settings.websearch_per_call ?? 5)}**`);
|
|
1197
1215
|
}
|
|
1198
1216
|
}
|
|
1199
1217
|
break;
|
|
@@ -1212,9 +1230,8 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1212
1230
|
emitDelta(`Invalid value: "${args}". Must be a positive integer.`);
|
|
1213
1231
|
}
|
|
1214
1232
|
else {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
emitDelta(`WebSearch/WebFetch limit per turn set to: **${val}**`);
|
|
1233
|
+
this.settingsManager.update({ websearch_per_call: val });
|
|
1234
|
+
emitDelta(`WebSearch/WebFetch limit per turn set to: **${String(val)}**`);
|
|
1218
1235
|
}
|
|
1219
1236
|
}
|
|
1220
1237
|
break;
|
|
@@ -1343,122 +1360,121 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1343
1360
|
const sub = parts[0]?.toLowerCase();
|
|
1344
1361
|
const arg1 = parts[1]?.toLowerCase();
|
|
1345
1362
|
const arg2 = parts[2]?.toLowerCase();
|
|
1363
|
+
const d = DEFAULT_SETTINGS.auto_compact;
|
|
1364
|
+
/** Persist through update() — get() returns a deep clone, so mutating it is a no-op. */
|
|
1365
|
+
const saveAutoCompact = (patch) => {
|
|
1366
|
+
this.settingsManager.update({ auto_compact: { ...settings.auto_compact, ...patch } });
|
|
1367
|
+
};
|
|
1346
1368
|
if (sub === 'auto') {
|
|
1347
|
-
const
|
|
1348
|
-
if (!s.auto_compact) {
|
|
1349
|
-
s.auto_compact = { enabled: 'on', threshold: 80, warn_threshold: 15, forced_threshold: 85 };
|
|
1350
|
-
}
|
|
1369
|
+
const ac = settings.auto_compact ?? d;
|
|
1351
1370
|
if (!arg1) {
|
|
1352
|
-
emitDelta(`### Auto-Compaction Settings
|
|
1353
|
-
* **Auto-compact**: \`${s.auto_compact.enabled}\`
|
|
1354
|
-
* **Threshold**: \`${s.auto_compact.threshold ?? 80}%\`
|
|
1355
|
-
* **Warning Threshold**: \`${s.auto_compact.warn_threshold ?? 15}%\`
|
|
1356
|
-
* **Pricing Warn**: \`${s.pricing_tier_warn ?? 'off'}\`
|
|
1371
|
+
emitDelta(`### Auto-Compaction Settings
|
|
1357
1372
|
|
|
1358
|
-
**
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1373
|
+
* **Auto-compact**: \`${ac.enabled}\`
|
|
1374
|
+
* **Warn at**: \`${String(ac.warn_threshold ?? d.warn_threshold)}%\`
|
|
1375
|
+
* **Suggest at**: \`${String(ac.threshold ?? d.threshold)}%\`
|
|
1376
|
+
* **Compact at**: \`${String(ac.forced_threshold ?? d.forced_threshold)}%\`
|
|
1377
|
+
* **Summarizer model**: \`${ac.model || '(active model)'}\`
|
|
1378
|
+
* **Pricing warn**: \`${settings.pricing_tier_warn ?? 'off'}\`
|
|
1379
|
+
|
|
1380
|
+
**Usage**: \`/context auto [on|off|threshold N|warn N|forced N|pricing on/off]\``);
|
|
1364
1381
|
}
|
|
1365
|
-
else if (arg1 === 'off') {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
emitDelta(`Autocompaction disabled.`);
|
|
1382
|
+
else if (arg1 === 'on' || arg1 === 'off') {
|
|
1383
|
+
saveAutoCompact({ enabled: arg1 });
|
|
1384
|
+
emitDelta(`Auto-compaction ${arg1 === 'on' ? 'enabled' : 'disabled'}.`);
|
|
1369
1385
|
}
|
|
1370
|
-
else if (arg1 === 'threshold' && arg2) {
|
|
1386
|
+
else if ((arg1 === 'threshold' || arg1 === 'warn' || arg1 === 'forced') && arg2) {
|
|
1371
1387
|
const pct = parseInt(arg2, 10);
|
|
1372
|
-
if (isNaN(pct) || pct <
|
|
1373
|
-
emitDelta(`Invalid
|
|
1388
|
+
if (isNaN(pct) || pct < 5 || pct > 99) {
|
|
1389
|
+
emitDelta(`Invalid value. Use a percentage between 5 and 99.`);
|
|
1374
1390
|
}
|
|
1375
1391
|
else {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
emitDelta(
|
|
1392
|
+
const key = arg1 === 'threshold' ? 'threshold' : arg1 === 'warn' ? 'warn_threshold' : 'forced_threshold';
|
|
1393
|
+
saveAutoCompact({ [key]: pct });
|
|
1394
|
+
emitDelta(`${arg1 === 'threshold' ? 'Suggest' : arg1 === 'warn' ? 'Warn' : 'Forced compaction'} threshold set to ${String(pct)}%.`);
|
|
1379
1395
|
}
|
|
1380
1396
|
}
|
|
1381
|
-
else if (arg1 === '
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
emitDelta(`Invalid warning threshold. Use a value between 5 and 95.`);
|
|
1385
|
-
}
|
|
1386
|
-
else {
|
|
1387
|
-
s.auto_compact.warn_threshold = pct;
|
|
1388
|
-
this.settingsManager.save();
|
|
1389
|
-
emitDelta(`Warning threshold set to ${pct}%.`);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
else if (arg1 === 'pricing') {
|
|
1393
|
-
if (arg2 === 'on') {
|
|
1394
|
-
s.pricing_tier_warn = 'on';
|
|
1395
|
-
this.settingsManager.save();
|
|
1396
|
-
emitDelta(`Pricing tier warnings enabled.`);
|
|
1397
|
-
}
|
|
1398
|
-
else if (arg2 === 'off') {
|
|
1399
|
-
s.pricing_tier_warn = 'off';
|
|
1400
|
-
this.settingsManager.save();
|
|
1401
|
-
emitDelta(`Pricing tier warnings disabled.`);
|
|
1402
|
-
}
|
|
1403
|
-
else {
|
|
1404
|
-
emitDelta(`Usage: \`/context auto pricing on/off\``);
|
|
1405
|
-
}
|
|
1397
|
+
else if (arg1 === 'pricing' && (arg2 === 'on' || arg2 === 'off')) {
|
|
1398
|
+
this.settingsManager.update({ pricing_tier_warn: arg2 });
|
|
1399
|
+
emitDelta(`Pricing tier warnings ${arg2 === 'on' ? 'enabled' : 'disabled'}.`);
|
|
1406
1400
|
}
|
|
1407
1401
|
else {
|
|
1408
|
-
emitDelta(`Usage: \`/context auto [on|off|threshold N|warn N|pricing on/off]\``);
|
|
1402
|
+
emitDelta(`Usage: \`/context auto [on|off|threshold N|warn N|forced N|pricing on/off]\``);
|
|
1409
1403
|
}
|
|
1410
1404
|
}
|
|
1411
1405
|
else if (sub === 'compact') {
|
|
1412
|
-
emitDelta(
|
|
1406
|
+
emitDelta(`Compacting conversation history…`);
|
|
1413
1407
|
try {
|
|
1414
|
-
const
|
|
1415
|
-
emitDelta(`\n\n
|
|
1408
|
+
const r = await this.runAutomaticCompaction(sessionId);
|
|
1409
|
+
emitDelta(`\n\n**Compacted.** ${String(r.summarizedMessageCount)} message(s) summarized, ` +
|
|
1410
|
+
`~${formatTokens(r.estimatedTokensBefore)} → ~${formatTokens(r.estimatedTokensAfter)} tokens. ` +
|
|
1411
|
+
`The full transcript is preserved on disk.\n\n**Summary:**\n\n${r.summary}`);
|
|
1416
1412
|
}
|
|
1417
1413
|
catch (err) {
|
|
1418
|
-
emitDelta(`\n\n
|
|
1414
|
+
emitDelta(`\n\n**Compaction failed**: ${err instanceof Error ? err.message : String(err)}`);
|
|
1419
1415
|
}
|
|
1420
1416
|
}
|
|
1417
|
+
else if (sub === 'messages') {
|
|
1418
|
+
const messages = reconstructMessagesFromEvents(this.sessionStore.loadEvents(sessionId) || []);
|
|
1419
|
+
if (messages.length === 0) {
|
|
1420
|
+
emitDelta(`No messages in this session yet.`);
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
const lines = [`### Messages (${String(messages.length)})`, '', '| # | Role | Tokens | Detail |', '|---|---|---|---|'];
|
|
1424
|
+
messages.forEach((m, i) => {
|
|
1425
|
+
const tokens = estimateRequestTokens({ messages: [m] });
|
|
1426
|
+
const detail = m.role === 'tool'
|
|
1427
|
+
? (m.toolName ?? 'tool')
|
|
1428
|
+
: m.role === 'user'
|
|
1429
|
+
? `${String(m.content).slice(0, 60).replace(/[\n|]/g, ' ')}…`
|
|
1430
|
+
: m.content.map((b) => b.type === 'tool-use' ? `→ ${b.name}` : b.type).join(', ');
|
|
1431
|
+
lines.push(`| ${String(i + 1)} | ${m.role} | ${formatTokens(tokens)} | ${detail} |`);
|
|
1432
|
+
});
|
|
1433
|
+
emitDelta(lines.join('\n'));
|
|
1434
|
+
}
|
|
1421
1435
|
else {
|
|
1436
|
+
// Emit measurements, not markup. The TUI and the web dashboard each
|
|
1437
|
+
// render this natively — the hand-written HTML this replaces showed
|
|
1438
|
+
// a themed gauge in the browser and nothing at all in the terminal.
|
|
1439
|
+
const budget = resolveBudget(settings);
|
|
1422
1440
|
const activeLoop = this.turnLoops.get(sessionId);
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1441
|
+
const model = settings.providers[settings.current_provider]?.model || settings.model;
|
|
1442
|
+
const messages = activeLoop
|
|
1443
|
+
? activeLoop.getMessages()
|
|
1444
|
+
: reconstructMessagesFromEvents(this.sessionStore.loadEvents(sessionId) || []);
|
|
1445
|
+
const chars = breakdownChars({
|
|
1446
|
+
system: this.systemPrompt,
|
|
1447
|
+
messages,
|
|
1448
|
+
toolDefinitions: this.tools.map((t) => t.definition),
|
|
1449
|
+
});
|
|
1450
|
+
const toTokens = (c) => Math.ceil(c / DEFAULT_CALIBRATION);
|
|
1451
|
+
const breakdown = [
|
|
1452
|
+
{ label: 'System prompt', tokens: toTokens(chars.system) },
|
|
1453
|
+
{ label: 'Tool definitions', tokens: toTokens(chars.toolDefinitions) },
|
|
1454
|
+
{ label: 'Conversation', tokens: toTokens(chars.conversation) },
|
|
1455
|
+
{ label: 'Tool results', tokens: toTokens(chars.toolResults) },
|
|
1456
|
+
];
|
|
1457
|
+
const usedTokens = breakdown.reduce((sum, b) => sum + b.tokens, 0);
|
|
1458
|
+
const report = {
|
|
1459
|
+
type: 'context-report',
|
|
1460
|
+
id: crypto.randomUUID(),
|
|
1461
|
+
model,
|
|
1462
|
+
windowTokens: budget.windowTokens,
|
|
1463
|
+
usedTokens,
|
|
1464
|
+
reservedOutput: budget.reservedOutput,
|
|
1465
|
+
breakdown,
|
|
1466
|
+
timestamp: Date.now(),
|
|
1439
1467
|
};
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
emitDelta(`<div style="background:var(--s3);border-radius:8px;padding:12px;margin:8px 0">` +
|
|
1451
|
-
`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">` +
|
|
1452
|
-
`<span style="font-family:monospace;font-size:12px;color:var(--text)">Context Window (${model})</span>` +
|
|
1453
|
-
`<span style="font-family:monospace;font-size:13px;font-weight:bold;color:${barColor}">${pct}%</span>` +
|
|
1454
|
-
`</div>` +
|
|
1455
|
-
`<div style="background:var(--s2);border-radius:4px;height:8px;overflow:hidden">` +
|
|
1456
|
-
`<div style="width:${pct}%;height:100%;background:${barGradient};border-radius:4px"></div>` +
|
|
1457
|
-
`</div>` +
|
|
1458
|
-
`<div style="display:flex;justify-content:space-between;margin-top:8px;font-family:monospace;font-size:11px;color:var(--muted)">` +
|
|
1459
|
-
`<span>In: ${fmt(input)}</span><span>Out: ${fmt(output)}</span><span>Max: ${fmt(windowSize)}</span>` +
|
|
1460
|
-
`</div></div>`);
|
|
1461
|
-
}
|
|
1468
|
+
this.sharedEventBus?.emit({ ...report, sessionId });
|
|
1469
|
+
// Plain-text fallback so the command still says something useful on a
|
|
1470
|
+
// surface that has not yet learned the event.
|
|
1471
|
+
const pct = fillPct(usedTokens, budget);
|
|
1472
|
+
const rows = breakdown
|
|
1473
|
+
.map((b) => `* ${b.label}: ${formatTokens(b.tokens)} (${String(usedTokens > 0 ? Math.round((b.tokens / usedTokens) * 100) : 0)}%)`)
|
|
1474
|
+
.join('\n');
|
|
1475
|
+
emitDelta(`### Context Window — \`${model}\`\n\n` +
|
|
1476
|
+
`**${String(pct)}%** — ${formatTokens(usedTokens)} of ${formatTokens(budget.usableTokens)} usable ` +
|
|
1477
|
+
`(${formatTokens(budget.windowTokens)} window, ${formatTokens(budget.reservedOutput)} reserved for output)\n\n${rows}`);
|
|
1462
1478
|
}
|
|
1463
1479
|
break;
|
|
1464
1480
|
}
|
|
@@ -1476,7 +1492,8 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1476
1492
|
emitDelta(`Failed to parse reminder time. Please format like: \`in 2 hours call developer\` or \`tomorrow at 9:00 am meeting\`.`);
|
|
1477
1493
|
break;
|
|
1478
1494
|
}
|
|
1479
|
-
|
|
1495
|
+
this.daemonApp.taskManager.load();
|
|
1496
|
+
this.daemonApp.taskManager.create({ title: parsed.message, mode: 'notify', scope: 'personal', scheduled_at: parsed.scheduledAt });
|
|
1480
1497
|
emitDelta(`Reminder scheduled! **"${parsed.message}"** at ${new Date(parsed.scheduledAt).toLocaleString()}`);
|
|
1481
1498
|
break;
|
|
1482
1499
|
}
|
|
@@ -1562,7 +1579,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1562
1579
|
}
|
|
1563
1580
|
case 'enable': {
|
|
1564
1581
|
settings.heartbeat.schedule = 'on';
|
|
1565
|
-
this.settingsManager.
|
|
1582
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1566
1583
|
if (this.daemonApp) {
|
|
1567
1584
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1568
1585
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1577,7 +1594,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1577
1594
|
}
|
|
1578
1595
|
case 'disable': {
|
|
1579
1596
|
settings.heartbeat.schedule = 'off';
|
|
1580
|
-
this.settingsManager.
|
|
1597
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1581
1598
|
if (this.daemonApp) {
|
|
1582
1599
|
this.daemonApp.taskManager.cancelAllHeartbeats();
|
|
1583
1600
|
}
|
|
@@ -1605,7 +1622,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1605
1622
|
}
|
|
1606
1623
|
else {
|
|
1607
1624
|
settings.heartbeat.intraday = tokens.join(',');
|
|
1608
|
-
this.settingsManager.
|
|
1625
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1609
1626
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1610
1627
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1611
1628
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1633,7 +1650,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1633
1650
|
}
|
|
1634
1651
|
else {
|
|
1635
1652
|
settings.heartbeat.daily = rest;
|
|
1636
|
-
this.settingsManager.
|
|
1653
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1637
1654
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1638
1655
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1639
1656
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1662,7 +1679,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1662
1679
|
}
|
|
1663
1680
|
else {
|
|
1664
1681
|
settings.heartbeat.weekly = rest;
|
|
1665
|
-
this.settingsManager.
|
|
1682
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1666
1683
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1667
1684
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1668
1685
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1690,7 +1707,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1690
1707
|
}
|
|
1691
1708
|
else {
|
|
1692
1709
|
settings.heartbeat.monthly = rest;
|
|
1693
|
-
this.settingsManager.
|
|
1710
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1694
1711
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1695
1712
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1696
1713
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1718,7 +1735,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1718
1735
|
}
|
|
1719
1736
|
else {
|
|
1720
1737
|
settings.heartbeat.dreaming = rest;
|
|
1721
|
-
this.settingsManager.
|
|
1738
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1722
1739
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1723
1740
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1724
1741
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -2214,129 +2231,50 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
2214
2231
|
}
|
|
2215
2232
|
return true;
|
|
2216
2233
|
}
|
|
2217
|
-
|
|
2234
|
+
/**
|
|
2235
|
+
* Compact a session's history.
|
|
2236
|
+
*
|
|
2237
|
+
* The summarization itself lives in `@curie-agent/core` so the TurnLoop can
|
|
2238
|
+
* run it mid-run — this is the out-of-band entry point for `/context compact`
|
|
2239
|
+
* on an idle session.
|
|
2240
|
+
*
|
|
2241
|
+
* Nothing is deleted. A `compaction` marker is appended and message
|
|
2242
|
+
* reconstruction replays forward from it, so the full transcript survives for
|
|
2243
|
+
* the UI, resume and audit while the model carries only the summary.
|
|
2244
|
+
*/
|
|
2245
|
+
async runAutomaticCompaction(sessionId) {
|
|
2218
2246
|
if (!this.createProvider) {
|
|
2219
2247
|
throw new Error('No provider configured');
|
|
2220
2248
|
}
|
|
2221
2249
|
const settings = this.settingsManager.get();
|
|
2222
2250
|
const provider = this.createProvider(settings);
|
|
2223
|
-
const
|
|
2251
|
+
const activeModel = settings.providers[settings.current_provider]?.model || settings.model;
|
|
2224
2252
|
const events = this.sessionStore.loadEvents(sessionId) || [];
|
|
2225
2253
|
if (events.length === 0) {
|
|
2226
2254
|
throw new Error('No events to compact');
|
|
2227
2255
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
for (const e of events) {
|
|
2231
|
-
if (e.type === 'user-prompt' && e.text) {
|
|
2232
|
-
transcriptParts.push(`User: ${e.text}`);
|
|
2233
|
-
}
|
|
2234
|
-
else if (e.type === 'assistant-delta' && e.text) {
|
|
2235
|
-
const lastIdx = transcriptParts.length - 1;
|
|
2236
|
-
if (lastIdx >= 0 && transcriptParts[lastIdx]?.startsWith('Assistant:')) {
|
|
2237
|
-
transcriptParts[lastIdx] += e.text;
|
|
2238
|
-
}
|
|
2239
|
-
else {
|
|
2240
|
-
transcriptParts.push(`Assistant: ${e.text}`);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
const transcript = transcriptParts.join('\n\n');
|
|
2245
|
-
if (!transcript.trim()) {
|
|
2256
|
+
const messages = reconstructMessagesFromEvents(events);
|
|
2257
|
+
if (messages.length === 0) {
|
|
2246
2258
|
throw new Error('No conversational history found to compact');
|
|
2247
2259
|
}
|
|
2248
|
-
const
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
text: `This is a continuation of a compacted conversation. Here is the high-fidelity summary of our session so far:\n\n${cleanSummary}\n\nLet's continue!`,
|
|
2267
|
-
cwd: process.cwd(),
|
|
2268
|
-
timestamp: Date.now() + 1,
|
|
2269
|
-
},
|
|
2270
|
-
{
|
|
2271
|
-
type: 'assistant-delta',
|
|
2272
|
-
id: crypto.randomUUID(),
|
|
2273
|
-
text: `Got it! I have fully restored our conversation summary and details. Let let me know what you would like to do next!`,
|
|
2274
|
-
timestamp: Date.now() + 2,
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
type: 'assistant-stop',
|
|
2278
|
-
id: crypto.randomUUID(),
|
|
2279
|
-
timestamp: Date.now() + 3,
|
|
2280
|
-
}
|
|
2281
|
-
];
|
|
2282
|
-
const data = newEvents.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
2283
|
-
writeFileSync(eventsPath, data, 'utf-8');
|
|
2284
|
-
return cleanSummary;
|
|
2285
|
-
}
|
|
2286
|
-
async checkContextThresholds(sessionId) {
|
|
2287
|
-
const settings = this.settingsManager.get();
|
|
2288
|
-
const history = this.sessionStore.loadEvents(sessionId) || [];
|
|
2289
|
-
const usageEvents = history.filter((e) => e.type === 'usage');
|
|
2290
|
-
const latestUsage = usageEvents[usageEvents.length - 1];
|
|
2291
|
-
const input = latestUsage?.inputTokens ?? 0;
|
|
2292
|
-
if (input === 0)
|
|
2293
|
-
return; // No token data yet
|
|
2294
|
-
const windowSize = settings.providers?.[settings.current_provider]?.model_context_window ?? 200000;
|
|
2295
|
-
const pct = Math.min(100, Math.round((input / windowSize) * 100));
|
|
2296
|
-
const autoCompact = settings.auto_compact || { enabled: 'on', threshold: 80, warn_threshold: 60, forced_threshold: 85 };
|
|
2297
|
-
const warnThresh = autoCompact.warn_threshold ?? 60;
|
|
2298
|
-
const compactThresh = autoCompact.threshold ?? 80;
|
|
2299
|
-
const forcedThresh = autoCompact.forced_threshold ?? 85;
|
|
2300
|
-
const enabled = autoCompact.enabled ?? 'on';
|
|
2301
|
-
if (pct >= forcedThresh && enabled === 'on') {
|
|
2302
|
-
try {
|
|
2303
|
-
const summary = await this.runAutomaticCompaction(sessionId, 'detailed');
|
|
2304
|
-
const successMessage = `⚡ **Auto-Compaction Executed Successfully!**\n\nContext usage was at **${pct}%** (forced threshold: **${forcedThresh}%**).\nWe have summarized the conversation, reducing the history size down to just ~500 tokens. The agent will continue seamlessly!\n\n**Restored Context Summary:**\n\n${summary}`;
|
|
2305
|
-
const warningEvent = {
|
|
2306
|
-
type: 'context-warning',
|
|
2307
|
-
id: crypto.randomUUID(),
|
|
2308
|
-
message: successMessage,
|
|
2309
|
-
timestamp: Date.now(),
|
|
2310
|
-
};
|
|
2311
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2312
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2313
|
-
}
|
|
2314
|
-
catch (err) {
|
|
2315
|
-
console.error('[compaction] Auto-compaction failed:', err);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
else if (pct >= compactThresh) {
|
|
2319
|
-
const suggestMessage = `⚠️ **Context Fill High (${pct}%)**\n\nYour context fill is at **${pct}%** (Threshold: **${compactThresh}%**). Suggesting conversation compaction.\n\nType \`/context compact\` to run compaction, summarize history, and free memory immediately!`;
|
|
2320
|
-
const warningEvent = {
|
|
2321
|
-
type: 'context-warning',
|
|
2322
|
-
id: crypto.randomUUID(),
|
|
2323
|
-
message: suggestMessage,
|
|
2324
|
-
timestamp: Date.now(),
|
|
2325
|
-
};
|
|
2326
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2327
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2328
|
-
}
|
|
2329
|
-
else if (pct >= warnThresh) {
|
|
2330
|
-
const warnMessage = `⚠️ **Context Warning (${pct}%)**\n\nContext window is **${pct}%** full (Warning threshold: **${warnThresh}%**).`;
|
|
2331
|
-
const warningEvent = {
|
|
2332
|
-
type: 'context-warning',
|
|
2333
|
-
id: crypto.randomUUID(),
|
|
2334
|
-
message: warnMessage,
|
|
2335
|
-
timestamp: Date.now(),
|
|
2336
|
-
};
|
|
2337
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2338
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2339
|
-
}
|
|
2260
|
+
const result = await compactMessages({
|
|
2261
|
+
messages,
|
|
2262
|
+
provider: provider,
|
|
2263
|
+
model: settings.auto_compact?.model || activeModel,
|
|
2264
|
+
budget: resolveBudget(settings),
|
|
2265
|
+
});
|
|
2266
|
+
const marker = {
|
|
2267
|
+
type: 'compaction',
|
|
2268
|
+
id: crypto.randomUUID(),
|
|
2269
|
+
summary: result.summary,
|
|
2270
|
+
summarizedMessageCount: result.summarizedMessageCount,
|
|
2271
|
+
tokensBefore: result.estimatedTokensBefore,
|
|
2272
|
+
tokensAfter: result.estimatedTokensAfter,
|
|
2273
|
+
timestamp: Date.now(),
|
|
2274
|
+
};
|
|
2275
|
+
this.sessionStore.appendEvent(sessionId, { ...marker, sessionId });
|
|
2276
|
+
this.sharedEventBus?.emit({ ...marker, sessionId });
|
|
2277
|
+
return result;
|
|
2340
2278
|
}
|
|
2341
2279
|
}
|
|
2342
2280
|
//# sourceMappingURL=jsonrpc-handler.js.map
|