tide-commander 0.67.3 → 0.69.0

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/index.html CHANGED
@@ -22,11 +22,11 @@
22
22
  <link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
23
23
  <link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
24
24
  <title>Tide Commander</title>
25
- <script type="module" crossorigin src="/assets/main-C0I0fw2M.js"></script>
25
+ <script type="module" crossorigin src="/assets/main-MxBRZvMh.js"></script>
26
26
  <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
28
28
  <link rel="modulepreload" crossorigin href="/assets/vendor-three-DJ4p3FLF.js">
29
- <link rel="stylesheet" crossorigin href="/assets/main-flegxlsj.css">
29
+ <link rel="stylesheet" crossorigin href="/assets/main-C8GYJbe5.css">
30
30
  </head>
31
31
  <body>
32
32
  <div id="app"></div>
@@ -1,6 +1,16 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
+ <!-- Google tag (gtag.js) -->
5
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-5WBC1WJMYY"></script>
6
+ <script>
7
+ window.dataLayer = window.dataLayer || [];
8
+ function gtag(){dataLayer.push(arguments);}
9
+ gtag('js', new Date());
10
+
11
+ gtag('config', 'G-5WBC1WJMYY');
12
+ </script>
13
+
4
14
  <meta charset="UTF-8" />
5
15
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
16
  <title>Tide Commander - Visual Multi-Agent Orchestrator for Claude Code & Codex</title>
@@ -123,6 +123,8 @@ export class ClaudeBackend {
123
123
  log.log(`parseEvent: assistant message has ${toolUseBlocks.length} tool_use block(s): ${toolUseBlocks.map((b) => b.name).join(', ')}`);
124
124
  }
125
125
  }
126
+ // Capture parent_tool_use_id from the raw event for propagation
127
+ const parentToolUseId = event.parent_tool_use_id;
126
128
  let result = null;
127
129
  switch (event.type) {
128
130
  case 'system':
@@ -151,6 +153,19 @@ export class ClaudeBackend {
151
153
  // Log when we're dropping events (assistant events may return null for text-only content)
152
154
  log.log(`parseEvent: returned NULL for type=${event.type}, subtype=${event.subtype || 'none'}`);
153
155
  }
156
+ // Propagate parent_tool_use_id onto all returned events (links subagent internal events to parent)
157
+ if (result && parentToolUseId) {
158
+ if (Array.isArray(result)) {
159
+ for (const r of result) {
160
+ if (!r.parentToolUseId)
161
+ r.parentToolUseId = parentToolUseId;
162
+ }
163
+ }
164
+ else {
165
+ if (!result.parentToolUseId)
166
+ result.parentToolUseId = parentToolUseId;
167
+ }
168
+ }
154
169
  return result;
155
170
  }
156
171
  parseUserEvent(event) {
@@ -177,12 +192,29 @@ export class ClaudeBackend {
177
192
  // Look up the tool name from the tool_use_id mapping
178
193
  const toolName = toolUseIdToName.get(block.tool_use_id) || 'unknown';
179
194
  log.log(`parseUserEvent: Found tool_result for tool_use_id=${block.tool_use_id}, toolName=${toolName}, content length=${content?.length || 0}, hasToolUseResult=${!!event.tool_use_result}`);
180
- toolResults.push({
195
+ const toolResult = {
181
196
  type: 'tool_result',
182
197
  toolName,
183
198
  toolOutput: content,
184
199
  toolUseId: block.tool_use_id, // Preserve for subagent correlation
185
- });
200
+ };
201
+ // Extract subagent stats from Task tool completion metadata
202
+ if (toolName === 'Task' && event.tool_use_result) {
203
+ const tur = event.tool_use_result;
204
+ if (tur.totalDurationMs || tur.totalTokens || tur.totalToolUseCount) {
205
+ toolResult.subagentStats = {
206
+ durationMs: tur.totalDurationMs || 0,
207
+ tokensUsed: tur.totalTokens || 0,
208
+ toolUseCount: tur.totalToolUseCount || 0,
209
+ };
210
+ log.log(`parseUserEvent: Task tool stats - duration=${tur.totalDurationMs}ms, tokens=${tur.totalTokens}, tools=${tur.totalToolUseCount}`);
211
+ }
212
+ }
213
+ // Propagate parent_tool_use_id if present
214
+ if (event.parent_tool_use_id) {
215
+ toolResult.parentToolUseId = event.parent_tool_use_id;
216
+ }
217
+ toolResults.push(toolResult);
186
218
  // Clean up the mapping after use (tool_use_id is unique per invocation)
187
219
  toolUseIdToName.delete(block.tool_use_id);
188
220
  }
@@ -234,6 +266,10 @@ export class ClaudeBackend {
234
266
  errorMessage: event.error,
235
267
  };
236
268
  }
