Skip to content

Commit d98f9db

Browse files
author
Yevgeniy Vahlis
committed
fix(mm-plugin): freeze subagent elapsed time on completion instead of growing forever
1 parent 1995edc commit d98f9db

File tree

4 files changed

+71
-67
lines changed

4 files changed

+71
-67
lines changed

.opencode/plugin/mattermost-control/event-handlers/subagent.ts

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -185,22 +185,23 @@ export async function handleTaskToolCompleted(event: any): Promise<void> {
185185
}
186186

187187
export async function collapseSubagentOnIdle(childSessionId: string): Promise<void> {
188-
const info = PluginState.subagentRegistry.get(childSessionId);
189-
const mmClient = PluginState.mmClient;
190-
if (!info || !mmClient) return;
191-
if (info.status !== "running") return;
188+
const info = PluginState.subagentRegistry.get(childSessionId);
189+
const mmClient = PluginState.mmClient;
190+
if (!info || !mmClient) return;
191+
if (info.status !== "running") return;
192192

193-
// Get child context before deleting it
194-
const childCtx = PluginState.activeResponseContexts.get(childSessionId);
193+
// Get child context before deleting it
194+
const childCtx = PluginState.activeResponseContexts.get(childSessionId);
195195

196-
const elapsed = formatElapsedTime(Date.now() - info.startTime);
197-
const costPart = childCtx?.cost?.currentMessage ? ` | 💰 $${childCtx.cost.currentMessage.toFixed(2)}` : "";
198-
const modelPart = info.modelId ? ` | 🧠 ${info.modelId}` : (childCtx?.modelId ? ` | 🧠 ${childCtx.modelId}` : "");
199-
const summary = `✅ ${formatTaskLabel(info.agentType, info.description)} (${elapsed}, ${info.toolCount} tools)${costPart}${modelPart}`;
196+
info.endTime = Date.now();
197+
const elapsed = formatElapsedTime(info.endTime - info.startTime);
198+
const costPart = childCtx?.cost?.currentMessage ? ` | 💰 $${childCtx.cost.currentMessage.toFixed(2)}` : "";
199+
const modelPart = info.modelId ? ` | 🧠 ${info.modelId}` : (childCtx?.modelId ? ` | 🧠 ${childCtx.modelId}` : "");
200+
const summary = `✅ ${formatTaskLabel(info.agentType, info.description)} (${elapsed}, ${info.toolCount} tools)${costPart}${modelPart}`;
200201

201-
log.info(`[Subagent] Child idle — collapsing: child=${childSessionId.substring(0, 8)}, type=${info.agentType}, ${elapsed}, ${info.toolCount} tools`);
202-
await mmClient.updatePost(info.replyPostId, summary);
203-
info.status = "completed";
202+
log.info(`[Subagent] Child idle — collapsing: child=${childSessionId.substring(0, 8)}, type=${info.agentType}, ${elapsed}, ${info.toolCount} tools`);
203+
await mmClient.updatePost(info.replyPostId, summary);
204+
info.status = "completed";
204205

205206
await updateResponseStream(info.parentSessionId);
206207

@@ -210,30 +211,31 @@ export async function collapseSubagentOnIdle(childSessionId: string): Promise<vo
210211
}
211212

