@curie-agent/daemon 0.2.5 → 0.3.1

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.
@@ -2,7 +2,7 @@ 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
4
  import { Method } from '@curie-agent/protocol';
5
- import { TurnLoop, parseReminderTime, listSnapshots, revertTo, createIdentityFiles } from '@curie-agent/core';
5
+ import { TurnLoop, parseReminderTime, listSnapshots, revertTo, createIdentityFilesAuto } from '@curie-agent/core';
6
6
  import { listSkills, discoverAllSkills } from '@curie-agent/tools';
7
7
  import { executeCd } from './slash-cd.js';
8
8
  export class JsonRpcHandler {
@@ -199,7 +199,7 @@ export class JsonRpcHandler {
199
199
  break;
200
200
  }
201
201
  if (text.startsWith('/')) {
202
- const targetSessionId = sessionId || this.sessionStore.create(process.cwd(), this.settingsManager.get().model_override || this.settingsManager.get().model, this.settingsManager.get().current_provider || 'unknown', type || 'webui').id;
202
+ const targetSessionId = sessionId || this.sessionStore.create(process.cwd(), this.settingsManager.getActiveModel(), this.settingsManager.get().current_provider || 'unknown', type || 'webui').id;
203
203
  // Wait 150ms to guarantee that the client has completed the HTTP roundtrip,
204
204
  // received the sessionId, and successfully subscribed to WebSocket events
205
205
  // before any synchronous slash command output is emitted.
@@ -216,7 +216,7 @@ export class JsonRpcHandler {
216
216
  // The turn loop runs asynchronously in the background.
217
217
  if (!sessionId) {
218
218
  const settings = this.settingsManager.get();
219
- const session = this.sessionStore.create(process.cwd(), settings.model_override || settings.model, settings.current_provider || 'unknown', type || 'webui');
219
+ const session = this.sessionStore.create(process.cwd(), settings.providers[settings.current_provider]?.model || settings.model, settings.current_provider || 'unknown', type || 'webui');
220
220
  // Start turn loop in background — events stream via WS
221
221
  if (this.daemonApp) {
222
222
  this.daemonApp.channelManager.send(session.id, text, undefined, type || 'webui').then(res => {
@@ -412,7 +412,7 @@ export class JsonRpcHandler {
412
412
  if (!message || !scheduledAt)
413
413
  return this.paramError('message, scheduledAt');
414
414
  if (this.daemonApp) {
415
- const mode = type === 'task' ? 'auto' : 'notify';
415
+ const mode = type === 'task' ? 'agent' : 'notify';
416
416
  const task = this.daemonApp.taskManager.create({ title: message, mode, scope: 'personal', scheduled_at: scheduledAt });
417
417
  result = task;
418
418
  }
@@ -488,7 +488,7 @@ export class JsonRpcHandler {
488
488
  const userName = this.getStringParam(p, 'userName') || 'User';
489
489
  const userTimezone = this.getStringParam(p, 'userTimezone') || 'UTC';
490
490
  const userLanguages = this.getStringParam(p, 'userLanguages') || 'TypeScript, Python';
491
- createIdentityFiles({
491
+ createIdentityFilesAuto({
492
492
  provider: provider,
493
493
  apiKey,
494
494
  model,
@@ -509,7 +509,6 @@ export class JsonRpcHandler {
509
509
  settings.current_provider = provider;
510
510
  settings.model = model;
511
511
  this.settingsManager.update(settings);
512
- this.settingsManager.save();
513
512
  this.sharedEventBus?.emit({
514
513
  type: 'config-changed',
515
514
  id: Math.random().toString(36).substring(7),
@@ -517,16 +516,58 @@ export class JsonRpcHandler {
517
516
  key: 'init',
518
517
  value: true,
519
518
  });
520
- result = { status: 'complete', files: ['SOUL.md', 'USER.md', 'AGENTS.md', 'MEMORY.md', 'TOOLS.md', 'HEARTBEAT.md'] };
519
+ result = { status: 'complete', files: ['SOUL.md', 'USER.md', 'AGENTS.md', 'MEMORY.md', 'TOOLS.md', 'HEARTBEAT.md'], skills: ['deep-research', 'planning'] };
521
520
  break;
522
521
  }
523
522
  // Not yet implemented
524
523
  case Method.ORCHESTRA_PANES:
525
524
  case Method.ORCHESTRA_BROADCAST:
526
- case Method.WIKI_QUERY:
527
- case Method.WIKI_PAGE_GET:
528
525
  result = { status: 'not-implemented', method };
529
526
  break;
527
+ // Wiki operations
528
+ case Method.WIKI_QUERY: {
529
+ const { WikiManager } = await import('@curie-agent/wiki');
530
+ const query = this.getStringParam(params, 'query');
531
+ if (!query)
532
+ return this.paramError('query');
533
+ const wm = new WikiManager(this.settingsManager.get());
534
+ wm.ensureStructure();
535
+ result = wm.search(query);
536
+ break;
537
+ }
538
+ case Method.WIKI_PAGE_GET: {
539
+ const { WikiManager } = await import('@curie-agent/wiki');
540
+ const slug = this.getStringParam(params, 'slug');
541
+ if (!slug)
542
+ return this.paramError('slug');
543
+ const wm = new WikiManager(this.settingsManager.get());
544
+ wm.ensureStructure();
545
+ const content = wm.readPage(slug);
546
+ result = content !== null ? { slug, content } : { error: `Page not found: ${slug}` };
547
+ break;
548
+ }
549
+ case Method.WIKI_INGEST: {
550
+ const { WikiManager } = await import('@curie-agent/wiki');
551
+ const wm = new WikiManager(this.settingsManager.get());
552
+ wm.ensureStructure();
553
+ const pages = wm.listPages();
554
+ result = { pages, index: wm.readIndex() };
555
+ break;
556
+ }
557
+ case Method.WIKI_LINT: {
558
+ const { WikiManager } = await import('@curie-agent/wiki');
559
+ const wm = new WikiManager(this.settingsManager.get());
560
+ wm.ensureStructure();
561
+ result = wm.lintReport();
562
+ break;
563
+ }
564
+ case Method.WIKI_GRAPH: {
565
+ const { WikiManager } = await import('@curie-agent/wiki');
566
+ const wm = new WikiManager(this.settingsManager.get());
567
+ wm.ensureStructure();
568
+ result = wm.graph();
569
+ break;
570
+ }
530
571
  // Subagent management
531
572
  case Method.SUBAGENT_SPAWN: {
532
573
  const p = params;
@@ -550,17 +591,12 @@ export class JsonRpcHandler {
550
591
  const providerInstance = this.createProvider(spawnSettings);
551
592
  // Resolve model for the subagent.
552
593
  // - Explicit 'model' param from UI always wins.
553
- // - If spawning on a different provider, use that provider's default config (ignore model_override).
554
- // - If same provider as parent, inherit model_override if set.
594
+ // - Otherwise use the target provider's configured model.
555
595
  const effectiveModel = (() => {
556
596
  if (model)
557
597
  return model;
558
- if (providerName && providerName !== settings.current_provider) {
559
- // Different provider — use target's default, not parent's model_override
560
- return spawnSettings.providers?.[providerName]?.model || settings.model;
561
- }
562
- // Same provider — allow model_override to apply
563
- return spawnSettings.model_override || spawnSettings.model;
598
+ const targetProvider = spawnSettings.providers?.[providerName || spawnSettings.current_provider];
599
+ return targetProvider?.model || spawnSettings.model;
564
600
  })();
565
601
  const handle = await this.daemonApp.subagentExecutor.spawn({
566
602
  provider: providerInstance,
@@ -691,7 +727,7 @@ export class JsonRpcHandler {
691
727
  this.daemonApp.taskManager.load();
692
728
  const task = this.daemonApp.taskManager.create({
693
729
  title: instruction,
694
- mode: 'auto',
730
+ mode: 'agent',
695
731
  scope: 'personal',
696
732
  scheduled_at: scheduledAtMs,
697
733
  description: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : '',
@@ -705,6 +741,99 @@ export class JsonRpcHandler {
705
741
  };
706
742
  break;
707
743
  }
744
+ // ---- Unified task (todo) management for Kanban board ----
745
+ case Method.TODO_LIST: {
746
+ if (!this.daemonApp) {
747
+ return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Daemon not initialized' } };
748
+ }
749
+ this.daemonApp.taskManager.load();
750
+ const p = (params || {});
751
+ const filters = {};
752
+ if (typeof p.status === 'string')
753
+ filters.status = p.status;
754
+ if (typeof p.mode === 'string')
755
+ filters.mode = p.mode;
756
+ if (typeof p.scope === 'string')
757
+ filters.scope = p.scope;
758
+ if (typeof p.priority === 'string')
759
+ filters.priority = p.priority;
760
+ result = this.daemonApp.taskManager.list(Object.keys(filters).length ? filters : undefined);
761
+ break;
762
+ }
763
+ case Method.TODO_CREATE: {
764
+ if (!this.daemonApp) {
765
+ return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Daemon not initialized' } };
766
+ }
767
+ this.daemonApp.taskManager.load();
768
+ const p = params;
769
+ const title = this.getStringParam(p, 'title') || '';
770
+ if (!title)
771
+ return this.paramError('title');
772
+ const task = this.daemonApp.taskManager.create({
773
+ title,
774
+ description: this.getStringParam(p, 'description'),
775
+ mode: typeof p.mode === 'string' ? p.mode : 'human',
776
+ scope: typeof p.scope === 'string' ? p.scope : 'personal',
777
+ priority: typeof p.priority === 'string' ? p.priority : 'medium',
778
+ tags: Array.isArray(p.tags) ? p.tags : [],
779
+ scheduled_at: typeof p.scheduled_at === 'number' ? p.scheduled_at : undefined,
780
+ });
781
+ // Override status if specified (create() sets default based on mode)
782
+ if (typeof p.status === 'string') {
783
+ this.daemonApp.taskManager.updateTaskStatus(task.id, p.status);
784
+ }
785
+ this.sharedEventBus?.emit({ type: 'todo-changed', id: crypto.randomUUID(), timestamp: Date.now(), action: 'created', taskId: task.id });
786
+ result = this.daemonApp.taskManager.findTask(task.id);
787
+ break;
788
+ }
789
+ case Method.TODO_UPDATE: {
790
+ if (!this.daemonApp) {
791
+ return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Daemon not initialized' } };
792
+ }
793
+ this.daemonApp.taskManager.load();
794
+ const p = params;
795
+ const taskId = this.getStringParam(p, 'id');
796
+ if (!taskId)
797
+ return this.paramError('id');
798
+ const task = this.daemonApp.taskManager.findTask(taskId);
799
+ if (!task)
800
+ return { jsonrpc: '2.0', id, error: { code: -32602, message: `Task ${taskId} not found` } };
801
+ if (typeof p.status === 'string') {
802
+ this.daemonApp.taskManager.updateTaskStatus(taskId, p.status);
803
+ }
804
+ if (typeof p.priority === 'string')
805
+ task.priority = p.priority;
806
+ if (typeof p.title === 'string')
807
+ task.title = p.title;
808
+ if (typeof p.description === 'string')
809
+ task.description = p.description;
810
+ if (Array.isArray(p.tags))
811
+ task.tags = p.tags;
812
+ if (typeof p.mode === 'string')
813
+ task.mode = p.mode;
814
+ if (typeof p.scope === 'string')
815
+ task.scope = p.scope;
816
+ if (typeof p.scheduled_at === 'number')
817
+ task.scheduled_at = p.scheduled_at;
818
+ this.daemonApp.taskManager.save();
819
+ this.sharedEventBus?.emit({ type: 'todo-changed', id: crypto.randomUUID(), timestamp: Date.now(), action: 'updated', taskId });
820
+ result = { ok: true, task: this.daemonApp.taskManager.findTask(taskId) };
821
+ break;
822
+ }
823
+ case Method.TODO_REMOVE: {
824
+ if (!this.daemonApp) {
825
+ return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Daemon not initialized' } };
826
+ }
827
+ this.daemonApp.taskManager.load();
828
+ const p = params;
829
+ const taskId = this.getStringParam(p, 'id');
830
+ if (!taskId)
831
+ return this.paramError('id');
832
+ const removed = this.daemonApp.taskManager.removeTask(taskId);
833
+ this.sharedEventBus?.emit({ type: 'todo-changed', id: crypto.randomUUID(), timestamp: Date.now(), action: 'removed', taskId });
834
+ result = { removed };
835
+ break;
836
+ }
708
837
  default:
709
838
  return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
710
839
  }
@@ -731,7 +860,7 @@ export class JsonRpcHandler {
731
860
  // Build the turn loop config
732
861
  const loop = new TurnLoop({
733
862
  provider,
734
- model: settings.model_override || settings.model,
863
+ model: settings.providers[settings.current_provider]?.model || settings.model,
735
864
  tools: this.tools,
736
865
  cwd: sessionInfo?.cwd || join(homedir(), '.curie-agent'),
737
866
  settings,
@@ -744,11 +873,13 @@ export class JsonRpcHandler {
744
873
  }, this.sessionStore);
745
874
  // Store the loop for potential cancellation
746
875
  this.turnLoops.set(sessionId, loop);
747
- // Bridge the turn loop's event bus to the shared daemon event bus
748
- // so that WS clients receive real-time events.
876
+ // Bridge the turn loop's event bus to the shared daemon event bus.
877
+ // Note: 'approval-request' is NOT bridged ApprovalTracker.register()
878
+ // emits it directly. In direct mode (no daemonApp), approval events
879
+ // are not tracked externally anyway.
749
880
  const eventTypes = [
750
881
  'user-prompt', 'assistant-delta', 'assistant-stop', 'tool-call',
751
- 'tool-result', 'approval-request', 'approval-decision', 'usage',
882
+ 'tool-result', 'approval-decision', 'usage',
752
883
  'error', 'session-start', 'session-stop', 'hook', 'status',
753
884
  'session-resumed', 'context-warning', 'thinking-delta',
754
885
  // Subagent events
@@ -953,7 +1084,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
953
1084
  }
954
1085
  }
955
1086
  else {
956
- settings.model_override = args;
1087
+ pConfig.model = args;
957
1088
  settings.model = args;
958
1089
  this.settingsManager.update(settings);
959
1090
  this.sharedEventBus?.emit({
@@ -980,9 +1111,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
980
1111
  emitDelta(`Unknown provider: "${args}". Valid options are: ${valid.join(', ')}`);
981
1112
  }
982
1113
  else {
983
- settings.current_provider = provider;
984
- settings.model_override = undefined;
985
- this.settingsManager.update(settings);
1114
+ this.settingsManager.setCurrentProvider(provider);
986
1115
  this.sharedEventBus?.emit({
987
1116
  type: 'config-changed',
988
1117
  id: Math.random().toString(36).substring(7),
@@ -1303,29 +1432,40 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1303
1432
  ? activeLoop.eventBus.history()
1304
1433
  : (this.sessionStore.loadEvents(sessionId) || []);
1305
1434
  const usageEvents = history.filter((e) => e.type === 'usage');
1306
- let input = 0;
1307
- let output = 0;
1308
- for (const e of usageEvents) {
1309
- input += e.inputTokens || 0;
1310
- output += e.outputTokens || 0;
1311
- }
1435
+ const latestUsage = usageEvents[usageEvents.length - 1];
1436
+ const input = latestUsage ? (latestUsage.inputTokens || 0) : 0;
1437
+ const output = latestUsage ? (latestUsage.outputTokens || 0) : 0;
1312
1438
  const model = settings.model || 'unknown';
1313
1439
  const windowSize = settings.providers?.[settings.current_provider]?.model_context_window ?? 200000;
1314
1440
  const pct = input > 0 ? Math.min(100, Math.round((input / windowSize) * 100)) : 0;
1315
- const filled = Math.round((pct / 100) * 24);
1316
- const bar = '█'.repeat(filled) + '░'.repeat(24 - filled);
1317
- const fmt = (n) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
1441
+ const fmt = (n) => {
1442
+ if (n >= 1_000_000)
1443
+ return `${(n / 1_000_000).toFixed(1)}m`;
1444
+ if (n >= 1_000)
1445
+ return `${(n / 1_000).toFixed(1)}k`;
1446
+ return String(n);
1447
+ };
1318
1448
  if (input === 0 && output === 0) {
1319
1449
  emitDelta(`No token data yet. Start a conversation to see context window usage.\n\nActive model: \`${model}\` (Max Context: \`${fmt(windowSize)}\` tokens).`);
1320
1450
  }
1321
1451
  else {
1322
- const lines = [
1323
- `### Context Window Usage (\`${model}\`):`,
1324
- `\`${bar}\` **${pct}%** (${fmt(input)}/${fmt(windowSize)})`,
1325
- `* **Tokens in**: \`${input.toLocaleString()}\``,
1326
- `* **Tokens out**: \`${output.toLocaleString()}\``,
1327
- ];
1328
- emitDelta(lines.join('\n'));
1452
+ const barColor = pct > 80 ? 'var(--red)' : pct > 50 ? 'var(--yellow)' : 'var(--green)';
1453
+ const barGradient = pct > 80
1454
+ ? 'linear-gradient(90deg, var(--red), #e08070)'
1455
+ : pct > 50
1456
+ ? 'linear-gradient(90deg, var(--yellow), #f0d090)'
1457
+ : 'linear-gradient(90deg, var(--green), #c0d8a8)';
1458
+ emitDelta(`<div style="background:var(--s3);border-radius:8px;padding:12px;margin:8px 0">` +
1459
+ `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">` +
1460
+ `<span style="font-family:monospace;font-size:12px;color:var(--text)">Context Window (${model})</span>` +
1461
+ `<span style="font-family:monospace;font-size:13px;font-weight:bold;color:${barColor}">${pct}%</span>` +
1462
+ `</div>` +
1463
+ `<div style="background:var(--s2);border-radius:4px;height:8px;overflow:hidden">` +
1464
+ `<div style="width:${pct}%;height:100%;background:${barGradient};border-radius:4px"></div>` +
1465
+ `</div>` +
1466
+ `<div style="display:flex;justify-content:space-between;margin-top:8px;font-family:monospace;font-size:11px;color:var(--muted)">` +
1467
+ `<span>In: ${fmt(input)}</span><span>Out: ${fmt(output)}</span><span>Max: ${fmt(windowSize)}</span>` +
1468
+ `</div></div>`);
1329
1469
  }
1330
1470
  }
1331
1471
  break;
@@ -1633,7 +1773,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1633
1773
  emitDelta(`Could not parse scheduled time from: "${rest}". Try: \`at 7:55 do something\` or \`tomorrow at 9am do something\`.`);
1634
1774
  }
1635
1775
  else {
1636
- const task = this.daemonApp.taskManager.create({ title: parsed.message, mode: 'auto', scope: 'personal', scheduled_at: parsed.scheduledAt });
1776
+ const task = this.daemonApp.taskManager.create({ title: parsed.message, mode: 'agent', scope: 'personal', scheduled_at: parsed.scheduledAt });
1637
1777
  const timeStr = new Date(task.scheduled_at).toLocaleString();
1638
1778
  emitDelta(`### Task Scheduled Successfully!
1639
1779
  * **Task ID**: \`${task.id}\`
@@ -1647,7 +1787,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1647
1787
  const filter = ['pending', 'executing', 'completed', 'failed', 'cancelled'].includes(rest.toLowerCase())
1648
1788
  ? rest.toLowerCase()
1649
1789
  : undefined;
1650
- const tasks = this.daemonApp.taskManager.list({ mode: 'auto' });
1790
+ const tasks = this.daemonApp.taskManager.list({ mode: 'agent' });
1651
1791
  if (tasks.length === 0) {
1652
1792
  emitDelta(filter ? `No tasks found with status **${filter}**.` : `No tasks scheduled yet. Use \`/task create\` to schedule a task.`);
1653
1793
  }
@@ -1721,7 +1861,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1721
1861
  const hasAllowlist = raw && (Array.isArray(raw) ? raw.length > 0 : typeof raw === 'string' && raw.trim().length > 0);
1722
1862
  const display = Array.isArray(raw) ? raw.join(', ') : raw;
1723
1863
  lines.push(`* **Allowlist:** ${hasAllowlist ? `\`${display}\`` : '(empty)'}`);
1724
- lines.push(`**Blocked:** \`${curieDir}/settings.json\` (API keys)`);
1864
+ lines.push(`**Blocked:** \`~/.curie-settings.json\` (API keys)`);
1725
1865
  emitDelta(lines.join('\n'));
1726
1866
  break;
1727
1867
  }
@@ -1836,7 +1976,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1836
1976
  const provider = this.createProvider(settings);
1837
1977
  const handle = await this.daemonApp.subagentExecutor.spawn({
1838
1978
  provider,
1839
- model: settings.model_override || settings.model,
1979
+ model: settings.providers[settings.current_provider]?.model || settings.model,
1840
1980
  tools: this.tools,
1841
1981
  cwd: join(homedir(), '.curie-agent'),
1842
1982
  settings,
@@ -1862,11 +2002,11 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1862
2002
  const parts = args.trim().split(/\s+/);
1863
2003
  const sub = parts[0]?.toLowerCase() || '';
1864
2004
  const rest = parts.slice(1).join(' ').trim();
1865
- // Detect mode keyword (auto/notify) from full args
2005
+ // Detect mode keyword (agent/notify) from full args
1866
2006
  const fullArgsLower = args.toLowerCase();
1867
- let mode = 'manual';
1868
- if (/^auto\s/.test(fullArgsLower) || /^\bat\b/.test(fullArgsLower)) {
1869
- mode = 'auto';
2007
+ let mode = 'human';
2008
+ if (/^agent\s/.test(fullArgsLower) || /^\bat\b/.test(fullArgsLower)) {
2009
+ mode = 'agent';
1870
2010
  }
1871
2011
  else if (/^notify\s/.test(fullArgsLower) || /remind\s/.test(fullArgsLower)) {
1872
2012
  mode = 'notify';
@@ -1910,10 +2050,10 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
1910
2050
  const parsed = parseReminderTime(instruction);
1911
2051
  if (parsed) {
1912
2052
  instruction = parsed.message;
1913
- if (mode === 'auto') {
2053
+ if (mode === 'agent') {
1914
2054
  const task = this.daemonApp.taskManager.create({
1915
2055
  title: instruction,
1916
- mode: 'auto', scope, scheduled_at: parsed.scheduledAt,
2056
+ mode: 'agent', scope, scheduled_at: parsed.scheduledAt,
1917
2057
  });
1918
2058
  emitDelta(`**Task scheduled**: "${instruction}" at ${new Date(parsed.scheduledAt).toLocaleString()} (ID: \`${task.id.slice(0, 8)}...\`)`);
1919
2059
  }
@@ -2079,7 +2219,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
2079
2219
  }
2080
2220
  const settings = this.settingsManager.get();
2081
2221
  const provider = this.createProvider(settings);
2082
- const model = settings.model_override || settings.model;
2222
+ const model = settings.providers[settings.current_provider]?.model || settings.model;
2083
2223
  const events = this.sessionStore.loadEvents(sessionId) || [];
2084
2224
  if (events.length === 0) {
2085
2225
  throw new Error('No events to compact');
@@ -2146,12 +2286,8 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
2146
2286
  const settings = this.settingsManager.get();
2147
2287
  const history = this.sessionStore.loadEvents(sessionId) || [];
2148
2288
  const usageEvents = history.filter((e) => e.type === 'usage');
2149
- let input = 0;
2150
- let output = 0;
2151
- for (const e of usageEvents) {
2152
- input += e.inputTokens || 0;
2153
- output += e.outputTokens || 0;
2154
- }
2289
+ const latestUsage = usageEvents[usageEvents.length - 1];
2290
+ const input = latestUsage?.inputTokens ?? 0;
2155
2291
  if (input === 0)
2156
2292
  return; // No token data yet
2157
2293
  const windowSize = settings.providers?.[settings.current_provider]?.model_context_window ?? 200000;