269
+ // Capture task_started events (links task_id to tool_use_id)
270
+ if (event.subtype === 'task_started' && event.task_id) {
271
+ log.log(`parseSystemEvent: task_started - task_id=${event.task_id}, tool_use_id=${event.tool_use_id}`);
272
+ }
237
273
  return null;
238
274
  }
239
275
  parseAssistantEvent(event) {
@@ -243,6 +279,7 @@ export class ClaudeBackend {
243
279
  const events = [];
244
280
  // Use event UUID if available (unique identifier from Claude)
245
281
  const uuid = event.uuid;
282
+ const usage = event.message.usage;
246
283
  // Extract text blocks - emit as non-streaming final text
247
284
  // This ensures text is captured even if streaming deltas were missed
248
285
  const textBlocks = event.message.content.filter((b) => b.type === 'text');
@@ -272,6 +309,10 @@ export class ClaudeBackend {
272
309
  toolUseId: block.id,
273
310
  uuid: block.id, // tool_use block has unique ID for deduplication
274
311
  };
312
+ // Propagate parent_tool_use_id if present (links subagent events to parent Task invocation)
313
+ if (event.parent_tool_use_id) {
314
+ toolEvent.parentToolUseId = event.parent_tool_use_id;
315
+ }
275
316
  // Extract subagent metadata from Task tool inputs
276
317
  if (toolName === 'Task' && block.input) {
277
318
  const input = block.input;
@@ -283,8 +324,23 @@ export class ClaudeBackend {
283
324
  }
284
325
  events.push(toolEvent);
285
326
  }
327
+ // Assistant messages include usage snapshots that reflect current context
328
+ // occupancy during the turn. Emit a lightweight event so runtime state can
329
+ // update in near real-time instead of waiting for step_complete or /context.
330
+ if (usage) {
331
+ events.push({
332
+ type: 'usage_snapshot',
333
+ tokens: {
334
+ input: usage.input_tokens || 0,
335
+ output: usage.output_tokens || 0,
336
+ cacheCreation: usage.cache_creation_input_tokens || 0,
337
+ cacheRead: usage.cache_read_input_tokens || 0,
338
+ },
339
+ uuid,
340
+ });
341
+ }
286
342
  if (events.length > 0) {
287
- log.log(`parseAssistantEvent: extracted ${textBlocks.length} text block(s) and ${toolUseBlocks.length} tool_use block(s), uuid=${uuid}`);
343
+ log.log(`parseAssistantEvent: extracted ${textBlocks.length} text block(s), ${toolUseBlocks.length} tool_use block(s), usage=${usage ? 'yes' : 'no'}, uuid=${uuid}`);
288
344
  return events.length === 1 ? events[0] : events;
289
345
  }
290
346
  }
@@ -495,16 +551,18 @@ export function parseContextOutput(content) {
495
551
  const modelMatch = content.match(/\*\*Model:\*\*\s*(.+)/);
496
552
  const model = modelMatch ? modelMatch[1].trim() : 'unknown';
497
553
  // Extract total tokens and context window
498
- // Format: **Tokens:** 19.6k / 200.0k (10%)
499
- const tokensMatch = content.match(/\*\*Tokens:\*\*\s*([\d.]+)k?\s*\/\s*([\d.]+)k?\s*\((\d+)%\)/);
554
+ // Format: **Tokens:** 19.6k / 200.0k (10%) or **Tokens:** 377.3k / 1000.0k (38%)
555
+ const tokensMatch = content.match(/\*\*Tokens:\*\*\s*([\d.]+k?)\s*\/\s*([\d.]+k?)\s*\((\d+)%\)/);
500
556
  if (!tokensMatch) {
501
557
  log.log('parseContextOutput: Could not parse tokens line');
502
558
  return null;
503
559
  }
504
560
  const parseTokens = (str) => {
505
- const num = parseFloat(str);
506
- // If original string had 'k' suffix, multiply by 1000
507
- return str.includes('k') || num < 1000 ? num * 1000 : num;
561
+ // The k suffix is now included in the capture group
562
+ if (str.endsWith('k') || str.endsWith('K')) {
563
+ return parseFloat(str) * 1000;
564
+ }
565
+ return parseFloat(str);
508
566
  };
509
567
  const totalTokens = parseTokens(tokensMatch[1]);
510
568
  const contextWindow = parseTokens(tokensMatch[2]);
@@ -512,10 +570,12 @@ export function parseContextOutput(content) {
512
570
  // Parse category table
513
571
  const parseCategory = (name) => {
514
572
  // Match: | Category Name | 3.1k | 1.6% |
515
- const regex = new RegExp(`\\|\\s*${name}\\s*\\|\\s*([\\d.]+)k?\\s*\\|\\s*([\\d.]+)%\\s*\\|`, 'i');
573
+ const regex = new RegExp(`\\|\\s*${name}\\s*\\|\\s*([\\d.]+k?)\\s*\\|\\s*([\\d.]+)%\\s*\\|`, 'i');
516
574
  const match = content.match(regex);
517
575
  if (match) {
518
- const tokens = parseFloat(match[1]) * (match[1].includes('k') || parseFloat(match[1]) < 100 ? 1000 : 1);
576
+ const tokens = match[1].endsWith('k') || match[1].endsWith('K')
577
+ ? parseFloat(match[1]) * 1000
578
+ : parseFloat(match[1]);
519
579
  return { tokens, percent: parseFloat(match[2]) };
520
580
  }
521
581
  return { tokens: 0, percent: 0 };
@@ -101,6 +101,10 @@ export class RunnerStdoutPipeline {
101
101
  if (event.toolName === 'Task' && event.subagentName) {
102
102
  this.activeSubagentName.set(agentId, event.subagentName);
103
103
  }
104
+ // Skip output for subagent internal tools (shown in inline activity panel instead)
105
+ if (event.parentToolUseId) {
106
+ break;
107
+ }
104
108
  const toolStartSubName = event.subagentName || this.activeSubagentName.get(agentId);
105
109
  this.callbacks.onOutput(agentId, `Using tool: ${event.toolName}`, false, toolStartSubName, event.uuid, {
106
110
  toolName: event.toolName,
@@ -112,9 +116,12 @@ export class RunnerStdoutPipeline {
112
116
  break;
113
117
  }
114
118
  case 'tool_result': {
115
- const toolResultSubName = this.activeSubagentName.get(agentId);
116
- if (event.toolName === 'Bash' && event.toolOutput) {
117
- this.callbacks.onOutput(agentId, `Bash output:\n${event.toolOutput}`, false, toolResultSubName, event.uuid);
119
+ // Skip output for subagent internal tools (shown in inline activity panel)
120
+ if (!event.parentToolUseId) {
121
+ const toolResultSubName = this.activeSubagentName.get(agentId);
122
+ if (event.toolName === 'Bash' && event.toolOutput) {
123
+ this.callbacks.onOutput(agentId, `Bash output:\n${event.toolOutput}`, false, toolResultSubName, event.uuid);
124
+ }
118
125
  }
119
126
  if (event.toolName === 'Task') {
120
127
  this.activeSubagentName.delete(agentId);
@@ -141,6 +148,9 @@ export class RunnerStdoutPipeline {
141
148
  case 'error':
142
149
  this.callbacks.onError(agentId, event.errorMessage || 'Unknown error');
143
150
  break;
151
+ case 'usage_snapshot':
152
+ // Silently pass through to onEvent (already called above) - no output needed
153
+ break;
144
154
  case 'context_stats':
145
155
  if (event.contextStatsRaw) {
146
156
  this.callbacks.onOutput(agentId, event.contextStatsRaw, false, undefined, event.uuid);
@@ -46,7 +46,9 @@ export function initAgents() {
46
46
  const persistedContextUsed = typeof stored.contextUsed === 'number'
47
47
  ? stored.contextUsed
48
48
  : tokensUsed;
49
- const contextUsed = Math.max(0, Math.min(persistedContextUsed, contextLimit));
49
+ // Don't clamp to contextLimit - contextUsed can legitimately exceed the default
50
+ // 200k limit for models with larger context windows (up to 1M).
51
+ const contextUsed = Math.max(0, persistedContextUsed);
50
52
  // Track agents that were working before restart
51
53
  // Use lastAssignedTask (which persists) instead of currentTask (which gets cleared)
52
54
  // Only resume if task was assigned within the last 5 minutes
@@ -1,7 +1,7 @@
1
1
  import { loadSession } from '../claude/session-loader.js';
2
2
  import * as agentService from './agent-service.js';
3
3
  import * as supervisorService from './supervisor-service.js';
4
- import { clearPendingSilentContextRefresh, consumeStepCompleteReceived, hasPendingSilentContextRefresh, markStepCompleteReceived, } from './runtime-watchdog.js';
4
+ import { clearPendingSilentContextRefresh, consumeStepCompleteReceived, markStepCompleteReceived, } from './runtime-watchdog.js';
5
5
  import { handleTaskToolResult, handleTaskToolStart } from './runtime-subagents.js';
6
6
  const DEFAULT_CODEX_CONTEXT_WINDOW = 200000;
7
7
  const CODEX_ROLLING_CONTEXT_TURNS = 40;
@@ -86,7 +86,7 @@ function buildEstimatedCodexContextStats(totalTokens, contextWindow, model) {
86
86
  };
87
87
  }
88
88
  export function createRuntimeEventHandlers(deps) {
89
- const { log, emitEvent, emitOutput, emitComplete, emitError, parseUsageOutput, executeCommand, scheduleSilentContextRefresh, } = deps;
89
+ const { log, emitEvent, emitOutput, emitComplete, emitError, parseUsageOutput, executeCommand, } = deps;
90
90
  function handleEvent(agentId, event) {
91
91
  const agent = agentService.getAgent(agentId);
92
92
  if (!agent) {
@@ -112,6 +112,30 @@ export function createRuntimeEventHandlers(deps) {
112
112
  handleTaskToolResult(agentId, event, log);
113
113
  agentService.updateAgent(agentId, { currentTool: undefined });
114
114
  break;
115
+ case 'usage_snapshot': {
116
+ // Real-time context tracking from streaming usage data.
117
+ // Context window usage = input tokens only (output tokens don't count toward the limit).
118
+ // total_input = cache_read + cache_creation + input_tokens (the full prompt size).
119
+ if (event.tokens) {
120
+ const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
121
+ if (isClaudeProvider) {
122
+ const cacheRead = event.tokens.cacheRead || 0;
123
+ const cacheCreation = event.tokens.cacheCreation || 0;
124
+ const inputTokens = event.tokens.input || 0;
125
+ // Context window = input side only (system prompt + tools + messages).
126
+ // Output tokens are the model's response and don't count toward the limit.
127
+ const snapshotContextUsed = cacheRead + cacheCreation + inputTokens;
128
+ if (snapshotContextUsed > 0) {
129
+ const updates = {
130
+ contextUsed: Math.max(0, snapshotContextUsed),
131
+ };
132
+ agentService.updateAgent(agentId, updates, false);
133
+ log.log(`[usage_snapshot] ${agentId}: input=${inputTokens} + cacheRead=${cacheRead} + cacheCreation=${cacheCreation} = ${snapshotContextUsed} (output=${event.tokens.output || 0}) limit=${agent.contextLimit || '?'}`);
134
+ }
135
+ }
136
+ }
137
+ break;
138
+ }
115
139
  case 'step_complete': {
116
140
  markStepCompleteReceived(agentId);
117
141
  const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
@@ -120,12 +144,21 @@ export function createRuntimeEventHandlers(deps) {
120
144
  const isContextCommand = lastTask === '/context' || lastTask === '/cost' || lastTask === '/compact';
121
145
  let contextUsed = agent.contextUsed || 0;
122
146
  let contextLimit = agent.contextLimit || 200000;
123
- if (event.modelUsage) {
147
+ // IMPORTANT: event.modelUsage is often {} (empty object) which is truthy.
148
+ // We must check that it has actual data before using it, otherwise we'd
149
+ // zero out contextUsed (since all fields would be undefined → 0).
150
+ const hasModelUsageData = event.modelUsage && Object.keys(event.modelUsage).length > 0;
151
+ if (hasModelUsageData && event.modelUsage) {
124
152
  const cacheRead = event.modelUsage.cacheReadInputTokens || 0;
125
153
  const cacheCreation = event.modelUsage.cacheCreationInputTokens || 0;
126
154
  const inputTokens = event.modelUsage.inputTokens || 0;
127
155
  const outputTokens = event.modelUsage.outputTokens || 0;
128
- contextLimit = event.modelUsage.contextWindow || 200000;
156
+ // Only update contextLimit if the event actually carries contextWindow.
157
+ // If undefined, keep the existing agent.contextLimit (which may have been
158
+ // bumped by usage_snapshot or a previous step_complete).
159
+ if (event.modelUsage.contextWindow) {
160
+ contextLimit = event.modelUsage.contextWindow;
161
+ }
129
162
  if (isCodexProvider) {
130
163
  const turnGrowthEstimate = estimateTokensFromText(agent.lastAssignedTask) + outputTokens;
131
164
  const rollingEstimate = updateCodexRollingContextEstimate(agentId, turnGrowthEstimate);
@@ -136,16 +169,18 @@ export function createRuntimeEventHandlers(deps) {
136
169
  : rollingEstimate;
137
170
  }
138
171
  else {
139
- contextUsed = cacheRead + cacheCreation + inputTokens + outputTokens;
172
+ // Context window = input side only (output tokens don't count toward the limit)
173
+ contextUsed = cacheRead + cacheCreation + inputTokens;
140
174
  }
175
+ log.log(`[step_complete] modelUsage data for ${agentId}: cacheRead=${cacheRead}, cacheCreation=${cacheCreation}, input=${inputTokens}, contextWindow=${event.modelUsage.contextWindow || 'none'}`);
141
176
  }
142
177
  else if (event.tokens) {
143
178
  if (isClaudeProvider) {
144
179
  const cacheRead = event.tokens.cacheRead || 0;
145
180
  const cacheCreation = event.tokens.cacheCreation || 0;
146
181
  const inputTokens = event.tokens.input || 0;
147
- const outputTokens = event.tokens.output || 0;
148
- contextUsed = cacheRead + cacheCreation + inputTokens + outputTokens;
182
+ // Context window = input side only
183
+ contextUsed = cacheRead + cacheCreation + inputTokens;
149
184
  }
150
185
  else {
151
186
  const inputTokens = event.tokens.input || 0;
@@ -165,13 +200,35 @@ export function createRuntimeEventHandlers(deps) {
165
200
  && (event.tokens.output || 0) === 0
166
201
  && (event.tokens.cacheRead || 0) === 0
167
202
  && (event.tokens.cacheCreation || 0) === 0;
168
- const hasNoModelUsage = !event.modelUsage;
169
- if (isClaudeProvider && hasZeroTokenUsage && hasNoModelUsage && !isContextCommand) {
203
+ // Treat empty objects {} as "no model usage" (they have no meaningful data)
204
+ const hasNoModelUsage = !hasModelUsageData;
205
+ // For /context, /cost, /compact: the context_stats event already set the
206
+ // authoritative contextUsed/contextLimit values. Don't overwrite them with
207
+ // zero-token step_complete data from the local command.
208
+ if (isContextCommand) {
209
+ contextUsed = agent.contextUsed || 0;
210
+ contextLimit = agent.contextLimit || 200000;
211
+ log.log(`[step_complete] Context command for ${agentId}; preserving context values from context_stats`);
212
+ }
213
+ else if (isClaudeProvider && hasZeroTokenUsage && hasNoModelUsage) {
170
214
  contextUsed = agent.contextUsed || 0;
171
215
  contextLimit = agent.contextLimit || 200000;
172
216
  log.log(`[step_complete] Claude empty usage detected for ${agentId}; preserving previous context`);
173
217
  }
174
- contextUsed = Math.max(0, Math.min(contextUsed, contextLimit));
218
+ else if (isClaudeProvider && !hasModelUsageData && event.tokens) {
219
+ // modelUsage was {} (empty) but we DO have event.tokens.
220
+ // The usage_snapshot handler already set the correct contextUsed value
221
+ // from the assistant event's usage data. Don't overwrite it.
222
+ // The event.tokens here come from the result event, which for Claude
223
+ // in --print mode may have different semantics. Prefer the last
224
+ // usage_snapshot value which is the most accurate real-time reading.
225
+ log.log(`[step_complete] Claude empty modelUsage but has tokens for ${agentId}; preserving usage_snapshot contextUsed=${agent.contextUsed}`);
226
+ contextUsed = agent.contextUsed || 0;
227
+ }
228
+ // Don't clamp contextUsed to contextLimit - models can have up to 1M context.
229
+ // The contextLimit comes from modelUsage.contextWindow which is authoritative.
230
+ contextUsed = Math.max(0, contextUsed);
231
+ log.log(`[step_complete] Final for ${agentId}: contextUsed=${contextUsed}, contextLimit=${contextLimit}, hasModelUsageData=${hasModelUsageData}, tokens=${JSON.stringify(event.tokens)}`);
175
232
  const newTokensUsed = (agent.tokensUsed || 0) + (event.tokens?.input || 0) + (event.tokens?.output || 0);
176
233
  const updates = {
177
234
  tokensUsed: newTokensUsed,
@@ -195,16 +252,9 @@ export function createRuntimeEventHandlers(deps) {
195
252
  else {
196
253
  log.log(`[step_complete] Codex agent ${agentId} will be set idle on process completion`);
197
254
  }
198
- const hasPendingSilentRefresh = hasPendingSilentContextRefresh(agentId);
199
- log.log(`[step_complete] Auto-refresh check: agentId=${agentId}, lastTask="${lastTask}", isContextCmd=${isContextCommand}, hasPending=${hasPendingSilentRefresh}`);
255
+ // Real-time context tracking via usage_snapshot events replaces automatic /context refresh.
256
+ // The /context command is now only triggered manually via the UI refresh button.
200
257
  clearPendingSilentContextRefresh(agentId);
201
- const currentAgent = agentService.getAgent(agentId);
202
- const hasSession = !!currentAgent?.sessionId;
203
- const shouldRefresh = isClaudeProvider && hasSession && !isContextCommand && !hasPendingSilentRefresh;
204
- log.log(`[step_complete] sessionId=${currentAgent?.sessionId}, shouldRefresh=${shouldRefresh}`);
205
- if (shouldRefresh) {
206
- scheduleSilentContextRefresh(agentId, 'step_complete');
207
- }
208
258
  break;
209
259
  }
210
260
  case 'error':
@@ -255,18 +305,10 @@ export function createRuntimeEventHandlers(deps) {
255
305
  isDetached: false,
256
306
  });
257
307
  emitComplete(agentId, success);
308
+ // Real-time context tracking via usage_snapshot events replaces automatic /context refresh.
309
+ // The /context command is now only triggered manually via the UI refresh button.
258
310
  if (!receivedStepComplete && success) {
259
- const agent = agentService.getAgent(agentId);
260
- const lastTask = agent?.lastAssignedTask?.trim() || '';
261
- const isContextCommand = lastTask === '/context' || lastTask === '/cost' || lastTask === '/compact';
262
- const hasSession = !!agent?.sessionId;
263
- const hasPendingSilentRefresh = hasPendingSilentContextRefresh(agentId);
264
- log.log(`[handleComplete] Fallback /context check: agentId=${agentId}, receivedStepComplete=${receivedStepComplete}, lastTask="${lastTask}", isContextCmd=${isContextCommand}, hasSession=${hasSession}, hasPending=${hasPendingSilentRefresh}`);
265
- const isClaudeProvider = (agent?.provider ?? 'claude') === 'claude';
266
- if (isClaudeProvider && hasSession && !isContextCommand && !hasPendingSilentRefresh) {
267
- log.log(`[handleComplete] Triggering fallback /context refresh for agent ${agentId}`);
268
- scheduleSilentContextRefresh(agentId, 'handle_complete');
269
- }
311
+ clearPendingSilentContextRefresh(agentId);
270
312
  }
271
313
  }
272
314
  function handleError(agentId, error) {
@@ -34,6 +34,10 @@ export function handleTaskToolResult(agentId, event, log) {
34
34
  }
35
35
  log.log(`[Subagent] Completed: ${subagent.name} (${subagent.id}) for agent ${agentId}`);
36
36
  event.subagentName = subagent.name;
37
+ // Store completion stats from event onto the subagent before removal
38
+ if (event.subagentStats) {
39
+ subagent.stats = event.subagentStats;
40
+ }
37
41
  activeSubagents.delete(event.toolUseId);
38
42
  subagentIdToToolUseId.delete(subagent.id);
39
43
  }
@@ -266,6 +266,7 @@ export async function generateReport() {
266
266
  recentNarratives: allNarratives,
267
267
  tokensUsed: agent.tokensUsed,
268
268
  contextUsed: agent.contextUsed,
269
+ contextLimit: agent.contextLimit,
269
270
  lastActivityTime: agent.lastActivity,
270
271
  };
271
272
  }));
@@ -383,6 +384,7 @@ async function buildAgentSummary(agent) {
383
384
  recentNarratives: allNarratives,
384
385
  tokensUsed: agent.tokensUsed,
385
386
  contextUsed: agent.contextUsed,
387
+ contextLimit: agent.contextLimit,
386
388
  lastActivityTime: agent.lastActivity,
387
389
  };
388
390
  }
@@ -405,7 +407,7 @@ function buildSingleAgentPrompt(summary) {
405
407
  : 'No task assigned yet',
406
408
  taskAssignedSecondsAgo,
407
409
  tokensUsed: summary.tokensUsed,
408
- contextPercent: Math.round((summary.contextUsed / 200000) * 100),
410
+ contextPercent: Math.round((summary.contextUsed / (summary.contextLimit || 200000)) * 100),
409
411
  timeSinceActivity: Math.round((Date.now() - summary.lastActivityTime) / 1000),
410
412
  recentActivities: summary.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
411
413
  };
@@ -476,7 +478,7 @@ function buildSupervisorPrompt(summaries) {
476
478
  : 'No task assigned yet',
477
479
  taskAssignedSecondsAgo,
478
480
  tokensUsed: s.tokensUsed,
479
- contextPercent: Math.round((s.contextUsed / 200000) * 100),
481
+ contextPercent: Math.round((s.contextUsed / (s.contextLimit || 200000)) * 100),
480
482
  timeSinceActivity: Math.round((Date.now() - s.lastActivityTime) / 1000),
481
483
  recentActivities: s.recentNarratives.map((n) => sanitizeUnicode(n.narrative)).slice(0, 5),
482
484
  };