@@ -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 ) {
@@ -2237,6 +2254,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
22372254 public dispose ( ) : void {
22382255 console . log ( `[Task#dispose] disposing task ${ this . taskId } .${ this . instanceId } ` )
22392256
2257+ // Stop the idle telemetry check and report any unflushed activity as a
2258+ // shutdown installment, so a task torn down mid-work (panel closed, task
2259+ // switched, extension deactivated) isn't invisible to telemetry.
2260+ try {
2261+ clearInterval ( this . idleTelemetryCheckInterval )
2262+ this . idleTelemetryCheckInterval = undefined
2263+ this . flushTelemetryInstallment ( "shutdown" )
2264+ } catch ( error ) {
2265+ console . error ( "Error flushing shutdown telemetry:" , error )
2266+ }
2267+
22402268 // Cancel any in-progress HTTP request
22412269 try {
22422270 this . cancelCurrentRequest ( )
@@ -4644,6 +4672,70 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
46444672 }
46454673 }
46464674
4675+ /**
4676+ * Emits a Task Completed installment for whatever toolUsage/messageCounts have
4677+ * changed since the previous installment (from any reason), then advances the
4678+ * telemetry baseline so a later installment reports only its own delta. Does
4679+ * NOT touch task.toolUsage/messageCounts themselves -- those stay running totals
4680+ * for the public TaskCompleted API event and the UI. No-ops if nothing changed
4681+ * since the last installment, so idle/shutdown checks don't emit empty events
4682+ * for tasks that were already fully reported (e.g. right after attempt_completion).
4683+ */
4684+ public flushTelemetryInstallment ( reason : "attempt_completion" | "idle" | "shutdown" ) : void {
4685+ const toolUsageDelta : ToolUsage = { }
4686+
4687+ for ( const [ toolName , usage ] of Object . entries ( this . toolUsage ) as [ ToolName , ToolUsage [ ToolName ] ] [ ] ) {
4688+ if ( ! usage ) {
4689+ continue
4690+ }
4691+
4692+ const baseline = this . telemetryToolUsageBaseline [ toolName ]
4693+ const attempts = usage . attempts - ( baseline ?. attempts ?? 0 )
4694+ const failures = usage . failures - ( baseline ?. failures ?? 0 )
4695+
4696+ if ( attempts > 0 || failures > 0 ) {
4697+ toolUsageDelta [ toolName ] = { attempts, failures }
4698+ }
4699+ }
4700+
4701+ const messageCountDelta = {
4702+ user : this . messageCounts . user - this . telemetryMessageCountsBaseline . user ,
4703+ assistant : this . messageCounts . assistant - this . telemetryMessageCountsBaseline . assistant ,
4704+ }
4705+
4706+ const hasToolUsageDelta = Object . keys ( toolUsageDelta ) . length > 0
4707+ const hasMessageDelta = messageCountDelta . user > 0 || messageCountDelta . assistant > 0
4708+
4709+ if ( ! hasToolUsageDelta && ! hasMessageDelta ) {
4710+ return
4711+ }
4712+
4713+ this . emitFinalTokenUsageUpdate ( )
4714+ TelemetryService . instance . captureTaskCompleted ( this . taskId , toolUsageDelta , messageCountDelta , reason )
4715+
4716+ this . telemetryToolUsageBaseline = JSON . parse ( JSON . stringify ( this . toolUsage ) )
4717+ this . telemetryMessageCountsBaseline = { ...this . messageCounts }
4718+ this . lastTelemetryFlushAt = Date . now ( )
4719+ }
4720+
4721+ private startIdleTelemetryCheck ( ) : void {
4722+ this . idleTelemetryCheckInterval = setInterval ( ( ) => {
4723+ // lastMessageTs only moves forward on activity, so comparing it against the
4724+ // last flush tells us whether anything happened since that flush -- if the
4725+ // task has been quiet since well before the last flush, there's nothing new
4726+ // to report and flushTelemetryInstallment's own empty-check would no-op anyway,
4727+ // but skipping here avoids waking up to do that check needlessly.
4728+ const idleForMs = Date . now ( ) - ( this . lastMessageTs ?? this . lastTelemetryFlushAt )
4729+
4730+ if ( idleForMs >= Task . IDLE_TELEMETRY_THRESHOLD_MS ) {
4731+ this . flushTelemetryInstallment ( "idle" )
4732+ }
4733+ } , Task . IDLE_TELEMETRY_CHECK_INTERVAL_MS )
4734+
4735+ // Don't hold the process open just for this timer.
4736+ this . idleTelemetryCheckInterval ?. unref ?.( )
4737+ }
4738+
46474739 // Getters
46484740
46494741 public get taskStatus ( ) : TaskStatus {
0 commit comments