Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Tokscale is a Rust workspace with Bun-managed JavaScript packages. Core parsing,

## Coding Style & Naming Conventions

Use Rust 2021 conventions and keep code `rustfmt`-clean. Prefer explicit, domain-oriented names such as `model_id`, `provider`, `source`, and `session`; preserve raw model IDs for pricing while normalizing only display/grouping labels. TypeScript packages are ESM and should keep source under `src/` and build output under `dist/`.
Use Rust 2021 conventions and keep code `rustfmt`-clean. Prefer explicit, domain-oriented names such as `model_id`, `provider`, `source`, and `session`; preserve raw model observations for diagnostics, then use the canonical model ID for grouping and pricing. TypeScript packages are ESM and should keep source under `src/` and build output under `dist/`.

## Testing Guidelines

Expand Down Expand Up @@ -50,6 +50,10 @@ matching the split documented in `docs/development.md`.
- A behavior change justified by ADR 0001 must include a focused regression
case. If an established domain rule is changing, update its ADR and tests
deliberately rather than treating the change as generic cleanup.
- Provider attribution is optional usage metadata: retain valid model/token
records, infer centrally, and use `unknown` when inference fails. Only an
explicitly documented ownership/filter/dedup field may gate eligibility; see
ADR 0020 and the Zed source boundary.

## Git Identity & Merge Discipline

Expand Down
9 changes: 9 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ file take precedence when upstream semantics conflict with local needs.
the core model canonicalizer before aggregation and pricing. Date, release,
free-channel, and source decorations are not preserved as model identity in
this branch.
- `raw_model_label` is the non-empty model observation persisted by a source
before final canonicalization. It remains valid usage identity when optional
alias or provider enrichment is unavailable.
- `provider_id` is attribution metadata resolved from an explicit source value,
deterministic model-family inference, or `unknown`. It is not a prerequisite
for retaining model and token facts.
- `workspace` is the local working directory attribution used by reports and
the TUI.

Expand All @@ -24,6 +30,9 @@ file take precedence when upstream semantics conflict with local needs.
- Do not add silent fallback, fake success, mock execution, or defensive
degradation to make an unclear state look successful. Failures should surface
as explicit errors, logs, or failing tests.
- Do not reject a positive, timestamped usage record with a non-empty model
label merely because provider attribution cannot be resolved. Keep the model
and tokens, infer centrally when possible, and otherwise use `unknown`.
- Read local client storage in its accepted current format only, as established
by ADR 0019. OpenCode reads current SQLite databases, not legacy message JSON;
obsolete schemas and database I/O/query failures are explicit errors.
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/adapters/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::message_cache::{ParserId, ParserVersion};
use crate::sessions;

const ANTIGRAVITY_CLI_RECORD_REJECTION_REVISION: u32 =
crate::adapters::EXPLICIT_TOKEN_OVERFLOW_REVISION + 1;
crate::adapters::EXPLICIT_TOKEN_OVERFLOW_REVISION + 2;

pub(crate) struct AntigravityAdapter;

Expand Down
160 changes: 131 additions & 29 deletions crates/tokscale-core/src/adapters/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ pub(crate) fn plan_cache_hit(
detail: "cache planner lost its freshly validated snapshot".to_string(),
}
})?;
let stamp = unit.source_input_policy().stamp_from_snapshot(snapshot)?;
let stamp = match unit.source_input_policy().stamp_from_snapshot(snapshot) {
Ok(stamp) => stamp,
Err(source) if preserves_primary_on_related_failure(&unit, &source) => {
unit.mark_cache_lookup_completed_no_hit();
return Ok(CacheHitPlan::Miss(unit));
}
Err(source) => return Err(source.into()),
};
if cached.fingerprint.stamp != stamp {
unit.mark_cache_lookup_completed_no_hit();
return Ok(CacheHitPlan::Miss(unit));
Expand Down Expand Up @@ -74,29 +81,6 @@ where
load_or_scan_unit_cacheable(unit, ctx, ScanCacheOptions::default(), scan)
}

/// Scan a primary source whose related fingerprint inputs only provide
/// optional metadata. If hashing one of those inputs fails, the primary scan
/// still runs, the failure is exposed as partial health when the parser did
/// not report a more specific interruption, and no cache shard is written.
pub(crate) fn load_or_scan_unit_with_optional_related_inputs<F>(
unit: SourceUnit,
ctx: &ParseContext<'_>,
scan: F,
) -> ParsedUnit
where
F: Fn(&Path) -> crate::sessions::error::SessionParseResult<ScannedSource>,
{
load_or_scan_unit_cacheable(
unit,
ctx,
ScanCacheOptions {
preserve_primary_on_fingerprint_failure: true,
..ScanCacheOptions::default()
},
|path| scan(path).map(|scanned| (scanned, true)),
)
}

