Repository navigation
fix: wave 4 follow-ups (generation spend, clarification options, wait_loop args, residual order) #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: wave 4 follow-ups (generation spend, clarification options, wait_loop args, residual order) #254
Changes from all commits
55e768b
cad2df4
fc39881
2204ead
cb1697d
7038387
ad60997
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -391,7 +391,7 @@ impl<T: QueuedMessage> Drop for SteeringForwarderGuard<T> { | |
| .map(|(text, _lane, id)| (Some(id.clone()), Some(text.clone()))) | ||
| .unwrap_or((None, None)); | ||
|
|
||
| // `RunQueue` is async (tokio `Mutex`); `Drop` is synchronous. Put | ||
| // `RunQueue::push` is async (tokio `Mutex`); `Drop` is synchronous. Push | ||
| // the recovered steers back on a detached task so they land in the | ||
| // session queue and become the next turn's input. The forwarder poll | ||
| // loop keeps re-draining the queue, so even if the requeue completes | ||
|
|
@@ -400,19 +400,14 @@ impl<T: QueuedMessage> Drop for SteeringForwarderGuard<T> { | |
| Ok(rt) => { | ||
| let label = thread_label.clone(); | ||
| rt.spawn(async move { | ||
| // These were delivered before anything still queued, so | ||
| // they go back at the front of their lane, in order. | ||
| let mut steers = Vec::new(); | ||
| let mut collects = Vec::new(); | ||
| // Appended, not prepended: a later guard's residuals must | ||
| // not jump ahead of these, and successive drops each add | ||
| // theirs behind the last. | ||
| for (text, lane, id) in requeue_items { | ||
| let item = T::requeued(id, text, &label, now_ms()); | ||
| match lane { | ||
| QueueLane::Collect => collects.push(item), | ||
| _ => steers.push(item), | ||
| } | ||
| queue | ||
| .push(lane, T::requeued(id, text, &label, now_ms())) | ||
| .await; | ||
|
Comment on lines
+407
to
+409
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a steer arrives after the forwarder delivered an earlier message but before the guard is dropped, that newer steer remains in the queue while the earlier one is recovered from the handle. Appending the residual with Useful? React with 👍 / 👎. |
||
| } | ||
| queue.requeue_front(QueueLane::Steer, steers).await; | ||
| queue.requeue_front(QueueLane::Collect, collects).await; | ||
| }); | ||
| } | ||
| Err(_) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,7 +111,16 @@ fn spend_from_usages<'a>( | |
| /// `request_id` is the compaction's and no appended line already counted | ||
| /// spend under that `request_id`. A compaction written without a | ||
| /// `request_id` falls back to the usage on its final assistant row. | ||
| fn file_spend(path: &Path) -> Option<(TranscriptSpend, TranscriptMeta)> { | ||
| /// | ||
| /// `earlier_requests` holds the `request_id`s that earlier generation files of | ||
| /// the same conversation already counted. A successor generation starts by | ||
| /// writing the rows it retained from its parent, and those rows keep their | ||
| /// original `request_id`, so a row whose `request_id` is in that set is | ||
| /// carried history, not new spend. The ids this file counts are added to it. | ||
| fn file_spend( | ||
| path: &Path, | ||
| earlier_requests: &mut HashSet<String>, | ||
| ) -> Option<(TranscriptSpend, TranscriptMeta)> { | ||
| let display = match read_transcript_display(path) { | ||
| Ok(display) => display, | ||
| Err(err) => { | ||
|
|
@@ -130,7 +139,11 @@ fn file_spend(path: &Path) -> Option<(TranscriptSpend, TranscriptMeta)> { | |
| for record in &display.records { | ||
| match record { | ||
| DisplayRecord::Message(message) if !message.interrupted => { | ||
| if let Some(usage) = message.message.turn_usage.as_ref() { | ||
| let carried = message | ||
| .request_id | ||
| .as_deref() | ||
| .is_some_and(|request_id| earlier_requests.contains(request_id)); | ||
| if !carried && let Some(usage) = message.message.turn_usage.as_ref() { | ||
| usages.push(usage); | ||
| if let Some(request_id) = message.request_id.as_deref() { | ||
| counted_requests.insert(request_id); | ||
|
|
@@ -141,7 +154,9 @@ fn file_spend(path: &Path) -> Option<(TranscriptSpend, TranscriptMeta)> { | |
| DisplayRecord::Message(_) => {} | ||
| DisplayRecord::Compaction(marker) => { | ||
| match marker.request_id.as_deref() { | ||
| Some(turn) if !counted_requests.contains(turn) => { | ||
| Some(turn) | ||
| if !counted_requests.contains(turn) && !earlier_requests.contains(turn) => | ||
| { | ||
| let fresh: Vec<&TurnUsage> = marker | ||
| .replacement | ||
| .iter() | ||
|
|
@@ -180,6 +195,7 @@ fn file_spend(path: &Path) -> Option<(TranscriptSpend, TranscriptMeta)> { | |
| } | ||
| } | ||
| let spend = spend_from_usages(usages, display.meta.model.as_ref()); | ||
| earlier_requests.extend(counted_requests.into_iter().map(str::to_owned)); | ||
| Some((spend, display.meta)) | ||
| } | ||
|
|
||
|
|
@@ -246,9 +262,12 @@ pub struct ThreadSpend { | |
| pub fn thread_spend(workspace_dir: &Path, thread_id: &str) -> ThreadSpend { | ||
| let mut out = ThreadSpend::default(); | ||
| let roots = find_root_transcripts_for_thread(workspace_dir, thread_id); | ||
| // Shared across the thread's root files (oldest first), so rows a later | ||
| // generation carried over from an earlier one are not counted twice. | ||
| let mut root_requests = HashSet::new(); | ||
| for root in &roots { | ||
| out.found_transcript = true; | ||
| if let Some((spend, meta)) = file_spend(root) { | ||
| if let Some((spend, meta)) = file_spend(root, &mut root_requests) { | ||
|
Comment on lines
+267
to
+270
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a thread has multiple independent root transcripts—such as legacy cold-boot roots or roots scoped to different agents—and two roots reuse the same opaque Useful? React with 👍 / 👎. |
||
| out.root.input_tokens = out.root.input_tokens.saturating_add(spend.input_tokens); | ||
| out.root.output_tokens = out.root.output_tokens.saturating_add(spend.output_tokens); | ||
| out.root.cached_input_tokens = out | ||
|
|
@@ -272,7 +291,7 @@ pub fn thread_spend(workspace_dir: &Path, thread_id: &str) -> ThreadSpend { | |
| out.updated = Some(meta.updated); | ||
| } | ||
| for child in descendant_transcripts(root) { | ||
| let Some((spend, child_meta)) = file_spend(&child) else { | ||
| let Some((spend, child_meta)) = file_spend(&child, &mut HashSet::new()) else { | ||
| continue; | ||
| }; | ||
| out.found_transcript = true; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test for the requeue ordering change
The forwarder now appends requeued items via
pushinstead of prepending them in batches viarequeue_front. This is a deliberate behavioural change (the comment says "Appended, not prepended"), but no test asserts the new ordering or validates that the change does not regress. The repository rules require tests with every behaviour change. Without a test, a future refactor could silently revert to prepending.[RULE] missing-test ·