212213
export async function handleTaskToolError(event: any): Promise<void> {
213-
const part = event.properties?.part;
214-
if (!part || part.tool !== "task") return;
215-
if (part.state?.status !== "error") return;
214+
const part = event.properties?.part;
215+
if (!part || part.tool !== "task") return;
216+
if (part.state?.status !== "error") return;
216217

217-
const childSessionId = part.state?.metadata?.sessionId;
218-
if (!childSessionId) return;
218+
const childSessionId = part.state?.metadata?.sessionId;
219+
if (!childSessionId) return;
219220

220-
const info = PluginState.subagentRegistry.get(childSessionId);
221-
const mmClient = PluginState.mmClient;
222-
if (!info || !mmClient) return;
221+
const info = PluginState.subagentRegistry.get(childSessionId);
222+
const mmClient = PluginState.mmClient;
223+
if (!info || !mmClient) return;
223224

224-
// Get child context before deleting it
225-
const childCtx = PluginState.activeResponseContexts.get(childSessionId);
225+
// Get child context before deleting it
226+
const childCtx = PluginState.activeResponseContexts.get(childSessionId);
226227

227-
const errorMessage = part.state?.error || part.state?.metadata?.error;
228-
const costPart = childCtx?.cost?.currentMessage ? ` | 💰 $${childCtx.cost.currentMessage.toFixed(2)}` : "";
229-
const modelPart = info.modelId ? ` | 🧠 ${info.modelId}` : (childCtx?.modelId ? ` | 🧠 ${childCtx.modelId}` : "");
230-
const summary = errorMessage
231-
? `❌ ${formatTaskLabel(info.agentType, info.description)} (failed: ${errorMessage})${costPart}${modelPart}`
232-
: `❌ ${formatTaskLabel(info.agentType, info.description)} (failed)${costPart}${modelPart}`;
228+
info.endTime = Date.now();
229+
const errorMessage = part.state?.error || part.state?.metadata?.error;
230+
const costPart = childCtx?.cost?.currentMessage ? ` | 💰 $${childCtx.cost.currentMessage.toFixed(2)}` : "";
231+
const modelPart = info.modelId ? ` | 🧠 ${info.modelId}` : (childCtx?.modelId ? ` | 🧠 ${childCtx.modelId}` : "");
232+
const summary = errorMessage
233+
? `❌ ${formatTaskLabel(info.agentType, info.description)} (failed: ${errorMessage})${costPart}${modelPart}`
234+
: `❌ ${formatTaskLabel(info.agentType, info.description)} (failed)${costPart}${modelPart}`;
233235

234-
log.info(`[Subagent] Error: child=${childSessionId.substring(0, 8)}, type=${info.agentType}, error=${errorMessage || 'unknown'} — collapsing reply`);
235-
await mmClient.updatePost(info.replyPostId, summary);
236-
info.status = "error";
236+
log.info(`[Subagent] Error: child=${childSessionId.substring(0, 8)}, type=${info.agentType}, error=${errorMessage || 'unknown'} — collapsing reply`);
237+
await mmClient.updatePost(info.replyPostId, summary);
238+
info.status = "error";
237239

238240
await updateResponseStream(info.parentSessionId);
239241

@@ -243,28 +245,29 @@ export async function handleTaskToolError(event: any): Promise<void> {
243245
}
244246

245247
export async function cleanupSubagentsForParent(parentSessionId: string): Promise<void> {
246-
const mmClient = PluginState.mmClient;
247-
if (!mmClient) return;
248-
249-
const entries = Array.from(PluginState.subagentRegistry.values()).filter(
250-
(entry) => entry.parentSessionId === parentSessionId
251-
);
252-
253-
log.info(`[Subagent] Cleanup: parent=${parentSessionId.substring(0, 8)}, ${entries.length} child subagents to clean up`);
254-
for (const entry of entries) {
255-
if (entry.status === "running") {
256-
const summary = `❌ ${formatTaskLabel(entry.agentType, entry.description)} (cancelled)`;
257-
try {
258-
await mmClient.updatePost(entry.replyPostId, summary);
259-
} catch (e) {
260-
log.debug(`[Subagent] Failed to collapse reply ${entry.replyPostId}: ${e}`);
261-
}
262-
} else {
263-
log.debug(`[Subagent] Cleanup: skipping ${entry.childSessionId.substring(0, 8)} (already ${entry.status})`);
264-
}
265-
PluginState.activeResponseContexts.delete(entry.childSessionId);
266-
stopActiveToolTimer(entry.childSessionId);
267-
stopResponseTimer(entry.childSessionId);
268-
PluginState.subagentRegistry.delete(entry.childSessionId);
269-
}
248+
const mmClient = PluginState.mmClient;
249+
if (!mmClient) return;
250+
251+
const entries = Array.from(PluginState.subagentRegistry.values()).filter(
252+
(entry) => entry.parentSessionId === parentSessionId
253+
);
254+
255+
log.info(`[Subagent] Cleanup: parent=${parentSessionId.substring(0, 8)}, ${entries.length} child subagents to clean up`);
256+
for (const entry of entries) {
257+
if (entry.status === "running") {
258+
entry.endTime = Date.now();
259+
const summary = `❌ ${formatTaskLabel(entry.agentType, entry.description)} (cancelled)`;
260+
try {
261+
await mmClient.updatePost(entry.replyPostId, summary);
262+
} catch (e) {
263+
log.debug(`[Subagent] Failed to collapse reply ${entry.replyPostId}: ${e}`);
264+
}
265+
} else {
266+
log.debug(`[Subagent] Cleanup: skipping ${entry.childSessionId.substring(0, 8)} (already ${entry.status})`);
267+
}
268+
PluginState.activeResponseContexts.delete(entry.childSessionId);
269+
stopActiveToolTimer(entry.childSessionId);
270+
stopResponseTimer(entry.childSessionId);
271+
PluginState.subagentRegistry.delete(entry.childSessionId);
272+
}
270273
}

.opencode/plugin/mattermost-control/formatters.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,16 @@ export function formatSubagentStatus(subagents: SubagentInfo[]): string {
160160
return `🕵️ ${subagents.length} subagents (${runningCount} running, ${doneCount} done)`;
161161
}
162162

163-
return subagents.map((entry) => {
164-
if (entry.status === "completed") {
165-
const elapsed = formatElapsedTime(Date.now() - entry.startTime);
166-
return `✅ ${entry.agentType} (${elapsed})`;
167-
}
168-
if (entry.status === "error") {
169-
return `❌ ${entry.agentType} (failed)`;
170-
}
171-
return `🕵️ ${entry.agentType} (${entry.toolCount} tools)`;
172-
}).join(" | ");
163+
return subagents.map((entry) => {
164+
if (entry.status === "completed") {
165+
const elapsed = formatElapsedTime((entry.endTime || Date.now()) - entry.startTime);
166+
return `✅ ${entry.agentType} (${elapsed})`;
167+
}
168+
if (entry.status === "error") {
169+
return `❌ ${entry.agentType} (failed)`;
170+
}
171+
return `🕵️ ${entry.agentType} (${entry.toolCount} tools)`;
172+
}).join(" | ");
173173
}
174174

175175
export function formatShellOutput(

.opencode/plugin/mattermost-control/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface SubagentInfo {
6565
description: string;
6666
status: string;
6767
startTime: number;
68+
endTime?: number;
6869
toolCount: number;
6970
modelId?: string;
7071
agentHeader: string;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-mattermost-control",
3-
"version": "0.3.92",
3+
"version": "0.3.93",
44
"description": "OpenCode plugin for remote control via Mattermost DMs",
55
"type": "module",
66
"main": ".opencode/plugin/mattermost-control/index.ts",

0 commit comments

Comments
 (0)