Skip to content

Commit 8e23bef

Browse files
v0.2.0 (#10)
1 parent 8610e29 commit 8e23bef

4 files changed

Lines changed: 40 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "worko-ai-usage"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
edition = "2021"
55
rust-version = "1.86"
66
license = "MIT"

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ worko-ai-usage logout
6565
then exchanges a single-use token for a scoped Worko HR access token. The
6666
single-use token and your account password are never stored.
6767
- `status` prints locally detected counters and does not contact Worko.
68-
- `sync` uploads at most the latest 48 hourly snapshots.
68+
- `sync` uploads session-level snapshots from the latest 48 hours (up to 500 rows).
6969
- `logout` deletes the local Worko token and does not affect Claude or Codex login.
7070

7171
## Data collected
@@ -74,11 +74,18 @@ worko-ai-usage logout
7474
|---|---|
7575
| Provider (`claude`/`codex`) | Separate agent reporting |
7676
| Anonymous machine hash | Avoid duplicate hourly snapshots |
77+
| Anonymous session hash | Keep concurrent agent sessions separate without uploading paths |
7778
| UTC hour | Hourly reporting |
7879
| Input/cached/output token counts | Usage KPI |
7980
| Usage event count | Activity indicator |
8081
| Provider-reported 5-hour percentage | Included only when present in local provider logs |
8182

83+
Five-hour utilization is never inferred from token counts. It remains unknown when
84+
the provider log does not expose a limit signal. Session separation improves
85+
attribution, but a provider may report an account-wide window value rather than a
86+
session-specific quota; the HR dashboard shows signal coverage so this limitation
87+
stays visible.
88+
8289
## Development and releases
8390

8491
```bash

src/main.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ struct Config {
5454
struct Snapshot {
5555
provider: String,
5656
machine_id: String,
57+
session_hash: String,
5758
observed_at: String,
5859
input_tokens: u64,
5960
cached_input_tokens: u64,
6061
output_tokens: u64,
6162
sessions: u32,
6263
five_hour_percent: Option<f64>,
64+
limit_source: Option<String>,
6365
collector_version: String,
6466
ide: String,
6567
}
@@ -190,14 +192,24 @@ fn sync() -> Result<()> {
190192
println!("No Claude Code or Codex usage events found.");
191193
return Ok(());
192194
}
193-
let start = snapshots.len().saturating_sub(48);
195+
let recent_cutoff = Utc::now() - chrono::Duration::hours(48);
196+
let recent: Vec<&Snapshot> = snapshots
197+
.iter()
198+
.filter(|snapshot| {
199+
DateTime::parse_from_rfc3339(&snapshot.observed_at)
200+
.map(|time| time >= recent_cutoff)
201+
.unwrap_or(false)
202+
})
203+
.rev()
204+
.take(500)
205+
.collect();
194206
let response = client()
195207
.post(format!(
196208
"{}/api/v1/ai-agent-usage/snapshots",
197209
config.base_url
198210
))
199211
.bearer_auth(config.token)
200-
.json(&json!({"snapshots": &snapshots[start..]}))
212+
.json(&json!({"snapshots": recent.into_iter().rev().collect::<Vec<_>>()}))
201213
.send()?;
202214
if !response.status().is_success() {
203215
bail!("sync failed ({})", response.status());
@@ -220,13 +232,13 @@ fn collect() -> Result<Vec<Snapshot>> {
220232
];
221233
let machine_id = machine_id(&home);
222234
let cutoff = SystemTime::now() - Duration::from_secs(7 * 86_400);
223-
let mut buckets: BTreeMap<(String, String), Snapshot> = BTreeMap::new();
235+
let mut buckets: BTreeMap<(String, String, String), Snapshot> = BTreeMap::new();
224236

225237
for (provider, root) in sources {
226238
if !root.is_dir() {
227239
continue;
228240
}
229-
for entry in WalkDir::new(root)
241+
for entry in WalkDir::new(&root)
230242
.into_iter()
231243
.filter_map(|entry| entry.ok())
232244
{
@@ -240,6 +252,7 @@ fn collect() -> Result<Vec<Snapshot>> {
240252
continue;
241253
}
242254
let fallback = DateTime::<Utc>::from(fs::metadata(path)?.modified()?);
255+
let session_hash = session_hash(&machine_id, provider, path, &root);
243256
for line in io::BufReader::new(fs::File::open(path)?)
244257
.lines()
245258
.map_while(|line| line.ok())
@@ -262,16 +275,18 @@ fn collect() -> Result<Vec<Snapshot>> {
262275
.with_nanosecond(0)
263276
.unwrap()
264277
.to_rfc3339();
265-
let key = (provider.to_owned(), hour.clone());
278+
let key = (provider.to_owned(), session_hash.clone(), hour.clone());
266279
let bucket = buckets.entry(key).or_insert_with(|| Snapshot {
267280
provider: provider.to_owned(),
268281
machine_id: machine_id.clone(),
282+
session_hash: session_hash.clone(),
269283
observed_at: hour,
270284
input_tokens: 0,
271285
cached_input_tokens: 0,
272286
output_tokens: 0,
273287
sessions: 0,
274288
five_hour_percent: None,
289+
limit_source: None,
275290
collector_version: VERSION.to_owned(),
276291
ide: std::env::var("WORKO_IDE").unwrap_or_else(|_| "terminal".to_owned()),
277292
});
@@ -282,13 +297,23 @@ fn collect() -> Result<Vec<Snapshot>> {
282297
bucket.sessions += 1;
283298
if let Some(percent) = find_percent(&event) {
284299
bucket.five_hour_percent = Some(percent.clamp(0.0, 100.0));
300+
bucket.limit_source = Some("provider_log".to_owned());
285301
}
286302
}
287303
}
288304
}
289305
Ok(buckets.into_values().collect())
290306
}
291307

308+
fn session_hash(machine_id: &str, provider: &str, path: &Path, root: &Path) -> String {
309+
let relative = path.strip_prefix(root).unwrap_or(path);
310+
let mut hash = Sha256::new();
311+
hash.update(machine_id.as_bytes());
312+
hash.update(provider.as_bytes());
313+
hash.update(relative.to_string_lossy().as_bytes());
314+
hex::encode(hash.finalize())
315+
}
316+
292317
fn find_usage(value: &Value) -> Option<&Value> {
293318
if let Value::Object(map) = value {
294319
for key in ["usage", "token_usage", "last_token_usage"] {

0 commit comments

Comments
 (0)