@@ -322,10 +322,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
322322 consecutiveNoAssistantMessagesCount : number = 0
323323 toolUsage : ToolUsage = { }
324324
325- // Conversation message counts, summarized once on Task Completed instead
326- // of emitting a separate telemetry event per turn.
325+ // Conversation message counts, summarized once per Task Completed
326+ // installment instead of emitting a separate telemetry event per turn.
327327 messageCounts : { user : number ; assistant : number } = { user : 0 , assistant : 0 }
328328
329+ // Idle/shutdown telemetry flush: reports toolUsage/messageCounts for tasks that
330+ // go quiet or get torn down without the model ever calling attempt_completion
331+ // (or without the user accepting it), so long-running/abandoned tasks aren't
332+ // invisible to telemetry. Each flush reports only what changed since the previous
333+ // one, tracked via telemetryToolUsageBaseline/telemetryMessageCountsBaseline --
334+ // task.toolUsage/messageCounts themselves are never mutated by this, since they're
335+ // also read as running totals by the public TaskCompleted API event and the UI.
336+ // Checked on an interval rather than hooked into every say()/ask() call site.
337+ private static readonly IDLE_TELEMETRY_CHECK_INTERVAL_MS = 5 * 60 * 1000
338+ private static readonly IDLE_TELEMETRY_THRESHOLD_MS = 30 * 60 * 1000
339+ private idleTelemetryCheckInterval ?: NodeJS . Timeout
340+ private lastTelemetryFlushAt : number = Date . now ( )
341+ private telemetryToolUsageBaseline : ToolUsage = { }
342+ private telemetryMessageCountsBaseline : { user : number ; assistant : number } = { user : 0 , assistant : 0 }
343+
329344 // Checkpoints
330345 enableCheckpoints : boolean
331346 checkpointTimeout : number
@@ -598,6 +613,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
598613 { leading : true , trailing : true , maxWait : this . TOKEN_USAGE_EMIT_INTERVAL_MS } ,
599614 )
600615
616+ this . startIdleTelemetryCheck ( )
617+
601618 onCreated ?.( this )
602619
603620 if ( startTask ) {
@@ -2238,6 +2255,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
22382255 public dispose ( ) : void {
22392256 console . log ( `[Task#dispose] disposing task ${ this . taskId } .${ this . instanceId } ` )
22402257
2258+ // Stop the idle telemetry check and report any unflushed activity as a
2259+ // shutdown installment, so a task torn down mid-work (panel closed, task
2260+ // switched, extension deactivated) isn't invisible to telemetry.
2261+ try {
2262+ clearInterval ( this . idleTelemetryCheckInterval )
2263+ this . idleTelemetryCheckInterval = undefined
2264+ this . flushTelemetryInstallment ( "shutdown" )
2265+ } catch ( error ) {
2266+ console . error ( "Error flushing shutdown telemetry:" , error )
2267+ }
2268+
22412269 // Cancel any in-progress HTTP request
22422270 try {
22432271 this . cancelCurrentRequest ( )
@@ -4645,6 +4673,70 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
46454673 }
46464674 }
46474675
4676+ /**
4677+ * Emits a Task Completed installment for whatever toolUsage/messageCounts have
4678+ * changed since the previous installment (from any reason), then advances the
4679+ * telemetry baseline so a later installment reports only its own delta. Does
4680+ * NOT touch task.toolUsage/messageCounts themselves -- those stay running totals
4681+ * for the public TaskCompleted API event and the UI. No-ops if nothing changed
4682+ * since the last installment, so idle/shutdown checks don't emit empty events
4683+ * for tasks that were already fully reported (e.g. right after attempt_completion).
4684+ */
4685+ public flushTelemetryInstallment ( reason : "attempt_completion" | "idle" | "shutdown" ) : void {
4686+ const toolUsageDelta : ToolUsage = { }
4687+
4688+ for ( const [ toolName , usage ] of Object . entries ( this . toolUsage ) as [ ToolName , ToolUsage [ ToolName ] ] [ ] ) {
4689+ if ( ! usage ) {
4690+ continue
4691+ }
4692+
4693+ const baseline = this . telemetryToolUsageBaseline [ toolName ]
4694+ const attempts = usage . attempts - ( baseline ?. attempts ?? 0 )
4695+ const failures = usage . failures - ( baseline ?. failures ?? 0 )
4696+
4697+ if ( attempts > 0 || failures > 0 ) {
4698+ toolUsageDelta [ toolName ] = { attempts, failures }
4699+ }
4700+ }
4701+
4702+ const messageCountDelta = {
4703+ user : this . messageCounts . user - this . telemetryMessageCountsBaseline . user ,
4704+ assistant : this . messageCounts . assistant - this . telemetryMessageCountsBaseline . assistant ,
4705+ }
4706+
4707+ const hasToolUsageDelta = Object . keys ( toolUsageDelta ) . length > 0
4708+ const hasMessageDelta = messageCountDelta . user > 0 || messageCountDelta . assistant > 0
4709+
4710+ if ( ! hasToolUsageDelta && ! hasMessageDelta ) {
4711+ return
4712+ }
4713+
4714+ this . emitFinalTokenUsageUpdate ( )
4715+ TelemetryService . instance . captureTaskCompleted ( this . taskId , toolUsageDelta , messageCountDelta , reason )
4716+
4717+ this . telemetryToolUsageBaseline = JSON . parse ( JSON . stringify ( this . toolUsage ) )
4718+ this . telemetryMessageCountsBaseline = { ...this . messageCounts }
4719+ this . lastTelemetryFlushAt = Date . now ( )
4720+ }
4721+
4722+ private startIdleTelemetryCheck ( ) : void {
4723+ this . idleTelemetryCheckInterval = setInterval ( ( ) => {
4724+ // lastMessageTs only moves forward on activity, so comparing it against the
4725+ // last flush tells us whether anything happened since that flush -- if the
4726+ // task has been quiet since well before the last flush, there's nothing new
4727+ // to report and flushTelemetryInstallment's own empty-check would no-op anyway,
4728+ // but skipping here avoids waking up to do that check needlessly.
4729+ const idleForMs = Date . now ( ) - ( this . lastMessageTs ?? this . lastTelemetryFlushAt )
4730+
4731+ if ( idleForMs >= Task . IDLE_TELEMETRY_THRESHOLD_MS ) {
4732+ this . flushTelemetryInstallment ( "idle" )
4733+ }
4734+ } , Task . IDLE_TELEMETRY_CHECK_INTERVAL_MS )
4735+
4736+ // Don't hold the process open just for this timer.
4737+ this . idleTelemetryCheckInterval ?. unref ?.( )
4738+ }
4739+
46484740 // Getters
46494741
46504742 public get taskStatus ( ) : TaskStatus {
0 commit comments