pub(crate) fn load_or_scan_empty_sentinel_with_primary_hash<F>(
unit: SourceUnit,
ctx: &ParseContext<'_>,
Expand All @@ -116,7 +100,6 @@ where
hash: primary_hash,
snapshot: primary_snapshot,
}),
..ScanCacheOptions::default()
},
|path| scan(path).map(|scanned| (scanned, true)),
)
Expand All @@ -136,11 +119,11 @@ where
unit,
ctx,
ScanCacheOptions {
cache_clean_empty: false,
precomputed_content_hash: Some(PrecomputedContentHash::Dependency {
hash: dependency_hash,
snapshot: dependency_snapshot,
}),
..ScanCacheOptions::default()
},
|path| scan(path).map(|scanned| (scanned, true)),
)
Expand All @@ -159,7 +142,6 @@ enum PrecomputedContentHash {

#[derive(Default)]
struct ScanCacheOptions {
preserve_primary_on_fingerprint_failure: bool,
cache_clean_empty: bool,
precomputed_content_hash: Option<PrecomputedContentHash>,
}
Expand All @@ -174,7 +156,6 @@ where
F: Fn(&Path) -> crate::sessions::error::SessionParseResult<(ScannedSource, bool)>,
{
let ScanCacheOptions {
preserve_primary_on_fingerprint_failure,
cache_clean_empty,
precomputed_content_hash,
} = options;
Expand Down Expand Up @@ -222,7 +203,7 @@ where
};
let (fingerprint, fingerprint_failure) = match fingerprint_result {
Some(Ok(fingerprint)) => (Some(fingerprint), None),
Some(Err(source)) if preserve_primary_on_fingerprint_failure => {
Some(Err(source)) if preserves_primary_on_related_failure(&unit, &source) => {
(None, Some(snapshot_failure(source)))
}
Some(Err(source)) => return ParsedUnit::unavailable(unit, snapshot_failure(source)),
Expand Down Expand Up @@ -340,6 +321,13 @@ fn snapshot_failure(source: message_cache::SourceSnapshotError) -> SourceFailure
SourceFailure::new("snapshot source metadata and content", source.to_string())
}

fn preserves_primary_on_related_failure(
unit: &SourceUnit,
source: &message_cache::SourceSnapshotError,
) -> bool {
unit.preserves_primary_on_related_failure() && source.is_optional_related_input_unavailable()
}

pub(crate) fn fold_units(
parsed: Vec<ParsedUnit>,
ctx: &mut FoldContext<'_>,
Expand Down Expand Up @@ -534,6 +522,21 @@ mod tests {
)
}

fn scanned_message() -> UnifiedMessage {
UnifiedMessage::new(
"test",
"gpt-5",
"openai",
"session",
1,
TokenBreakdown {
input: 1,
..Default::default()
},
0.0,
)
}

fn pi_unit(path: &Path) -> SourceUnit {
SourceUnit::plain_file(ClientId::Pi, path.to_path_buf())
}
Expand Down Expand Up @@ -974,6 +977,105 @@ mod tests {
));
}

#[test]
fn optional_related_failure_scans_primary_and_invalidates_warm_cache() {
let dir = tempfile::TempDir::new().unwrap();
let primary = dir.path().join("session.json");
let related = dir.path().join("metadata.jsonl");
std::fs::write(&primary, b"primary contents").unwrap();
std::fs::write(&related, b"related contents").unwrap();
let unit = SourceUnit::plain_file(ClientId::Kiro, primary.clone())
.with_optional_dependency(related.clone());
let parser_version = unit.parser_version;
let fingerprint = unit.source_input_policy().fingerprint().unwrap();
let mut cache = message_cache::SourceMessageCache::default();
cache.insert(message_cache::CachedSourceEntry::new_with_version(
&primary,
unit.parser_version,
fingerprint,
vec![cached_message()],
None,
));

std::fs::remove_file(&related).unwrap();
std::fs::create_dir(&related).unwrap();
let miss = expect_cache_miss(
plan_cache_hit(unit, &cache),
"an unavailable optional related input must force a cache miss",
);
let scan_called = std::cell::Cell::new(false);
let parsed = load_or_scan_unit_with(miss, &ParseContext { pricing: None }, |_| {
scan_called.set(true);
Ok(ScannedSource::complete(vec![scanned_message()]))
});

assert!(
scan_called.get(),
"the readable primary must still be scanned"
);
assert!(parsed.cache_write.is_none());
assert!(parsed.invalidate_cache);
assert!(matches!(parsed.health.status, SourceStatus::Partial { .. }));
assert!(matches!(
parsed.messages,
UnitMessageSource::Fresh(ref messages) if messages.len() == 1
));
let messages = fold_planned_unit(parsed, &mut cache);
assert_eq!(messages.len(), 1);
assert!(
cache.get_meta(&primary, parser_version).unwrap().is_none(),
"the stale shard must be invalidated instead of surviving the partial scan"
);
}

#[test]
fn required_related_fingerprint_failure_keeps_source_unavailable() {
let dir = tempfile::TempDir::new().unwrap();
let primary = dir.path().join("child.jsonl");
let dependency = dir.path().join("parent.jsonl");
std::fs::write(&primary, b"child contents").unwrap();
std::fs::create_dir(&dependency).unwrap();
let unit =
SourceUnit::plain_file(ClientId::CommandCode, primary).with_dependency(dependency);
let scan_called = std::cell::Cell::new(false);

let parsed = load_or_scan_unit_with(unit, &ParseContext { pricing: None }, |_| {
scan_called.set(true);
Ok(ScannedSource::complete(vec![cached_message()]))
});

assert!(!scan_called.get());
assert!(matches!(
parsed.health.status,
SourceStatus::Unavailable { .. }
));
assert!(parsed.cache_write.is_none());
}

#[test]
fn primary_fingerprint_failure_is_not_preserved_by_optional_contract() {
let dir = tempfile::TempDir::new().unwrap();
let primary = dir.path().join("session.json");
let related = dir.path().join("metadata.jsonl");
std::fs::create_dir(&primary).unwrap();
std::fs::write(&related, b"related contents").unwrap();
let unit =
SourceUnit::plain_file(ClientId::Kiro, primary).with_optional_dependency(related);
let scan_called = std::cell::Cell::new(false);

let parsed = load_or_scan_unit_with(unit, &ParseContext { pricing: None }, |_| {
scan_called.set(true);
Ok(ScannedSource::complete(vec![cached_message()]))
});

assert!(!scan_called.get());
assert!(matches!(
parsed.health.status,
SourceStatus::Unavailable { .. }
));
assert!(parsed.cache_write.is_none());
}

#[test]
fn wal_change_during_parse_prevents_cache_write() {
let dir = tempfile::TempDir::new().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions crates/tokscale-core/src/adapters/codebuddy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use crate::UnifiedMessage;

const MIRROR_DEDUP_WINDOW_MS: i64 = 1000;
const CODEBUDDY_JSONL_RECORD_REJECTION_REVISION: u32 =
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 1;
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 2;
const CODEBUDDY_EXTENSION_RECORD_REJECTION_REVISION: u32 =
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 1;
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 2;

pub(crate) struct CodeBuddyAdapter;

Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-core/src/adapters/codebuff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::sessions;
pub(crate) struct CodebuffAdapter;

const CODEBUFF_RECORD_REJECTION_REVISION: u32 =
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 1;
crate::adapters::MODEL_ID_CANONICALIZATION_REVISION + 2;

impl LocalSourceAdapter for CodebuffAdapter {
fn client(&self) -> ClientId {
Expand Down
25 changes: 20 additions & 5 deletions crates/tokscale-core/src/adapters/discover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,29 @@ fn source_unit_for_policy(
},
);
}
FingerprintPolicy::PrimaryWithSiblings { sibling_names } => {
FingerprintPolicy::PrimaryWithSiblings {
sibling_names,
related_failure_policy,
} => {
let mut unit = SourceUnit::plain_file(client, path);
unit.fingerprint_policy = FingerprintPolicy::PrimaryWithSiblings { sibling_names };
unit.fingerprint_policy = FingerprintPolicy::PrimaryWithSiblings {
sibling_names,
related_failure_policy: *related_failure_policy,
};
unit
}
FingerprintPolicy::PrimaryWithDependency { dependency_path } => {
SourceUnit::plain_file(client, path).with_dependency(dependency_path.clone())
}
FingerprintPolicy::PrimaryWithDependency {
dependency_path,
related_failure_policy,
} => match related_failure_policy {
crate::message_cache::RelatedInputFailurePolicy::FailSource => {
SourceUnit::plain_file(client, path).with_dependency(dependency_path.clone())
}
crate::message_cache::RelatedInputFailurePolicy::PreservePrimary => {
SourceUnit::plain_file(client, path)
.with_optional_dependency(dependency_path.clone())
}
},
FingerprintPolicy::NoMessageCache => SourceUnit::no_message_cache(client, path),
};
Ok(unit)
Expand Down
Loading
Loading