Skip to content

feat(report): add multi-backend summarizer and LLM task grouping - #633

Merged
junhoyeo merged 10 commits into
junhoyeo:mainfrom
leecoder:feat/report-task-grouping
Jun 17, 2026
Merged

feat(report): add multi-backend summarizer and LLM task grouping#633
junhoyeo merged 10 commits into
junhoyeo:mainfrom
leecoder:feat/report-task-grouping

Conversation

@leecoder

@leecoder leecoder commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Add multi-backend LLM summarizer support and automatic task grouping to the tokscale report command.

Changes

Multi-backend summarizer

  • Support claude, codex, gemini, kiro as summarizer backends in addition to the default apple-fm
  • New --summarizer flag to select backend (default: apple-fm)
  • Batch processing with progress indicator for CLI-based backends

LLM task grouping (2nd pass)

  • After summarization, a second LLM pass clusters all titled sessions into 3–8 high-level task groups
  • Groups are displayed in the report table with sub-session titles indented below
  • Results cached in wiki DB — subsequent runs skip already-grouped sessions

Date-scoped operations

  • Summarization now respects --week, --since/--until date filters
  • New --rebuild flag resets cached summaries within the date range and re-summarizes from scratch

Schema changes

  • Add task_group column to wiki_entries table with auto-migration
  • Add reset_summaries_in_range and get_unsummarized_session_ids_in_range DB methods

Documentation

  • Add Task-Attributed Report section to README with usage examples, backend table, and sample output

Usage

tokscale report --week --summarizer claude
tokscale report --rebuild --summarizer codex
tokscale report --week --json

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 report shows model and task-group breakdowns; daily view for --week/--month, session list for today. Results are cached in a local wiki DB; --rebuild re-summarizes in range. Auto-migration adds task_group.
    • Multi-backend --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-fm skips grouping with a clear message.
    • JSON output (--json) and filters for --workspace and --client; --no-summarize shows raw data without LLM calls.
  • Bug Fixes

    • Accurate pricing: all costs calculated through the canonical PricingService (no hardcoded rates), including cache write/read and reasoning tokens; uses a fresh dataset with cached fallback for offline runs.
    • Date ranges: until is exclusive (next-day 00:00 minus 1ms) and DB queries use < to match; unknown --summarizer now errors and DB errors from summary/group updates propagate to the CLI.
    • Robustness: bound all LLM summarizer subprocesses with a 5‑minute timeout; on timeout or non‑zero exit we log and skip to avoid hangs (pure stdlib, no new deps). Fixed UTF‑8 truncation, zero-length model splits, and NaN sort panics; --no-summarize --json on an empty home emits valid JSON. wiki-summarizer.py validates task_category/complexity, handles missing IDs, and resolves the fallback path correctly.

Written for commit c1352d2. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Preview Jun 17, 2026 7:47pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread crates/tokscale-cli/src/commands/report.rs Outdated
Comment thread crates/tokscale-core/src/content_extractor.rs Outdated
Comment thread crates/tokscale-cli/src/commands/report.rs Outdated
Comment thread crates/tokscale-cli/src/commands/report.rs Outdated
Comment thread crates/tokscale-cli/src/commands/report.rs Outdated
Comment thread crates/tokscale-cli/src/commands/report.rs
Comment thread crates/tokscale-core/src/wiki.rs Outdated
Comment thread crates/tokscale-core/src/wiki.rs Outdated
Comment thread scripts/wiki-summarizer.py Outdated
Comment thread scripts/wiki-summarizer.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread crates/tokscale-cli/src/main.rs Outdated
week,
month,
});
if optimize && result.is_ok() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if optimize && result.is_ok() {
if optimize && !json && result.is_ok() {

Comment thread crates/tokscale-cli/src/commands/optimize.rs Outdated
Comment thread crates/tokscale-cli/src/commands/optimize.rs Outdated
Comment thread crates/tokscale-cli/src/main.rs Outdated
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])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
format!("{}…", &m.model[..27])
format!("{}…", m.model.chars().take(27).collect::<String>())

Comment thread README.md Outdated
Tokscale can analyze your usage patterns and generate actionable recommendations to reduce costs and improve productivity.

```bash
# Standalone optimization analysis (defaults to today)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
# Standalone optimization analysis (defaults to today)
# Standalone optimization analysis (uses all cached sessions by default)

@leecoder
leecoder force-pushed the feat/report-task-grouping branch 3 times, most recently from 2cf54a9 to 36c4ce5 Compare May 29, 2026 09:47
leecoder added 5 commits June 1, 2026 11:58
- 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
@leecoder
leecoder force-pushed the feat/report-task-grouping branch from 36c4ce5 to d6c5432 Compare June 1, 2026 03:00

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread .opencode/skill/deploy.md Outdated
--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"]' \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@leecoder
leecoder force-pushed the feat/report-task-grouping branch 2 times, most recently from 7657640 to f00d277 Compare June 1, 2026 09:01
junhoyeo added 2 commits June 18, 2026 03:08
…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)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

junhoyeo added 3 commits June 18, 2026 03:28
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
@junhoyeo
junhoyeo merged commit 46f8fff into junhoyeo:main Jun 17, 2026
1 check passed
junhoyeo added a commit that referenced this pull request Jun 22, 2026
… 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
junhoyeo added a commit that referenced this pull request Jun 22, 2026
…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
pinion05 added a commit to pinion05/tokscale that referenced this pull request Jun 23, 2026
…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>
t1000040 pushed a commit to tmobi-internal/tokscale that referenced this pull request Jun 30, 2026
…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>
t1000040 pushed a commit to tmobi-internal/tokscale that referenced this pull request Jun 30, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants