feat(report): add multi-backend summarizer and LLM task grouping - #633
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
10 issues found across 8 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/tokscale-core/src/content_extractor.rs">
<violation number="1" location="crates/tokscale-core/src/content_extractor.rs:262">
P2: The truncation guard uses byte length instead of character count, which incorrectly appends ellipses for non-ASCII input.</violation>
</file>
<file name="crates/tokscale-cli/src/commands/report.rs">
<violation number="1" location="crates/tokscale-cli/src/commands/report.rs:208">
P2: Unknown `--summarizer` values return `Ok(())` instead of an error, causing silent misconfiguration and skipped summarization.</violation>
<violation number="2" location="crates/tokscale-cli/src/commands/report.rs:250">
P1: DB errors are swallowed when writing summaries, which can silently lose summarization results while reporting success.</violation>
<violation number="3" location="crates/tokscale-cli/src/commands/report.rs:327">
P2: Task grouping is silently skipped for unsupported backends (including the default `apple-fm`), so the feature can appear to run successfully while never producing task groups.</violation>
<violation number="4" location="crates/tokscale-cli/src/commands/report.rs:347">
P2: DB errors are swallowed when saving `task_group`, so grouping can silently fail while the command reports success.</violation>
<violation number="5" location="crates/tokscale-cli/src/commands/report.rs:727">
P2: End-of-day filtering truncates the last 999ms, so some sessions at the end of the `--until` day are incorrectly excluded.</violation>
</file>
<file name="crates/tokscale-core/src/wiki.rs">
<violation number="1" location="crates/tokscale-core/src/wiki.rs:116">
P2: Fallback config path uses an unexpanded `~`, which can write the wiki DB to an unintended location.</violation>
<violation number="2" location="crates/tokscale-core/src/wiki.rs:375">
P2: The `until` filter is inclusive in `query_entries` but exclusive in other range methods, causing inconsistent date-scoped behavior.</violation>
</file>
<file name="scripts/wiki-summarizer.py">
<violation number="1" location="scripts/wiki-summarizer.py:129">
P2: Validate `task_category`/`complexity` against allowed values before storing. Right now any model output string is accepted, even when it violates the declared schema.</violation>
<violation number="2" location="scripts/wiki-summarizer.py:136">
P2: The recovery path can crash because `session['session_id']` is dereferenced inside the exception handler. A malformed session then raises a second `KeyError` and aborts summarization.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
6 issues found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/tokscale-cli/src/commands/optimize.rs">
<violation number="1" location="crates/tokscale-cli/src/commands/optimize.rs:195">
P2: `partial_cmp(...).unwrap()` on `f64` can panic if `total_cost` is NaN. Use the safer pattern already established in the codebase.</violation>
<violation number="2" location="crates/tokscale-cli/src/commands/optimize.rs:455">
P2: Truncating `String` with `[..27]` can panic on non-ASCII model names due to invalid UTF-8 boundary slicing.</violation>
</file>
<file name="crates/tokscale-cli/src/main.rs">
<violation number="1" location="crates/tokscale-cli/src/main.rs:763">
P1: `--optimize` currently runs during `--json` report output, appending human-formatted text and corrupting JSON output for automation.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:683">
P2: The new optimize example claims it defaults to today, but the command actually uses all sessions unless a date flag is provided.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| week, | ||
| month, | ||
| }); | ||
| if optimize && result.is_ok() { |
There was a problem hiding this comment.
P1: --optimize currently runs during --json report output, appending human-formatted text and corrupting JSON output for automation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/tokscale-cli/src/main.rs, line 763:
<comment>`--optimize` currently runs during `--json` report output, appending human-formatted text and corrupting JSON output for automation.</comment>
<file context>
@@ -745,6 +759,38 @@ fn main() -> Result<()> {
week,
month,
+ });
+ if optimize && result.is_ok() {
+ let _ = commands::optimize::run_optimize(commands::optimize::OptimizeOptions {
+ json: false,
</file context>
| if optimize && result.is_ok() { | |
| if optimize && !json && result.is_ok() { |
| println!(" {}", "─".repeat(68)); | ||
| for m in report.model_insights.iter().take(8) { | ||
| let model_display: String = if m.model.len() > 28 { | ||
| format!("{}…", &m.model[..27]) |
There was a problem hiding this comment.
P2: Truncating String with [..27] can panic on non-ASCII model names due to invalid UTF-8 boundary slicing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/tokscale-cli/src/commands/optimize.rs, line 455:
<comment>Truncating `String` with `[..27]` can panic on non-ASCII model names due to invalid UTF-8 boundary slicing.</comment>
<file context>
@@ -0,0 +1,554 @@
+ println!(" {}", "─".repeat(68));
+ for m in report.model_insights.iter().take(8) {
+ let model_display: String = if m.model.len() > 28 {
+ format!("{}…", &m.model[..27])
+ } else {
+ m.model.clone()
</file context>
| format!("{}…", &m.model[..27]) | |
| format!("{}…", m.model.chars().take(27).collect::<String>()) |
| Tokscale can analyze your usage patterns and generate actionable recommendations to reduce costs and improve productivity. | ||
|
|
||
| ```bash | ||
| # Standalone optimization analysis (defaults to today) |
There was a problem hiding this comment.
P2: The new optimize example claims it defaults to today, but the command actually uses all sessions unless a date flag is provided.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 683:
<comment>The new optimize example claims it defaults to today, but the command actually uses all sessions unless a date flag is provided.</comment>
<file context>
@@ -674,6 +675,65 @@ tokscale report --workspace my-project --client opencode
+Tokscale can analyze your usage patterns and generate actionable recommendations to reduce costs and improve productivity.
+
+```bash
+# Standalone optimization analysis (defaults to today)
+tokscale optimize
+
</file context>
| # Standalone optimization analysis (defaults to today) | |
| # Standalone optimization analysis (uses all cached sessions by default) |
2cf54a9 to
36c4ce5
Compare
- Group sessions by model and task title in summary tables - Show daily breakdown for --week/--month, session list for --today - Integrate Apple FM summarizer for session classification - Add wiki DB for caching session summaries
- Support claude, codex, gemini, kiro as summarizer backends (in addition to apple-fm) - Add 2nd LLM pass to cluster sessions into high-level task groups - Add --summarizer flag to select backend, --rebuild to reset cached summaries - Scope summarization to date range (--week, --since/--until) - Add task_group column to wiki DB with migration - Update README with Task-Attributed Report documentation
…agate DB errors - Return error instead of Ok(()) for unknown --summarizer values - Propagate DB errors from update_summary and update_task_group - Clarify skip message when task grouping backend is unsupported (apple-fm)
- Fix end-of-day filtering: use next_day_00:00 - 1ms instead of 23:59:59
- Fix wiki.rs fallback path: use dirs::home_dir() instead of literal '~/.config'
- Fix until filter inconsistency: query_entries now uses '<' (exclusive) matching other range methods
- Validate task_category/complexity against allowed values in wiki-summarizer.py
- Fix recovery path crash: use session.get('session_id') instead of session['session_id'] in exception handler
- content_extractor: use chars().count() instead of byte len() for truncation guard - wiki.rs: rename from_str to parse to avoid clippy::should_implement_trait - report.rs: use div_ceil() instead of manual ceiling division - usage/copilot.rs: use strip_prefix() instead of manual prefix stripping - usage/minimax.rs: remove redundant closure - usage/zai.rs: fix reference-to-reference pattern - usage/mod.rs: extract type alias for complex type, use .ok() instead of manual match
36c4ce5 to
d6c5432
Compare
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/tokscale-cli/src/main.rs">
<violation number="1" location="crates/tokscale-cli/src/main.rs:763">
P1: `--optimize` currently runs during `--json` report output, appending human-formatted text and corrupting JSON output for automation.</violation>
</file>
<file name="crates/tokscale-cli/src/commands/optimize.rs">
<violation number="1" location="crates/tokscale-cli/src/commands/optimize.rs:455">
P2: Truncating `String` with `[..27]` can panic on non-ASCII model names due to invalid UTF-8 boundary slicing.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:683">
P2: The new optimize example claims it defaults to today, but the command actually uses all sessions unless a date flag is provided.</violation>
</file>
<file name=".opencode/skill/deploy.md">
<violation number="1" location=".opencode/skill/deploy.md:49">
P2: `docker compose exec` allocates a TTY by default; in non-interactive environments like AWS SSM this can fail with "the input device is not a TTY". Use `-T` to disable pseudo-TTY allocation.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| --instance-ids "i-078fe82953c3047b5" \ | ||
| --document-name "AWS-RunShellScript" \ | ||
| --region ap-northeast-2 \ | ||
| --parameters 'commands=["export HOME=/home/ubuntu && cd /home/ubuntu/tokscale/self-host && docker compose exec app npx drizzle-kit push --force"]' \ |
There was a problem hiding this comment.
P2: docker compose exec allocates a TTY by default; in non-interactive environments like AWS SSM this can fail with "the input device is not a TTY". Use -T to disable pseudo-TTY allocation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .opencode/skill/deploy.md, line 49:
<comment>`docker compose exec` allocates a TTY by default; in non-interactive environments like AWS SSM this can fail with "the input device is not a TTY". Use `-T` to disable pseudo-TTY allocation.</comment>
<file context>
@@ -0,0 +1,61 @@
+ --instance-ids "i-078fe82953c3047b5" \
+ --document-name "AWS-RunShellScript" \
+ --region ap-northeast-2 \
+ --parameters 'commands=["export HOME=/home/ubuntu && cd /home/ubuntu/tokscale/self-host && docker compose exec app npx drizzle-kit push --force"]' \
+ --timeout-seconds 120 \
+ --output json
</file context>
7657640 to
f00d277
Compare
…pair build after rebase After merging main, the report arm called build_date_filter with the old 5-arg form; the DateRangeFlags refactor changed it to take &DateRangeFlags. compute_msg_cost hardcoded flat per-token rates that only matched Sonnet and ignored cache_write/reasoning; it now prices through the canonical PricingService (calculate_cost_with_provider) exactly like the aggregator, loading a fresh dataset with a cached fallback for offline use. Display truncation used byte slicing on LLM-generated strings (UTF-8 boundary panic), per-model attribution divided by models_used.len() with no zero guard, and cost sorts used partial_cmp().unwrap() (NaN panic). Constraint: pricing must come from canonical PricingService, never hardcoded constants or fuzzy matching Confidence: medium Scope-risk: moderate Not-tested: live LLM summarizer backends (apple-fm / claude/codex/gemini CLIs)
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/tokscale-cli/src/commands/report.rs">
<violation number="1" location="crates/tokscale-cli/src/commands/report.rs:114">
P1: Cost attribution can be permanently zeroed for newly ingested sessions when pricing data is temporarily unavailable</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| }) | ||
| .map_err(|e| anyhow::anyhow!("{}", e))?; | ||
|
|
||
| let pricing = load_pricing_service(); |
There was a problem hiding this comment.
P1: Cost attribution can be permanently zeroed for newly ingested sessions when pricing data is temporarily unavailable
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/tokscale-cli/src/commands/report.rs, line 114:
<comment>Cost attribution can be permanently zeroed for newly ingested sessions when pricing data is temporarily unavailable</comment>
<file context>
@@ -112,6 +111,8 @@ fn populate_wiki_from_sessions(db: &WikiDb, opts: &ReportOptions) -> Result<()>
})
.map_err(|e| anyhow::anyhow!("{}", e))?;
+ let pricing = load_pricing_service();
+
let mut session_map: HashMap<String, SessionAgg> = HashMap::new();
</file context>
Every LLM backend invocation in run_task_grouping, run_cli_summarizer, and the apple-fm path spawned a child and blocked forever with no timeout, so an auth prompt or network stall could hang `tokscale report` indefinitely. Add a pure-std run_command_with_timeout helper (spawn + reader threads draining stdout/stderr + try_wait deadline loop + kill on timeout, mirroring run_capture_command in main.rs) bounded by a generous 300s BACKEND_TIMEOUT. A timeout surfaces as an io::ErrorKind::TimedOut that each call site now degrades gracefully (log + skip the backend), matching the existing non-zero-exit behavior. The apple-fm path keeps its stdin-write behavior via the helper's optional stdin_bytes argument. Constraint: pure-std timeout (spawn + try_wait deadline + kill), mirroring main.rs — no new dependency Confidence: high Scope-risk: narrow
… keying Addresses automated review feedback on the #633 report-summarizer-content fix (PR #746). The summarizer still surfaced (none) for normal Codex/Gemini sessions and could mis-route cross-client session_id collisions. - content_extractor: parse the current on-disk Codex format (event_msg with payload.type == "user_message", text in payload.message) and skip harness-injected context blocks (<environment_context>/<system-reminder>/ <user_instructions>), mirroring sessions::codex. - content_extractor: Gemini extractor now handles chat-recording JSON (messages[].type == "user" / content) and falls back to scanning line-delimited JSONL; empty/whitespace user text is treated as not-found. - extract_session_content: an empty/whitespace first_user_message no longer counts as success, so scanning continues to a later candidate with real text. - report: SessionPathIndex is keyed by (client, session_id) to prevent cross-client collisions, and Gemini files are keyed by their in-file sessionId (via gemini_session_id_for_file) rather than the filename stem, since the wiki entry's session_id is derived from inside the file. - Added fixture-based regression tests for all of the above. Constraint: wiki session_id for Gemini comes from the in-file sessionId, not the path stem Rejected: match any leading '<' for Codex injected blocks | drops legit prompts starting with markup Confidence: high Scope-risk: narrow
…746) * docs: sync EN/ja/ko/zh-cn for unreleased clients + breaking flag removal Pre-v3.2.0 documentation sync for the unreleased range (v3.1.3..main): - #465: replace the now-false legacy per-client flag notices in ja/ko/zh-cn (they claimed the removed flags still work) with a v3.2.0 breaking-change migration note; add the same note to README.md (which had none). - #728: document the MiniMax Token Plan subscription source (distinct from the MINIMAX_API_KEY row) in all locales; port the entire Subscription Usage section into ja/ko/zh-cn (was English-only). - #718: add the Jcode table row + detail section to the locales missing them. - #726: document the TOKSCALE_FM_DEBUG env var in all locales. - #633: add the missing task-attributed report bullet to README.ja Key Features. - drift: add Junie to the frontend Source-filtering list (all locales). - #710: fix the MiMo Code repo link (XiaomiMiMo/MiMo -> XiaomiMiMo/MiMo-Code). - #717: disclose Command Code token usage is estimated (~4 chars/token). Confidence: medium Scope-risk: narrow Directive: ja/ko/zh-cn translations of the ported Subscription Usage section are machine-generated and should get a native-speaker review pass Not-tested: #713 Antigravity CLI detail section was not added — no English source section exists to port from * fix(report): feed real session content to the summarizer (#633) extract_content_for_session unconditionally returned metadata_only_content() (first_user_message hardcoded None), so the report summarizer never saw any conversation content and the four real per-client extractors were dead code. Add content_extractor::extract_session_content, which dispatches to the correct per-client extractor (opencode/claude/codex/gemini) and falls back to metadata-only — never erroring or panicking — for unknown clients, missing candidates, or unreadable/unparseable files. report.rs builds a SessionPathIndex once (session_id -> transcript file, plus opencode DBs) and threads it through run_summarizer so each payload carries the real first user message. Confidence: high Scope-risk: moderate Rejected: thread file paths through core's scanner/WikiEntry | too invasive; indexed at the report layer instead Not-tested: end-to-end opencode/codex/gemini extraction in report.rs (core dispatcher covers claude + all fallback paths; per-client extractors are pre-existing) * docs: name the breaking release v4.0.0 (was v3.2.0) The per-client flag removal (#465) is a breaking change, so the next release is v4.0.0, not v3.2.0. Update the migration notes in all four README locales and the main.rs doc comment accordingly. * fix(report): real Codex/Gemini extraction + (client,session_id) index keying Addresses automated review feedback on the #633 report-summarizer-content fix (PR #746). The summarizer still surfaced (none) for normal Codex/Gemini sessions and could mis-route cross-client session_id collisions. - content_extractor: parse the current on-disk Codex format (event_msg with payload.type == "user_message", text in payload.message) and skip harness-injected context blocks (<environment_context>/<system-reminder>/ <user_instructions>), mirroring sessions::codex. - content_extractor: Gemini extractor now handles chat-recording JSON (messages[].type == "user" / content) and falls back to scanning line-delimited JSONL; empty/whitespace user text is treated as not-found. - extract_session_content: an empty/whitespace first_user_message no longer counts as success, so scanning continues to a later candidate with real text. - report: SessionPathIndex is keyed by (client, session_id) to prevent cross-client collisions, and Gemini files are keyed by their in-file sessionId (via gemini_session_id_for_file) rather than the filename stem, since the wiki entry's session_id is derived from inside the file. - Added fixture-based regression tests for all of the above. Constraint: wiki session_id for Gemini comes from the in-file sessionId, not the path stem Rejected: match any leading '<' for Codex injected blocks | drops legit prompts starting with markup Confidence: high Scope-risk: narrow
…hoyeo#633) * feat(report): add report command with model/task/daily breakdown - Group sessions by model and task title in summary tables - Show daily breakdown for --week/--month, session list for --today - Integrate Apple FM summarizer for session classification - Add wiki DB for caching session summaries * feat(report): add multi-backend summarizer and LLM task grouping - Support claude, codex, gemini, kiro as summarizer backends (in addition to apple-fm) - Add 2nd LLM pass to cluster sessions into high-level task groups - Add --summarizer flag to select backend, --rebuild to reset cached summaries - Scope summarization to date range (--week, --since/--until) - Add task_group column to wiki DB with migration - Update README with Task-Attributed Report documentation * fix(report): address cubic review - error on unknown summarizer, propagate DB errors - Return error instead of Ok(()) for unknown --summarizer values - Propagate DB errors from update_summary and update_task_group - Clarify skip message when task grouping backend is unsupported (apple-fm) * fix(report): address remaining cubic review issues - Fix end-of-day filtering: use next_day_00:00 - 1ms instead of 23:59:59 - Fix wiki.rs fallback path: use dirs::home_dir() instead of literal '~/.config' - Fix until filter inconsistency: query_entries now uses '<' (exclusive) matching other range methods - Validate task_category/complexity against allowed values in wiki-summarizer.py - Fix recovery path crash: use session.get('session_id') instead of session['session_id'] in exception handler * fix: resolve clippy lint errors - content_extractor: use chars().count() instead of byte len() for truncation guard - wiki.rs: rename from_str to parse to avoid clippy::should_implement_trait - report.rs: use div_ceil() instead of manual ceiling division - usage/copilot.rs: use strip_prefix() instead of manual prefix stripping - usage/minimax.rs: remove redundant closure - usage/zai.rs: fix reference-to-reference pattern - usage/mod.rs: extract type alias for complex type, use .ok() instead of manual match * fix(report): use canonical pricing, fix UTF-8/NaN/div-zero panics, repair build after rebase After merging main, the report arm called build_date_filter with the old 5-arg form; the DateRangeFlags refactor changed it to take &DateRangeFlags. compute_msg_cost hardcoded flat per-token rates that only matched Sonnet and ignored cache_write/reasoning; it now prices through the canonical PricingService (calculate_cost_with_provider) exactly like the aggregator, loading a fresh dataset with a cached fallback for offline use. Display truncation used byte slicing on LLM-generated strings (UTF-8 boundary panic), per-model attribution divided by models_used.len() with no zero guard, and cost sorts used partial_cmp().unwrap() (NaN panic). Constraint: pricing must come from canonical PricingService, never hardcoded constants or fuzzy matching Confidence: medium Scope-risk: moderate Not-tested: live LLM summarizer backends (apple-fm / claude/codex/gemini CLIs) * style: apply rustfmt * fix(report): bound LLM summarizer subprocesses with a timeout Every LLM backend invocation in run_task_grouping, run_cli_summarizer, and the apple-fm path spawned a child and blocked forever with no timeout, so an auth prompt or network stall could hang `tokscale report` indefinitely. Add a pure-std run_command_with_timeout helper (spawn + reader threads draining stdout/stderr + try_wait deadline loop + kill on timeout, mirroring run_capture_command in main.rs) bounded by a generous 300s BACKEND_TIMEOUT. A timeout surfaces as an io::ErrorKind::TimedOut that each call site now degrades gracefully (log + skip the backend), matching the existing non-zero-exit behavior. The apple-fm path keeps its stdin-write behavior via the helper's optional stdin_bytes argument. Constraint: pure-std timeout (spawn + try_wait deadline + kill), mirroring main.rs — no new dependency Confidence: high Scope-risk: narrow --------- Co-authored-by: Junho Yeo <i@junho.io>
…hoyeo#633) * feat(report): add report command with model/task/daily breakdown - Group sessions by model and task title in summary tables - Show daily breakdown for --week/--month, session list for --today - Integrate Apple FM summarizer for session classification - Add wiki DB for caching session summaries * feat(report): add multi-backend summarizer and LLM task grouping - Support claude, codex, gemini, kiro as summarizer backends (in addition to apple-fm) - Add 2nd LLM pass to cluster sessions into high-level task groups - Add --summarizer flag to select backend, --rebuild to reset cached summaries - Scope summarization to date range (--week, --since/--until) - Add task_group column to wiki DB with migration - Update README with Task-Attributed Report documentation * fix(report): address cubic review - error on unknown summarizer, propagate DB errors - Return error instead of Ok(()) for unknown --summarizer values - Propagate DB errors from update_summary and update_task_group - Clarify skip message when task grouping backend is unsupported (apple-fm) * fix(report): address remaining cubic review issues - Fix end-of-day filtering: use next_day_00:00 - 1ms instead of 23:59:59 - Fix wiki.rs fallback path: use dirs::home_dir() instead of literal '~/.config' - Fix until filter inconsistency: query_entries now uses '<' (exclusive) matching other range methods - Validate task_category/complexity against allowed values in wiki-summarizer.py - Fix recovery path crash: use session.get('session_id') instead of session['session_id'] in exception handler * fix: resolve clippy lint errors - content_extractor: use chars().count() instead of byte len() for truncation guard - wiki.rs: rename from_str to parse to avoid clippy::should_implement_trait - report.rs: use div_ceil() instead of manual ceiling division - usage/copilot.rs: use strip_prefix() instead of manual prefix stripping - usage/minimax.rs: remove redundant closure - usage/zai.rs: fix reference-to-reference pattern - usage/mod.rs: extract type alias for complex type, use .ok() instead of manual match * fix(report): use canonical pricing, fix UTF-8/NaN/div-zero panics, repair build after rebase After merging main, the report arm called build_date_filter with the old 5-arg form; the DateRangeFlags refactor changed it to take &DateRangeFlags. compute_msg_cost hardcoded flat per-token rates that only matched Sonnet and ignored cache_write/reasoning; it now prices through the canonical PricingService (calculate_cost_with_provider) exactly like the aggregator, loading a fresh dataset with a cached fallback for offline use. Display truncation used byte slicing on LLM-generated strings (UTF-8 boundary panic), per-model attribution divided by models_used.len() with no zero guard, and cost sorts used partial_cmp().unwrap() (NaN panic). Constraint: pricing must come from canonical PricingService, never hardcoded constants or fuzzy matching Confidence: medium Scope-risk: moderate Not-tested: live LLM summarizer backends (apple-fm / claude/codex/gemini CLIs) * style: apply rustfmt * fix(report): bound LLM summarizer subprocesses with a timeout Every LLM backend invocation in run_task_grouping, run_cli_summarizer, and the apple-fm path spawned a child and blocked forever with no timeout, so an auth prompt or network stall could hang `tokscale report` indefinitely. Add a pure-std run_command_with_timeout helper (spawn + reader threads draining stdout/stderr + try_wait deadline loop + kill on timeout, mirroring run_capture_command in main.rs) bounded by a generous 300s BACKEND_TIMEOUT. A timeout surfaces as an io::ErrorKind::TimedOut that each call site now degrades gracefully (log + skip the backend), matching the existing non-zero-exit behavior. The apple-fm path keeps its stdin-write behavior via the helper's optional stdin_bytes argument. Constraint: pure-std timeout (spawn + try_wait deadline + kill), mirroring main.rs — no new dependency Confidence: high Scope-risk: narrow --------- Co-authored-by: Junho Yeo <i@junho.io>
…unhoyeo#746) * docs: sync EN/ja/ko/zh-cn for unreleased clients + breaking flag removal Pre-v3.2.0 documentation sync for the unreleased range (v3.1.3..main): - junhoyeo#465: replace the now-false legacy per-client flag notices in ja/ko/zh-cn (they claimed the removed flags still work) with a v3.2.0 breaking-change migration note; add the same note to README.md (which had none). - junhoyeo#728: document the MiniMax Token Plan subscription source (distinct from the MINIMAX_API_KEY row) in all locales; port the entire Subscription Usage section into ja/ko/zh-cn (was English-only). - junhoyeo#718: add the Jcode table row + detail section to the locales missing them. - junhoyeo#726: document the TOKSCALE_FM_DEBUG env var in all locales. - junhoyeo#633: add the missing task-attributed report bullet to README.ja Key Features. - drift: add Junie to the frontend Source-filtering list (all locales). - junhoyeo#710: fix the MiMo Code repo link (XiaomiMiMo/MiMo -> XiaomiMiMo/MiMo-Code). - junhoyeo#717: disclose Command Code token usage is estimated (~4 chars/token). Confidence: medium Scope-risk: narrow Directive: ja/ko/zh-cn translations of the ported Subscription Usage section are machine-generated and should get a native-speaker review pass Not-tested: junhoyeo#713 Antigravity CLI detail section was not added — no English source section exists to port from * fix(report): feed real session content to the summarizer (junhoyeo#633) extract_content_for_session unconditionally returned metadata_only_content() (first_user_message hardcoded None), so the report summarizer never saw any conversation content and the four real per-client extractors were dead code. Add content_extractor::extract_session_content, which dispatches to the correct per-client extractor (opencode/claude/codex/gemini) and falls back to metadata-only — never erroring or panicking — for unknown clients, missing candidates, or unreadable/unparseable files. report.rs builds a SessionPathIndex once (session_id -> transcript file, plus opencode DBs) and threads it through run_summarizer so each payload carries the real first user message. Confidence: high Scope-risk: moderate Rejected: thread file paths through core's scanner/WikiEntry | too invasive; indexed at the report layer instead Not-tested: end-to-end opencode/codex/gemini extraction in report.rs (core dispatcher covers claude + all fallback paths; per-client extractors are pre-existing) * docs: name the breaking release v4.0.0 (was v3.2.0) The per-client flag removal (junhoyeo#465) is a breaking change, so the next release is v4.0.0, not v3.2.0. Update the migration notes in all four README locales and the main.rs doc comment accordingly. * fix(report): real Codex/Gemini extraction + (client,session_id) index keying Addresses automated review feedback on the junhoyeo#633 report-summarizer-content fix (PR junhoyeo#746). The summarizer still surfaced (none) for normal Codex/Gemini sessions and could mis-route cross-client session_id collisions. - content_extractor: parse the current on-disk Codex format (event_msg with payload.type == "user_message", text in payload.message) and skip harness-injected context blocks (<environment_context>/<system-reminder>/ <user_instructions>), mirroring sessions::codex. - content_extractor: Gemini extractor now handles chat-recording JSON (messages[].type == "user" / content) and falls back to scanning line-delimited JSONL; empty/whitespace user text is treated as not-found. - extract_session_content: an empty/whitespace first_user_message no longer counts as success, so scanning continues to a later candidate with real text. - report: SessionPathIndex is keyed by (client, session_id) to prevent cross-client collisions, and Gemini files are keyed by their in-file sessionId (via gemini_session_id_for_file) rather than the filename stem, since the wiki entry's session_id is derived from inside the file. - Added fixture-based regression tests for all of the above. Constraint: wiki session_id for Gemini comes from the in-file sessionId, not the path stem Rejected: match any leading '<' for Codex injected blocks | drops legit prompts starting with markup Confidence: high Scope-risk: narrow
Summary
Add multi-backend LLM summarizer support and automatic task grouping to the
tokscale reportcommand.Changes
Multi-backend summarizer
claude,codex,gemini,kiroas summarizer backends in addition to the defaultapple-fm--summarizerflag to select backend (default:apple-fm)LLM task grouping (2nd pass)
Date-scoped operations
--week,--since/--untildate filters--rebuildflag resets cached summaries within the date range and re-summarizes from scratchSchema changes
task_groupcolumn towiki_entriestable with auto-migrationreset_summaries_in_rangeandget_unsummarized_session_ids_in_rangeDB methodsDocumentation
Usage
Summary by cubic
Adds a task-attributed usage report with multi-backend LLM summarization and optional task grouping. Pricing now uses the canonical
PricingService, and the CLI is resilient across empty or malformed inputs.New Features
tokscale reportshows model and task-group breakdowns; daily view for--week/--month, session list for today. Results are cached in a local wiki DB;--rebuildre-summarizes in range. Auto-migration addstask_group.--summarizer:apple-fm(default),claude,codex,gemini,kiro; batch mode for CLI backends. A second LLM pass clusters titled sessions into 3–8 task groups (requires a CLI backend);apple-fmskips grouping with a clear message.--json) and filters for--workspaceand--client;--no-summarizeshows raw data without LLM calls.Bug Fixes
PricingService(no hardcoded rates), including cache write/read and reasoning tokens; uses a fresh dataset with cached fallback for offline runs.untilis exclusive (next-day 00:00 minus 1ms) and DB queries use<to match; unknown--summarizernow errors and DB errors from summary/group updates propagate to the CLI.--no-summarize --jsonon an empty home emits valid JSON.wiki-summarizer.pyvalidatestask_category/complexity, handles missing IDs, and resolves the fallback path correctly.Written for commit c1352d2. Summary will update on new commits.