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/assets/main-C8GYJbe5.css +1 -0
- package/dist/assets/{main-C0I0fw2M.js → main-MxBRZvMh.js} +87 -87
- package/dist/index.html +2 -2
- package/dist/src/packages/landing/index.html +10 -0
- package/dist/src/packages/server/claude/backend.js +70 -10
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +13 -3
- package/dist/src/packages/server/services/agent-service.js +3 -1
- package/dist/src/packages/server/services/runtime-events.js +72 -30
- package/dist/src/packages/server/services/runtime-subagents.js +4 -0
- package/dist/src/packages/server/services/supervisor-service.js +4 -2
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +285 -21
- package/dist/src/packages/server/websocket/handlers/command-handler.js +13 -18
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +46 -6
- package/package.json +3 -2
- package/dist/assets/main-flegxlsj.css +0 -1
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-
|
|
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-
|
|
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
|
-
|
|
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)
|
|
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.]+)
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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.]+)
|
|
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 =
|
|
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
|
-
|
|
116
|
-
if (event.
|
|
117
|
-
this.
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
contextUsed = cacheRead + cacheCreation + inputTokens
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
};
|