Recover a session's context from the CLI's own file
A restarted server has been told nothing, so a Claude session that has not taken a turn since reported its context as unknown -- which was true, and useless, since the CLI had written the figure down at the time and it was sitting in the session's file the whole while. It now reads it from there at load, over the session's transport, in the background: the same three input fields the import list already reads, so it is a measurement rather than a guess. Only when nothing else has answered, and only for a provider that keeps such a file. A clear needs no special case even though it makes the last usage in a file stale, because clearing gives the CLI a new session id -- so the lookup lands on a file with no usage yet and answers unknown, which is what it is.
This commit is contained in:
1 parent
bc0a48799c
commit
0d623b7073
3 files changed
+76
No files matched your search
@@ -193,6 +193,12 @@ it touches the transcript or the phone:
|
||||
and the phone folds with the same one (2026-08-30: this replaced a running
|
||||
spend total, which could only climb and so kept reporting a context a
|
||||
compaction or a clear had already taken away).
|
||||
A session the server has no measurement of asks the CLI's own file
|
||||
instead of waiting for a turn — `import::context_of`, the same three
|
||||
fields the import list reads, in the background at load so a start never
|
||||
waits on an ssh. A clear needs no special case: it gives the CLI a new
|
||||
session id, so the lookup lands on a file with no usage in it and
|
||||
answers "unknown", which is true.
|
||||
- `Error { message }`.
|
||||
|
||||
Every event is appended to the session's transcript file with a sequence
|
||||
|
||||
@@ -688,6 +688,50 @@ pub async fn line_count(transport: &Transport, path: &str) -> Result<usize> {
|
||||
.with_context(|| format!("couldn't read a line count out of {out:?}"))
|
||||
}
|
||||
|
||||
/// What the CLI's own file says a session is holding, for a session this
|
||||
/// server has no measurement of.
|
||||
///
|
||||
/// A restarting server has been told nothing, and a session that has not
|
||||
/// taken a turn since will not tell it -- so a conversation that is nearly
|
||||
/// full reads as one nobody has counted until somebody sends a message to
|
||||
/// it. The CLI records the figure on every assistant message, so it is
|
||||
/// there to be read rather than waited for, and reading it is a
|
||||
/// measurement rather than a guess: the same three fields, from the same
|
||||
/// file, that the import list reports.
|
||||
///
|
||||
/// A clear needs no special case here even though it makes the last usage
|
||||
/// in a file stale. Clearing gives the CLI a *new* session id, which the
|
||||
/// reader persists as the resume token, so this looks in a file that has
|
||||
/// no usage in it yet and answers `None` -- which is the true answer.
|
||||
///
|
||||
/// `None` for every way it cannot be read: no resume token, no file, a
|
||||
/// machine that cannot be reached, or a file with no assistant turn in it.
|
||||
/// Not knowing is a state the status row draws, so there is nothing to be
|
||||
/// gained by inventing a number here.
|
||||
pub async fn context_of(transport: &Transport, session_id: &str) -> Option<u64> {
|
||||
// The id crosses as an argument rather than as script text: it comes
|
||||
// from the CLI, but it reaches a shell on a machine that may not be
|
||||
// this one, and the rule there is that data never becomes syntax.
|
||||
let script = r#"
|
||||
for f in "$HOME"/.claude/projects/*/"$1".jsonl; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -o '"usage":{[^}]*' "$f" | tail -1
|
||||
exit 0
|
||||
done
|
||||
"#;
|
||||
let launch = Launch::new(
|
||||
"sh",
|
||||
vec![
|
||||
"-c".to_string(),
|
||||
script.to_string(),
|
||||
"sh".to_string(),
|
||||
session_id.to_string(),
|
||||
],
|
||||
None,
|
||||
);
|
||||
context_tokens(&transport.capture(&launch).await.ok()?)
|
||||
}
|
||||
|
||||
/// Events from the lines after `after`, which is a 0-based count of lines
|
||||
/// already accounted for.
|
||||
pub async fn replay_after(
|
||||
|
||||
@@ -1299,6 +1299,32 @@ fn launch(
|
||||
written: Mutex::new(0),
|
||||
});
|
||||
|
||||
// Nothing here has measured this session's context: the transcript
|
||||
// predates the figure being recorded, or the last turn happened before
|
||||
// this server was watching. The CLI wrote it down at the time, so ask
|
||||
// its file rather than leaving the row saying "unknown" until somebody
|
||||
// sends a message. In the background, because it is a file read on a
|
||||
// machine that may be at the other end of an ssh connection, and a
|
||||
// server start must not wait on one.
|
||||
if provider.kind == DriverKind::ClaudeCli
|
||||
&& shared.context_tokens.lock().unwrap().is_none()
|
||||
&& let Some(session_id) = claude::read_resume_token(&dir)
|
||||
{
|
||||
let transport = Transport::for_setup(setup);
|
||||
let shared = Arc::clone(&shared);
|
||||
tokio::spawn(async move {
|
||||
if let Some(context) = import::context_of(&transport, &session_id).await {
|
||||
// Only if nothing else has answered in the meantime: a turn
|
||||
// that finished while this was in flight measured the
|
||||
// context after the one this read.
|
||||
let mut held = shared.context_tokens.lock().unwrap();
|
||||
if held.is_none() {
|
||||
*held = Some(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// An imported session shares its transcript file with the CLI --
|
||||
// `--resume` appends to the same one rather than forking, measured
|
||||
// rather than assumed -- so work done at a terminal belongs in this
|
||||
|
||||
Reference in new issue
Block a user