Skip to main content

vtcode_core/core/agent/events/
context.rs

1//! Runtime-owned task identity and item annotation shared by both runners.
2use crate::exec::events::{
3    CommandActivity, ExecutionContext, InputOrigin, ItemContext, ThreadEvent, ThreadItemDetails,
4};
5use crate::tools::tool_intent::{ShellActivity, classify_shell_activity};
6use std::collections::VecDeque;
7
8const MAX_TRACKED_EXEC_LAUNCHES: usize = 256;
9const MAX_EXEC_ID_BYTES: usize = 256;
10
11struct ExecSessionLaunch {
12    session_id: Option<String>,
13    call_item_id: String,
14    tool_call_id: Option<String>,
15    context: Box<ItemContext>,
16}
17
18/// Validate public rationale bounds at the tool boundary, independent of schemas.
19pub fn validate_decision_input(args: serde_json::Value) -> anyhow::Result<crate::exec::events::DecisionItem> {
20    use anyhow::{Context, bail};
21    crate::tools::output_limits::max_output_tokens(&args)?;
22    let args = crate::tools::output_limits::args_without_output_metadata(&args);
23    if args.as_object().is_none_or(|o| {
24        o.keys()
25            .any(|k| !matches!(k.as_str(), "summary" | "rationale" | "alternatives" | "evidence_ids"))
26    }) {
27        bail!("invalid decision fields");
28    }
29    let d: crate::exec::events::DecisionItem = serde_json::from_value(args).context("invalid public decision")?;
30    if d.summary.trim().is_empty()
31        || d.summary.chars().count() > 240
32        || d.rationale.trim().is_empty()
33        || d.rationale.chars().count() > 1000
34        || d.alternatives.len() > 3
35        || d.evidence_ids.len() > 8
36        || d.alternatives.iter().any(|a| a.chars().count() > 1000)
37        || d.evidence_ids.iter().any(|id| id.is_empty() || id.chars().count() > 240)
38    {
39        bail!("public decision exceeds input bounds");
40    }
41    Ok(d)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use serde_json::json;
48
49    fn track_launch(tracker: &mut ExecutionContextTracker, item: &str, session: &str) {
50        let args = json!({"cmd":"cargo check --locked"});
51        let mut started = super::super::tool_started_event(item.into(), "exec_command", Some(&args), Some(item));
52        tracker.annotate(&mut started);
53        let pending = tracker
54            .exec_session_output_event(item, "exec_command", &args, &json!({"session_id":session}))
55            .unwrap();
56        let ThreadEvent::ItemCompleted(completed) = pending else {
57            panic!("canonical output expected")
58        };
59        let ThreadItemDetails::ToolOutput(output) = completed.item.details else {
60            panic!("tool output expected")
61        };
62        assert_eq!(output.call_id, item);
63        assert_eq!(output.status, crate::exec::events::ToolCallStatus::InProgress);
64    }
65
66    fn background(session: &str, task: &str, exit_code: Option<i32>) -> ThreadEvent {
67        ThreadEvent::ItemCompleted(crate::exec::events::ItemCompletedEvent { item: crate::exec::events::ThreadItem {
68            id: "background".into(), context: None,
69            details: ThreadItemDetails::Harness(Box::new(serde_json::from_value(json!({
70                "event":crate::exec::events::HarnessEventKind::BackgroundSubprocessCompleted,
71                "task_id":task,"session_id":session,"exec_session_id":session,"exit_code":exit_code,"message":"observed exit",
72            })).unwrap())),
73        }})
74    }
75
76    #[test]
77    fn native_delegation_status_is_captured_only_for_its_recorded_parent() {
78        let mut tracker = ExecutionContextTracker::default();
79        let current = tracker.begin("parent", "turn", "Delegate", InputOrigin::User);
80        let output = json!({
81            "id":"agent-1", "session_id":"child", "parent_thread_id":"parent", "agent_name":"reviewer", "display_label":"Review", "description":"Check storage", "source":"builtin", "status":"running", "background":true, "depth":1, "created_at":"2026-10-03T00:00:00Z", "updated_at":"2026-10-03T00:00:01Z",
82        });
83        let event = tracker.delegation_status_event("call", "agent", &output).unwrap();
84        let ThreadEvent::ItemCompleted(completed) = event else {
85            panic!("status event expected")
86        };
87        let context = completed.item.context.unwrap();
88        assert_eq!(context.task_id, current.task_id);
89        assert_eq!(context.actor_id, "child");
90        assert_eq!(context.parent_actor_id.as_deref(), Some("parent"));
91        let ThreadItemDetails::Harness(status) = completed.item.details else {
92            panic!("harness status expected")
93        };
94        assert_eq!(status.status.as_deref(), Some("running"));
95        assert!(tracker.delegation_status_event("call", "exec_command", &output).is_none());
96        assert!(
97            tracker
98                .delegation_status_event("call", "agent", &json!({"session_id":"child"}))
99                .is_none()
100        );
101        let mut unrelated = output.clone();
102        unrelated["parent_thread_id"] = json!("unrelated");
103        assert!(tracker.delegation_status_event("call", "agent", &unrelated).is_none());
104        assert!(
105            tracker
106                .delegation_status_event("wait", "agent", &json!({"completed":true,"entry":output}))
107                .is_some()
108        );
109    }
110
111    #[test]
112    fn exec_session_mapping_is_bounded_and_isolates_actors_and_unrelated_sessions() {
113        let mut tracker = ExecutionContextTracker::default();
114        let original = tracker.begin("root", "turn-1", "Verify", InputOrigin::User);
115        for index in 0..=MAX_TRACKED_EXEC_LAUNCHES {
116            track_launch(&mut tracker, &format!("launch-{index}"), &format!("session-{index}"));
117        }
118        assert_eq!(tracker.exec_launches.len(), MAX_TRACKED_EXEC_LAUNCHES);
119        assert!(
120            tracker
121                .exec_session_output_event(
122                    "poll",
123                    "write_stdin",
124                    &json!({"session_id":"session-0","chars":""}),
125                    &json!({"exit_code":0})
126                )
127                .is_none()
128        );
129        assert!(
130            tracker
131                .exec_session_output_event(
132                    "poll",
133                    "write_stdin",
134                    &json!({"session_id":"unknown","chars":""}),
135                    &json!({"exit_code":0})
136                )
137                .is_none()
138        );
139        tracker.begin("other-actor", "turn-2", "Unrelated actor", InputOrigin::User);
140        assert!(
141            tracker
142                .exec_session_output_event(
143                    "poll",
144                    "write_stdin",
145                    &json!({"session_id":"session-1","chars":""}),
146                    &json!({"exit_code":0})
147                )
148                .is_none()
149        );
150        tracker.begin("root", "turn-3", "Another task", InputOrigin::User);
151        let completed = tracker
152            .exec_session_output_event(
153                "poll",
154                "write_stdin",
155                &json!({"session_id":"session-1","chars":""}),
156                &json!({"exit_code":0}),
157            )
158            .unwrap();
159        let ThreadEvent::ItemCompleted(completed) = completed else {
160            panic!("completion expected")
161        };
162        let context = completed.item.context.unwrap();
163        assert_eq!(context.task_id, original.task_id);
164        assert_eq!(context.turn_id, "turn-1");
165        assert_eq!(context.actor_id, "root");
166        assert_eq!(tracker.exec_launches.len(), MAX_TRACKED_EXEC_LAUNCHES - 1);
167    }
168
169    #[test]
170    fn raw_background_exit_correlates_to_the_original_verifier_only() {
171        let mut tracker = ExecutionContextTracker::default();
172        let original = tracker.begin("root", "turn", "Verify", InputOrigin::User);
173        track_launch(&mut tracker, "original-call", "session");
174        assert!(
175            tracker
176                .background_exec_output_event(&mut background("other", "exec:other", Some(0)))
177                .is_none()
178        );
179        assert!(
180            tracker
181                .background_exec_output_event(&mut background("session", "managed-task", Some(0)))
182                .is_none()
183        );
184        let pending = tracker
185            .background_exec_output_event(&mut background("session", "exec:session", None))
186            .unwrap();
187        let ThreadEvent::ItemCompleted(pending) = pending else {
188            panic!("pending output expected")
189        };
190        let ThreadItemDetails::ToolOutput(output) = pending.item.details else {
191            panic!("tool output expected")
192        };
193        assert_eq!(output.status, crate::exec::events::ToolCallStatus::InProgress);
194        tracker.begin("root", "new-turn", "Another task", InputOrigin::User);
195        let mut raw = background("session", "exec:session", Some(7));
196        tracker.annotate(&mut raw);
197        let terminal = tracker.background_exec_output_event(&mut raw).unwrap();
198        tracker.annotate(&mut raw);
199        let ThreadEvent::ItemCompleted(raw) = raw else {
200            panic!("raw completion expected")
201        };
202        assert_eq!(raw.item.context.unwrap().task_id, original.task_id);
203        assert_eq!(tracker.current.as_ref().unwrap().turn_id, "new-turn");
204        let ThreadEvent::ItemCompleted(terminal) = terminal else {
205            panic!("terminal output expected")
206        };
207        assert_eq!(terminal.item.context.unwrap().task_id, original.task_id);
208        let ThreadItemDetails::ToolOutput(output) = terminal.item.details else {
209            panic!("tool output expected")
210        };
211        assert_eq!(output.call_id, "original-call");
212        assert_eq!(output.status, crate::exec::events::ToolCallStatus::Failed);
213        assert_eq!(output.exit_code, Some(7));
214        assert!(
215            tracker
216                .background_exec_output_event(&mut background("session", "exec:session", Some(0)))
217                .is_none()
218        );
219    }
220
221    #[test]
222    fn execution_task_identity_survives_corrections_and_handoffs() {
223        let mut tracker = ExecutionContextTracker::default();
224        let first = tracker.begin("root", "turn-1", "Fix login", InputOrigin::User);
225        for (turn, origin) in [
226            ("turn-2", InputOrigin::Correction),
227            ("turn-3", InputOrigin::PlanApproval),
228            ("turn-4", InputOrigin::Continuation),
229            ("turn-5", InputOrigin::Retry),
230        ] {
231            let current = tracker.begin("root", turn, "changed input", origin);
232            assert_eq!(current.task_id, first.task_id);
233            assert_eq!(current.goal.as_deref(), Some("Fix login"));
234            assert_eq!(current.turn_id, turn);
235        }
236        let next = tracker.begin("root", "turn-6", "Add logging", InputOrigin::User);
237        assert_ne!(next.task_id, first.task_id);
238        assert_eq!(next.goal.as_deref(), Some("Add logging"));
239    }
240
241    #[test]
242    fn public_decision_bounds_are_validated_in_characters_and_fail_closed() {
243        assert!(validate_decision_input(json!({"summary":"é".repeat(240),"rationale":"r".repeat(1000),"alternatives":["a","b","c"],"evidence_ids":vec!["id";8]})).is_ok());
244        for invalid in [
245            json!({"summary":"a".repeat(241),"rationale":"why"}),
246            json!({"summary":"choice","rationale":"a".repeat(1001)}),
247            json!({"summary":"choice","rationale":"why","alternatives":["a","b","c","d"]}),
248            json!({"summary":"choice","rationale":"why","evidence_ids":[""]}),
249            json!({"summary":"choice","rationale":"why","private_reasoning":"hidden"}),
250            json!({"summary":" ","rationale":"why"}),
251        ] {
252            assert!(validate_decision_input(invalid).is_err());
253        }
254    }
255
256    #[test]
257    fn public_decision_accepts_valid_common_output_metadata() {
258        let args = json!({"summary":"Reuse parser","rationale":"Preserve validation","max_output_tokens":100});
259        let decision = validate_decision_input(args.clone()).unwrap();
260        assert_eq!(decision.summary, "Reuse parser");
261        let event = super::super::tool_invocation_completed_event(
262            "decision-call".into(),
263            "record_decision",
264            Some(&args),
265            None,
266            crate::exec::events::ToolCallStatus::Completed,
267            crate::exec::events::ToolOutcome::Success,
268        );
269        assert!(decision_completed_event(&event).is_some());
270        for invalid in [json!(0), json!(50_001), json!("100"), json!(1.5)] {
271            let mut args = args.clone();
272            args["max_output_tokens"] = invalid;
273            assert!(validate_decision_input(args).is_err());
274        }
275    }
276}
277
278/// Convert a successful decision call into a canonical public decision item.
279pub fn decision_completed_event(event: &ThreadEvent) -> Option<ThreadEvent> {
280    let ThreadEvent::ItemCompleted(e) = event else {
281        return None;
282    };
283    let ThreadItemDetails::ToolInvocation(t) = &e.item.details else {
284        return None;
285    };
286    if t.tool_name != crate::config::constants::tools::RECORD_DECISION
287        || t.status != crate::exec::events::ToolCallStatus::Completed
288        || t.outcome != Some(crate::exec::events::ToolOutcome::Success)
289    {
290        return None;
291    }
292    let decision = validate_decision_input(t.arguments.clone()?).ok()?;
293    Some(ThreadEvent::ItemCompleted(crate::exec::events::ItemCompletedEvent {
294        item: crate::exec::events::ThreadItem {
295            id: format!("{}:decision", e.item.id),
296            context: e.item.context.clone(),
297            details: ThreadItemDetails::Decision(Box::new(decision)),
298        },
299    }))
300}
301
302#[derive(Default)]
303pub struct ExecutionContextTracker {
304    current: Option<ExecutionContext>,
305    exec_launches: VecDeque<ExecSessionLaunch>,
306}
307
308impl ExecutionContextTracker {
309    /// Preserve native delegation metadata before tool-output compaction.
310    pub fn delegation_status_event(
311        &self,
312        call_item_id: &str,
313        tool_name: &str,
314        output: &serde_json::Value,
315    ) -> Option<ThreadEvent> {
316        use crate::config::constants::tools;
317        use crate::exec::events::{HarnessEventItem, HarnessEventKind, ItemCompletedEvent, ThreadItem};
318        if !matches!(
319            tool_name,
320            tools::AGENT
321                | tools::SPAWN_AGENT
322                | tools::WAIT_AGENT
323                | tools::RESUME_AGENT
324                | tools::CLOSE_AGENT
325                | tools::SEND_INPUT
326        ) {
327            return None;
328        }
329        let value = output.get("entry").unwrap_or(output);
330        let entry: crate::subagents::SubagentStatusEntry = serde_json::from_value(value.clone()).ok()?;
331        let current = self.current.as_ref()?;
332        if entry.session_id.is_empty()
333            || entry.session_id.len() > MAX_EXEC_ID_BYTES
334            || entry.parent_thread_id != current.actor_id
335        {
336            return None;
337        }
338        let item: HarnessEventItem = serde_json::from_value(serde_json::json!({
339            "event": HarnessEventKind::DelegatedAgentStatus,
340            "message": format!("Delegated {}: {}", entry.display_label, entry.description),
341            "task_id": entry.id,
342            "session_id": entry.session_id,
343            "status": entry.status.as_str(),
344        }))
345        .ok()?;
346        Some(ThreadEvent::ItemCompleted(ItemCompletedEvent {
347            item: ThreadItem {
348                id: format!("{call_item_id}:delegated-agent"),
349                context: Some(Box::new(ItemContext {
350                    task_id: current.task_id.clone(),
351                    turn_id: current.turn_id.clone(),
352                    actor_id: entry.session_id,
353                    parent_actor_id: Some(entry.parent_thread_id),
354                    timestamp: entry.updated_at.to_rfc3339(),
355                    activity: None,
356                })),
357                details: ThreadItemDetails::Harness(Box::new(item)),
358            },
359        }))
360    }
361
362    /// A new idle user request starts a task; other origins retain its root goal.
363    pub fn begin(&mut self, actor: &str, turn: &str, input: &str, origin: InputOrigin) -> ExecutionContext {
364        let previous = self.current.as_ref().filter(|_| origin != InputOrigin::User);
365        let context = ExecutionContext {
366            task_id: previous.map_or_else(|| format!("task-{}", uuid::Uuid::new_v4()), |c| c.task_id.clone()),
367            turn_id: turn.to_owned(),
368            actor_id: actor.to_owned(),
369            parent_actor_id: None,
370            origin,
371            timestamp: chrono::Utc::now().to_rfc3339(),
372            goal: previous.and_then(|c| c.goal.clone()).or_else(|| Some(input.to_owned())),
373        };
374        self.current = Some(context.clone());
375        context
376    }
377
378    /// Annotate before persistence and exporter fanout so all consumers agree.
379    pub fn annotate(&mut self, event: &mut ThreadEvent) {
380        let now = chrono::Utc::now().to_rfc3339();
381        match event {
382            ThreadEvent::TurnStarted(e) => {
383                if let Some(context) = e.context.as_deref() {
384                    self.current = Some(context.clone());
385                } else {
386                    e.context = self.current.clone().map(Box::new);
387                }
388            }
389            ThreadEvent::TurnCompleted(e) => {
390                e.completed_at.get_or_insert_with(|| Box::new(now));
391            }
392            ThreadEvent::TurnFailed(e) => {
393                e.completed_at.get_or_insert_with(|| Box::new(now));
394            }
395            ThreadEvent::ThreadCompleted(e) => {
396                e.completed_at.get_or_insert(now);
397            }
398            ThreadEvent::TurnBlocked(e) => {
399                e.completed_at.get_or_insert(now);
400            }
401            ThreadEvent::ItemStarted(e) => self.annotate_item(&mut e.item, now),
402            ThreadEvent::ItemUpdated(e) => self.annotate_item(&mut e.item, now),
403            ThreadEvent::ItemCompleted(e) => self.annotate_item(&mut e.item, now),
404            _ => {}
405        }
406    }
407
408    fn annotate_item(&mut self, item: &mut crate::exec::events::ThreadItem, timestamp: String) {
409        let Some(c) = &self.current else {
410            return;
411        };
412        let activity = match &item.details {
413            ThreadItemDetails::ToolInvocation(t) => t
414                .arguments
415                .as_ref()
416                .filter(|_| crate::tools::tool_intent::is_command_session_tool(&t.tool_name))
417                .map(|args| classify_shell_activity(&t.tool_name, args)),
418            ThreadItemDetails::CommandExecution(t) => {
419                Some(classify_shell_activity("exec_command", &serde_json::json!({"cmd": t.command})))
420            }
421            _ => None,
422        }
423        .map(|a| match a {
424            ShellActivity::Inspection => CommandActivity::Inspection,
425            ShellActivity::Verification => CommandActivity::Verification,
426            ShellActivity::Mutation => CommandActivity::Mutation,
427        });
428        item.context.get_or_insert_with(|| {
429            Box::new(ItemContext {
430                task_id: c.task_id.clone(),
431                turn_id: c.turn_id.clone(),
432                actor_id: c.actor_id.clone(),
433                parent_actor_id: c.parent_actor_id.clone(),
434                timestamp,
435                activity,
436            })
437        });
438        let ThreadItemDetails::ToolInvocation(invocation) = &item.details else {
439            return;
440        };
441        let Some(args) = invocation.arguments.as_ref() else {
442            return;
443        };
444        let Some(context) = item.context.as_ref() else { return };
445        if invocation.status == crate::exec::events::ToolCallStatus::Failed {
446            self.exec_launches.retain(|launch| {
447                launch.call_item_id != item.id
448                    || launch.context.task_id != context.task_id
449                    || launch.context.actor_id != context.actor_id
450            });
451            return;
452        }
453        if context.activity != Some(CommandActivity::Verification)
454            || !crate::tools::tool_intent::is_command_run_tool_call(&invocation.tool_name, args)
455            || item.id.len() > MAX_EXEC_ID_BYTES
456            || invocation.tool_call_id.as_ref().is_some_and(|id| id.len() > MAX_EXEC_ID_BYTES)
457            || [
458                &context.task_id,
459                &context.turn_id,
460                &context.actor_id,
461                &context.timestamp,
462            ]
463            .into_iter()
464            .any(|id| id.len() > MAX_EXEC_ID_BYTES)
465            || context.parent_actor_id.as_ref().is_some_and(|id| id.len() > MAX_EXEC_ID_BYTES)
466            || self.exec_launches.iter().any(|launch| {
467                launch.call_item_id == item.id
468                    && launch.context.task_id == context.task_id
469                    && launch.context.actor_id == context.actor_id
470            })
471        {
472            return;
473        }
474        if self.exec_launches.len() == MAX_TRACKED_EXEC_LAUNCHES {
475            self.exec_launches.pop_front();
476        }
477        self.exec_launches.push_back(ExecSessionLaunch {
478            session_id: None,
479            call_item_id: item.id.clone(),
480            tool_call_id: invocation.tool_call_id.clone(),
481            context: context.clone(),
482        });
483    }
484
485    /// Link authoritative exec results to their original verifier invocation.
486    /// Session identities, never repeated command text, own this association.
487    pub fn exec_session_output_event(
488        &mut self,
489        call_item_id: &str,
490        tool_name: &str,
491        args: &serde_json::Value,
492        output: &serde_json::Value,
493    ) -> Option<ThreadEvent> {
494        crate::tools::tool_intent::canonical_command_session_tool_name(tool_name)?;
495        let current = self.current.as_ref()?;
496        let is_launch = crate::tools::tool_intent::is_command_run_tool_call(tool_name, args);
497        let response_session = crate::tools::command_args::session_id_text(output);
498        let requested_session = crate::tools::command_args::session_id_text(args);
499        if response_session.is_some_and(|id| id.len() > MAX_EXEC_ID_BYTES)
500            || (!is_launch && response_session.is_some() && response_session != requested_session)
501        {
502            return None;
503        }
504        let position = self.exec_launches.iter().position(|launch| {
505            launch.context.actor_id == current.actor_id
506                && if is_launch {
507                    launch.context.task_id == current.task_id && launch.call_item_id == call_item_id
508                } else {
509                    requested_session.is_some() && launch.session_id.as_deref() == requested_session
510                }
511        })?;
512        let exit_code = output
513            .get("exit_code")
514            .and_then(serde_json::Value::as_i64)
515            .and_then(|code| i32::try_from(code).ok());
516        if exit_code == Some(0)
517            && (output.get("success").and_then(serde_json::Value::as_bool) == Some(false)
518                || output.get("error").is_some_and(|error| !error.is_null()))
519        {
520            return None;
521        }
522        if is_launch {
523            if exit_code.is_some() || response_session.is_none() {
524                self.exec_launches.remove(position);
525                return None;
526            }
527            self.exec_launches.get_mut(position)?.session_id = response_session.map(str::to_owned);
528        }
529        let payload = super::tool_output_payload_from_value(output);
530        self.exec_session_completion_event(
531            position,
532            exit_code,
533            payload.spool_path.as_deref(),
534            payload.aggregated_output,
535        )
536    }
537
538    /// Resolve raw subprocess completion facts through the same launch mapping.
539    pub fn background_exec_output_event(&mut self, event: &mut ThreadEvent) -> Option<ThreadEvent> {
540        let ThreadEvent::ItemCompleted(completed) = event else {
541            return None;
542        };
543        let ThreadItemDetails::Harness(harness) = &completed.item.details else {
544            return None;
545        };
546        if harness.event != crate::exec::events::HarnessEventKind::BackgroundSubprocessCompleted {
547            return None;
548        }
549        let session_id = harness.exec_session_id.as_deref()?;
550        // Raw exec completions carry this exact runtime-owned identity. Managed
551        // subagents use their own task/session identities and cannot alias it.
552        if harness.task_id.as_deref() != Some(format!("exec:{session_id}").as_str())
553            || harness.session_id.as_deref() != Some(session_id)
554        {
555            return None;
556        }
557        let current = self.current.as_ref()?;
558        let position = self.exec_launches.iter().position(|launch| {
559            launch.context.actor_id == current.actor_id && launch.session_id.as_deref() == Some(session_id)
560        })?;
561        let output = self.exec_session_completion_event(
562            position,
563            harness.exit_code,
564            None,
565            harness.message.clone().unwrap_or_default(),
566        )?;
567        if let ThreadEvent::ItemCompleted(output) = &output {
568            completed.item.context = output.item.context.clone();
569        }
570        Some(output)
571    }
572
573    fn exec_session_completion_event(
574        &mut self,
575        position: usize,
576        exit_code: Option<i32>,
577        spool_path: Option<&str>,
578        output: String,
579    ) -> Option<ThreadEvent> {
580        let launch = self.exec_launches.get(position)?;
581        let status = match exit_code {
582            Some(0) => crate::exec::events::ToolCallStatus::Completed,
583            Some(_) => crate::exec::events::ToolCallStatus::Failed,
584            None => crate::exec::events::ToolCallStatus::InProgress,
585        };
586        let mut event = super::tool_output_completed_event(
587            launch.call_item_id.clone(),
588            launch.tool_call_id.as_deref(),
589            status,
590            exit_code,
591            spool_path,
592            output,
593        );
594        if let ThreadEvent::ItemCompleted(completed) = &mut event {
595            completed.item.id = format!("{}:exec-session-output", launch.call_item_id);
596            let mut context = launch.context.clone();
597            context.timestamp = chrono::Utc::now().to_rfc3339();
598            completed.item.context = Some(context);
599        }
600        if exit_code.is_some() {
601            self.exec_launches.remove(position);
602        }
603        Some(event)
604    }
605}