@danypops/jittor 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,11 @@ SQLite runs in WAL mode with versioned migrations, JSON validation, bounded quer
35
35
 
36
36
  Operations currently include bounded metric recording/query/pruning, benchmark refresh/status/query, context assessment, routing control, telemetry polling, and service checkpointing. Every operation is exposed through the authenticated typed client; benchmark operations also have CLI parity.
37
37
 
38
- Provider adapters currently include official OpenRouter key/usage/model telemetry and an explicitly experimental Codex subscription adapter. The Codex adapter follows the pinned open-source CLI `/wham/usage` payload and `x-codex-*` response-header contracts, accepts additional metered limits, and fails closed on malformed windows or impossible percentages. File credentials must be explicitly configured and private (`0600`); Jittor reads only the access token and account ID, never refreshes credentials, and never logs or persists OAuth secrets. Anthropic has no personal-account polling endpoint (its Admin/Rate Limits API is documented as unavailable for individual accounts), so Jittor instead reads the official `anthropic-ratelimit-*` response headers Pi observes on every Messages API call and fails closed on schema drift the same way. Google Vertex AI has neither a personal polling endpoint nor a documented remaining-quota response header, so Jittor never fabricates a Vertex budget bar; it instead classifies Vertex's `google.rpc.Status` failure shape (quota, authentication, invalid-request, overload, transport, unknown) from Pi's bounded, content-free `errorMessage` and records only a bounded failure-count metric.
38
+ Provider adapters currently include official OpenRouter key/usage/model telemetry and an explicitly experimental Codex subscription adapter. The Codex adapter follows the pinned open-source CLI `/wham/usage` payload and `x-codex-*` response-header contracts, accepts additional metered limits, and fails closed on malformed windows or impossible percentages. File credentials must be explicitly configured and private (`0600`); Jittor reads only the access token and account ID, never refreshes credentials, and never logs or persists OAuth secrets. Anthropic has no personal-account polling endpoint (its Admin/Rate Limits API is documented as unavailable for individual accounts), so Jittor instead reads the official `anthropic-ratelimit-*` response headers Pi observes on every Messages API call and fails closed on schema drift the same way. Google Vertex AI has neither a personal polling endpoint nor a documented remaining-quota response header, so Jittor never fabricates a Vertex budget bar; it instead classifies Vertex's `google.rpc.Status` failure shape (quota, authentication, invalid-request, overload, transport, unknown) from Pi's bounded, content-free `errorMessage` and records only a bounded failure-count metric. Because no budget signal can ever exist for this provider, the footer's `budget` segment is omitted entirely for it rather than showing a permanent `?` placeholder that could never resolve; the `?` placeholder is reserved for providers that can report a budget but simply haven't yet (router not ready, or telemetry not observed on the first turn).
39
39
 
40
- The native Pi extension preflights input and every provider turn, applies model/thinking decisions, records response headers and finalized usage through the daemon, and blocks requests when required telemetry is unsafe. It follows Pi's current authenticated model/provider and synchronizes Pi's available models before every decision, so unavailable catalog routes are never selected. Its responsive integrated footer groups repository and model identity with cumulative usage, a color-coded context-window bar, and current-provider budget telemetry. Codex shows the active model's bounded quota as a draining remaining-budget bar with reset and freshness information. OpenRouter uses the same drain semantics when its official key telemetry exposes a configured limit and remaining balance; keys without a limit remain honest text-only spend and never receive a fabricated denominator. Anthropic shows the same drain semantics from its most-restrictive-in-effect token bucket, falling back to the request bucket when no token telemetry has been observed yet. During Pi compaction, the context bar drains against a learned median duration estimated from the last few completed compactions (bounded to the most recent 20 samples, requiring at least 3 before trusting it); until enough evidence exists it falls back to a fixed-rate cold-start heuristic and says so explicitly ("estimating") rather than claiming a time it does not know. Once learned, the status text reports both elapsed time and an approximate remaining time ("~Ns left"). A liveness dot blinks next to the bar once per render tick in either state so compaction never looks stalled. Run `jittor compaction estimate [--json]` to inspect the current estimate and its confidence directly. Unknown and stale telemetry are marked explicitly. Run `/jittor` for the consolidated Settings TUI (its default action), or `/jittor status` for detailed burn pressure, freshness, route state, and confirmed emergency-halt/override controls.
40
+ The third-party `anthropic-vertex` provider (Anthropic Claude models served through Google Vertex, e.g. via `@twogiants/pi-anthropic-vertex`) is tracked separately from both of the above: it reuses Pi's own Anthropic Messages stream implementation with Anthropic's official `@anthropic-ai/vertex-sdk` client, so its wire shape is Anthropic's, but its quota accounting is Google's. Jittor applies Google Vertex's failure classification to it (real-world reports confirm its 429s still carry GCP's own quota-exceeded shape even through Anthropic's own SDK) and, best-effort, also checks for genuine Anthropic rate-limit response headers on it, since it is unverified whether this specific passthrough ever forwards them. Either way, every metric is tagged `anthropic-vertex`, never blended into direct Anthropic's `anthropic` source or Pi's unrelated native `google-vertex` provider, since each represents a different account/quota pool. Its footer budget (labeled `vtok`/`vreq` when headers are observed) stays `null` (may still resolve) rather than `undefined` (provably impossible) until it's confirmed one way or the other.
41
+
42
+ The native Pi extension preflights input and every provider turn, applies model/thinking decisions, records response headers and finalized usage through the daemon, and blocks requests when required telemetry is unsafe. It follows Pi's current authenticated model/provider and synchronizes Pi's available models before every decision, so unavailable catalog routes are never selected. Its responsive integrated footer groups repository and model identity with cumulative usage, a color-coded context-window bar, and current-provider budget telemetry. Codex shows the active model's bounded quota as a draining remaining-budget bar with reset and freshness information. OpenRouter uses the same drain semantics when its official key telemetry exposes a configured limit and remaining balance; keys without a limit remain honest text-only spend and never receive a fabricated denominator. Anthropic shows the same drain semantics from its most-restrictive-in-effect token bucket, falling back to the request bucket when no token telemetry has been observed yet. During Pi compaction, the context bar drains against a learned median duration estimated from the last few completed compactions (bounded to the most recent 20 samples, requiring at least 3 before trusting it), in exact sync with a countdown ("compact ~Ns left") — never a count-up, never a fabricated total. Until enough evidence exists, the bar does not drain at all (there is no real rate to drain against) and no timer text is shown; the bar itself simply blinks in place at its starting fill once per render tick so compaction never looks stalled without claiming knowledge it doesn't have. Run `jittor compaction estimate [--json]` to inspect the current estimate and its confidence directly. Unknown and stale telemetry are marked explicitly. Run `/jittor` for the consolidated Settings TUI (its default action), or `/jittor status` for detailed burn pressure, freshness, route state, and confirmed emergency-halt/override controls.
41
43
 
42
44
  Jittor currently registers no model-callable native tools, so Pi's native model `content` versus renderer `details` contract is explicitly not applicable. Daemon JSON, CLI `--json`, human CLI output, command notifications, panels, and the footer remain separate bounded channels. See [`docs/OUTPUT_CHANNELS.md`](docs/OUTPUT_CHANNELS.md) for the conformance matrix and the requirements that apply if a native tool is introduced later.
43
45
 
@@ -66,6 +68,12 @@ Jittor observes finalized Codex assistant errors through Pi's public message lif
66
68
 
67
69
  `/usage` is its own top-level command, separate from `/jittor`. Bare `/usage` opens a colored Unicode cumulative graph with X/Y axes, per-provider/model series, and explicit **Hourly**, **Daily**, **Weekly**, **Monthly**, and **Quarterly** periods; `/usage cost` opens the same graph showing aggregated USD spend instead of tokens, reusing the `cost` metric already recorded content-free on every finalized Pi assistant message (no new instrumentation). Left/Right or Tab/Shift+Tab changes the time frame, `v` toggles between the token and cost views, and `r` refreshes.
68
70
 
71
+ The graph fetches metrics per distinct provider/model scope (`jittor metrics distinct-scopes`, bounded to 40 scopes, 250 rows each) rather than one flat "most recent rows" query. A flat query lets one heavy, long-running session monopolize the entire row budget with its own most recent activity, silently hiding every other provider from the chart no matter which time frame is selected, since the query would never reach back far enough in time to see anything else. Fetching per scope guarantees every active provider/model gets its own fair share of the query budget instead.
72
+
73
+ ### Cost per Papyrus task
74
+
75
+ Jittor observes Papyrus's task-focus lifecycle in real time over a shared Pi extension event bus (`papyrus.task-focus.v1`) -- Papyrus never depends on Jittor, it only broadcasts which task is currently focused. Every token/cost metric Jittor already records on a finalized Pi assistant message is tagged with the currently focused task's id the moment it is recorded (no time-window estimation, no new instrumentation). A paused or cleared focus stops tagging; spend recorded with nothing focused is reported separately as unattributed, never dropped or folded into an invented task. Run `jittor metrics cost-by-task --since <ms> --until <ms> [--json]` for a bounded per-task breakdown of cost and input/output/cache tokens.
76
+
69
77
  Series are colored with a categorical palette chosen to avoid this UI's own status colors ("success"/"warning"/"error" already mean something specific elsewhere in this panel, so reusing them for arbitrary model identity would make a model's bar segment look like a warning or a failure) and instead reuses the theme's syntax-highlighting roles, which are already tuned by theme authors to stay mutually distinguishable on screen — the same design problem as a categorical data palette. Once more series are active than there are hues, a series reuses a hue in bold rather than repeating an indistinguishable color. Multiple models active within the same cumulative time frame are rendered as one bar stacked by color, not separate bars.
70
78
 
71
79
  Token-budget thresholds are optional and must be configured by the user; Jittor never infers a token allowance from Codex or another provider's subscription percentage. Configure or clear one period with `/usage budget <hourly|daily|weekly|monthly|quarterly> <positive-tokens|off>`, and inspect all of them with `/usage budget`. A configured budget appears as a horizontal threshold on the cumulative graph with explicit remaining or **OVER BUDGET** state; the cost view does not yet support a budget threshold. These private settings persist in `$XDG_CONFIG_HOME/jittor/extension.json` (or `~/.config/jittor/extension.json`).
@@ -84,9 +92,9 @@ jittor benchmarks list --source openrouter-models [--model provider/model] [--di
84
92
 
85
93
  Only complete snapshots are queryable. Query output reports both completeness and freshness. See [`docs/BENCHMARK_SOURCES.md`](docs/BENCHMARK_SOURCES.md) for source authority, provenance, conflict, and redistribution rules.
86
94
 
87
- Jittor separately records content-free local model observations from Pi's public lifecycle: TTFT, wall latency, output throughput, token/cache/cost efficiency, provider retries, tool-loop counts, failures, and task class derived only from bounded tool names. Prompts, responses, tool arguments/results, credentials, and project paths are never retained. `/jittor outcome accepted` or `/jittor outcome rejected` attaches explicit outcome evidence to the latest completed local run; runtime completion alone is not treated as quality success. Robust aggregates report sample size, median, p90, median absolute deviation, recency, and confidence without merging local observations into external benchmark facts.
95
+ Jittor separately records content-free local model observations from Pi's public lifecycle: TTFT, wall latency, output throughput, token/cache/cost efficiency, provider retries, tool-loop counts, failures, and two independent classifications derived only from bounded tool names: domain (subject matter, e.g. `coding`) and type (activity, e.g. `research`, `planning`) -- a run can be domain=coding and type=research at once. Prompts, responses, tool arguments/results, credentials, and project paths are never retained. `/jittor outcome accepted` or `/jittor outcome rejected` attaches explicit outcome evidence to the latest completed local run; runtime completion alone is not treated as quality success. Robust aggregates report sample size, median, p90, median absolute deviation, recency, and confidence without merging local observations into external benchmark facts.
88
96
 
89
- The ranking operation accepts an explicit bounded candidate set and never adds identities found only in evidence. It scores task quality, cost, latency, context, and local reliability with bounded user weights, budget-pressure adjustment, component confidence, freshness, provenance, and deterministic tie-breaking. Missing evidence remains unknown and lowers confidence. Run `/jittor benchmarks [coding|research|planning|general]` for the responsive recommendation panel. Because the released Pi extension API does not expose the exact `/scoped-models` set, the current adapter labels candidates `available-models`; the panel says **ADVISORY** and offers no selection action. Automatic route ordering is allowed only for `exact-session` authority and then narrows/reorders routes already present in the supplied candidate set.
97
+ The ranking operation accepts an explicit bounded candidate set and never adds identities found only in evidence. It scores quality (both a domain-specific dimension, e.g. `quality-coding`, and a type-specific dimension, e.g. `quality-type-planning`, each optional and additive over the universal `quality-general` fallback), cost, latency, context, and local reliability with bounded user weights, budget-pressure adjustment, component confidence, freshness, provenance, and deterministic tie-breaking. Missing evidence remains unknown and lowers confidence. Run `/jittor benchmarks [coding|general] [research|planning|general]` (either order, either or both omitted) for the responsive recommendation panel. Because the released Pi extension API does not expose the exact `/scoped-models` set, the current adapter labels candidates `available-models`; the panel says **ADVISORY** and offers no selection action. Automatic route ordering is allowed only for `exact-session` authority and then narrows/reorders routes already present in the supplied candidate set.
90
98
 
91
99
  ### Context pressure
92
100
 
@@ -102,6 +110,9 @@ Every daemon operation is reachable from the CLI through the authenticated typed
102
110
  jittor metrics record --source <s> --scope <s> --metric <s> --value <number|null> --unit <unit> [--observed-at <ms>] [--attributes <json>] [--json]
103
111
  jittor metrics query [--source <s>] [--scope <s>] [--metric <s>] [--since <ms>] [--until <ms>] [--limit <n>] [--order asc|desc] [--json]
104
112
  jittor metrics prune --before <ms> [--json]
113
+ jittor metrics distinct-scopes --source <s> --since <ms> --until <ms> [--limit 1..40] [--json]
114
+ jittor metrics cost-by-task --since <ms> --until <ms> [--json]
115
+ jittor metrics prune --before <ms> [--force] [--json] # force required if before is newer than 24h ago
105
116
  jittor service checkpoint [--json]
106
117
  jittor telemetry poll [--json]
107
118
  jittor compaction estimate [--json]
@@ -183,7 +183,20 @@ anthropic-priority-output-tokens-limit / -remaining / -reset (Priority Tier onl
183
183
 
184
184
  Unlike Anthropic and OpenRouter, Vertex does not document a per-response rate-limit or remaining-quota header for `generateContent`/Messages-compatible calls. Quota is configured and reported at the Google Cloud project/region level (Service Usage / Quota APIs, Cloud Console "Quotas & System Limits"), which is an account-configuration surface, not a response header Jittor could read before a request is throttled — the same class of limitation Amazon Bedrock has (see Papyrus doc `jittor-provider-survey-which-additional-apis-to-support-0wma`, Tier 2). Failures instead surface as a `google.rpc.Status` shape, `{error: {code, message, status, details[]}}`, with `status` one of the canonical gRPC codes (`RESOURCE_EXHAUSTED`, `PERMISSION_DENIED`, `UNAUTHENTICATED`, `UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INVALID_ARGUMENT`, ...), sometimes with a `google.rpc.RetryInfo.retryDelay` or `google.rpc.QuotaFailure` detail.
185
185
 
186
- Jittor therefore does not fabricate a remaining-budget bar for Vertex. It classifies the bounded, content-free `errorMessage` string Pi already exposes for every provider (the same source `classifyCodexFailure` reads) into a failure kind and transience, and records only a bounded failure-count metric (`source: "google-vertex", scope: "failure", metric: <kind>, unit: "count"`) — never a `ratio` metric implying a known remaining fraction. This is an honest degradation: Jittor surfaces *that* and *what kind of* capacity/auth/request pressure Pi is seeing, without claiming to know how much budget remains.
186
+ Jittor therefore does not fabricate a remaining-budget bar for Vertex from the response path alone. It classifies the bounded, content-free `errorMessage` string Pi already exposes for every provider (the same source `classifyCodexFailure` reads) into a failure kind and transience, and records only a bounded failure-count metric (`source: "google-vertex", scope: "failure", metric: <kind>, unit: "count"`) — never a `ratio` metric implying a known remaining fraction. This is an honest degradation: Jittor surfaces *that* and *what kind of* capacity/auth/request pressure Pi is seeing, without claiming to know how much budget remains.
187
+
188
+ ### A real hot(ish)-path signal exists once quota moves to individual per-user GCP projects
189
+
190
+ The "no telemetry" conclusion above is specific to a *shared* GCP project, where Google has nothing to key a per-user signal on. Several organizations are migrating Vertex/Claude-on-Vertex access from one shared project to one GCP project per individual user specifically so usage/cost can be attributed per person — and that migration changes the honest answer, because Cloud Billing budgets support **project-scoped access** without any Cloud Billing account IAM role: `resourcemanager.projects.get` + `billing.resourcebudgets.read`/`billing.resourceCosts.get` on the project alone are enough (verified against `docs.cloud.google.com/billing/docs/how-to/budget-api-access-control` and `.../billing/docs/how-to/budgets`, fetched 2026-07-23) — matching a "passwordless/keyless", ADC-only individual-project auth model with no static service-account key.
191
+
192
+ Two API surfaces matter here, and they answer different questions:
193
+
194
+ - **`billingAccounts.budgets.get`/`.list`** (REST, project-scoped per above) returns only the budget's *policy* — its cap (`amount`), alert `thresholdRules`, and filters. It does **not** return current spend. Resolving which billing account owns a given project first requires `projects.getBillingInfo` (`cloudbilling.googleapis.com`, also just `resourcemanager.projects.get`/Project Viewer).
195
+ - **Cloud Billing's own programmatic budget notifications**, delivered over Pub/Sub, are the real signal: Google's docs state notifications are "sent to the Pub/Sub topic **multiple times per day** with the current status of your budget" (not only on threshold crossings), each message carrying real dollar figures — `costAmount`, `budgetAmount`, `costIntervalStart`, `currencyCode`, and (only once actually exceeded) `alertThresholdExceeded`/`forecastThresholdExceeded` (schema verified against `docs.cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification-format` and the worked fixture in `.../billing/docs/how-to/listen-to-notifications`, fetched 2026-07-23). Since Jittor is a local, loopback-only daemon with no public inbound endpoint, it **pulls** (never pushes) this topic via a Pub/Sub pull subscription (`pubsub.googleapis.com/v1/{subscription}:pull`), authenticated the same ADC way.
196
+
197
+ This is still not a fully real-time signal, and the docs are explicit about both caveats Jittor must preserve rather than paper over: (1) "Budgets use estimated Cloud Billing data which is subject to change until your invoice is finalized", and (2) "Pub/Sub only provides at-least-once delivery. You might receive a message multiple times, and messages might arrive out of order." Jittor's `GoogleVertexBudgetTelemetryAdapter` (`src/providers/google-vertex-budget.ts`) selects the freshest pulled message by its own Pub/Sub `publishTime`, fails closed (throws) on any message that doesn't match the documented schema, and reports this signal at a lower confidence (`0.6`, vs. `0.8` for Codex's header-derived windows) than a per-response header would earn. `spend`/`cap` are recorded as real USD metrics and their `spend-fraction` ratio is left unclamped (so a genuine over-cap soft-quota period stays visible); only the policy-facing `BudgetWindow.usedFraction` is clamped to `1.0`, since a known-to-be-≥100% real number floored to the window's documented `[0,1]` invariant is not the same thing as fabricating one from nothing.
198
+
199
+ Setting this up requires one-time configuration outside Jittor for each individual project: create a Pub/Sub topic, connect it to the project's budget (Project Owner/Editor role, plus Pub/Sub Admin on whichever project holds the topic), and create a pull subscription on it. Jittor only consumes the subscription; it does not provision any of this GCP-side configuration itself.
187
200
 
188
201
  ## Normalized Jittor model
189
202
 
@@ -267,7 +280,13 @@ Required safeguards: hysteresis, cooldown, maximum delay, minimum telemetry fres
267
280
  ### Google Vertex AI
268
281
 
269
282
  - Google Cloud/Gemini API 429 `RESOURCE_EXHAUSTED` error reports and `google.rpc.Status`/`QuotaFailure`/`RetryInfo` detail shapes, cross-checked across multiple live incident reports (fetched 2026-07-21); no official Vertex response header for remaining quota was found
270
- - Papyrus doc `jittor-provider-survey-which-additional-apis-to-support-0wma`, Tier 2 (Amazon Bedrock entry documents the same account-level-quota-not-header pattern)
283
+ - Amazon Bedrock documents the same account-level-quota-not-header pattern as a cross-check on the general "cloud-vendor AI quota lives at the account/project layer, not a response header" shape
284
+ - https://docs.cloud.google.com/billing/docs/how-to/budget-api-access-control (project-scoped `GetBudget`/`ListBudgets` permissions, fetched 2026-07-23)
285
+ - https://docs.cloud.google.com/billing/docs/how-to/budgets ("Project-scoped billing permissions" / single-project access section, fetched 2026-07-23)
286
+ - https://docs.cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo (fetched 2026-07-23)
287
+ - https://docs.cloud.google.com/billing/docs/how-to/budgets-programmatic-notifications#notification-format (fetched 2026-07-23)
288
+ - https://docs.cloud.google.com/billing/docs/how-to/listen-to-notifications (worked notification fixture used verbatim in `test/google-vertex-budget.test.ts`, fetched 2026-07-23)
289
+ - https://docs.cloud.google.com/billing/docs/reference/budget/rest/v1/billingAccounts.budgets (Budget resource schema — confirms no current-spend field, fetched 2026-07-23)
271
290
 
272
291
  ### Pi
273
292
 
@@ -9,7 +9,7 @@ import {
9
9
  MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
10
10
  MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
11
11
  } from "../../src/constants.ts";
12
- import type { ModelTaskClass } from "../../src/domain/model-observation.ts";
12
+ import type { ModelTaskDomain, ModelTaskType } from "../../src/domain/model-observation.ts";
13
13
  import type { ModelCandidate, ModelRankingResult, RankedModel, UtilityComponentName } from "../../src/domain/model-ranking.ts";
14
14
 
15
15
  export interface BenchmarkPanelClient {
@@ -52,7 +52,7 @@ export function renderBenchmarkView(result: ModelRankingResult, currentIdentity:
52
52
  theme.fg("borderMuted", "─".repeat(safeWidth)),
53
53
  theme.bold("Jittor Benchmark Recommendations"),
54
54
  result.scopeAuthority === "exact-session" ? "Scope: exact session" : "Scope: available models · ADVISORY (exact session scope unavailable)",
55
- `Task: ${result.taskClass} · evidence ${result.completeness}`,
55
+ `Domain: ${result.domain} · Type: ${result.type} · evidence ${result.completeness}`,
56
56
  reason,
57
57
  ...shown.flatMap((item, index) => candidateLines(item, index, currentIdentity)),
58
58
  ...(result.ranked.length > shown.length ? [` … ${result.ranked.length - shown.length} more candidates omitted`] : []),
@@ -68,13 +68,15 @@ export async function showBenchmarkPanel(
68
68
  client: BenchmarkPanelClient,
69
69
  candidates: ModelCandidate[],
70
70
  currentIdentity: string,
71
- taskClass: ModelTaskClass,
71
+ domain: ModelTaskDomain,
72
+ type: ModelTaskType,
72
73
  ): Promise<void> {
73
74
  for (;;) {
74
75
  const result = await client.call("models.rank", {
75
76
  candidates,
76
77
  scopeAuthority: "available-models",
77
- taskClass,
78
+ domain,
79
+ type,
78
80
  budgetPressure: 0,
79
81
  weights: {
80
82
  quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
@@ -7,7 +7,6 @@ import {
7
7
  FOOTER_CONTEXT_ACCENT_FRACTION,
8
8
  FOOTER_CONTEXT_ERROR_FRACTION,
9
9
  FOOTER_COMPACTION_BLINK_HALF_PERIOD_MS,
10
- FOOTER_COMPACTION_DRAIN_STEP_MS,
11
10
  FOOTER_CONTEXT_WARNING_FRACTION,
12
11
  FOOTER_WIDE_TERMINAL_WIDTH,
13
12
  MILLISECONDS_PER_DAY,
@@ -140,19 +139,18 @@ function fillColor(fraction: number | null): FooterColor {
140
139
  /**
141
140
  * Once a learned median duration is available (see estimateCompactionDuration / the
142
141
  * `compaction.estimate` daemon operation), the bar drains against that real estimate: fraction
143
- * counts down linearly from 1 to 0 over estimatedMs. Until then cold start, or the estimate
144
- * fetch has not resolved yet it falls back to draining from the context fill at a fixed rate,
145
- * which is a liveness heuristic only and never labeled as a time estimate (see
146
- * compactionStatusText, which is what actually communicates cold-start vs learned confidence).
142
+ * counts down linearly from 1 to 0 over estimatedMs, exactly in step with the countdown shown in
143
+ * compactionStatusTextsame elapsed/estimatedMs ratio drives both. Until then cold start, or
144
+ * the estimate fetch has not resolved yet there is no real duration to drain against, so the
145
+ * fill holds steady at the fraction observed when compaction started; the blink alone (see
146
+ * compactionBarGlyph) communicates liveness without fabricating a rate.
147
147
  */
148
148
  function compactionFraction(progress: CompactionProgress, width: number, now: number): number {
149
149
  if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
150
150
  const elapsed = Math.max(0, now - progress.startedAt);
151
151
  return Math.max(0, Math.min(1, 1 - (elapsed / progress.estimatedMs)));
152
152
  }
153
- const initialFilled = Math.round(Math.min(1, Math.max(0, progress.initialFraction)) * width);
154
- const drained = Math.floor(Math.max(0, now - progress.startedAt) / FOOTER_COMPACTION_DRAIN_STEP_MS);
155
- return Math.max(0, initialFilled - drained) / width;
153
+ return Math.min(1, Math.max(0, progress.initialFraction));
156
154
  }
157
155
 
158
156
  /**
@@ -166,18 +164,30 @@ export function compactionBlinkOn(startedAt: number, now: number, halfPeriodMs =
166
164
  return Math.floor(elapsed / halfPeriodMs) % 2 === 0;
167
165
  }
168
166
 
169
- function compactionBlinkGlyph(progress: CompactionProgress, now: number): string {
170
- return compactionBlinkOn(progress.startedAt, now) ? "●" : "○";
167
+ /**
168
+ * The compaction signal lives in the bar itself: it blinks between its normal draining fill and a
169
+ * blank track of the same width, rather than a separate indicator glyph next to it. Off-phase
170
+ * intentionally renders identically to the "no data" empty track (dim, all "░") so the bar reads
171
+ * as a single blinking element, not a bar plus a decoration.
172
+ */
173
+ function compactionBarGlyph(progress: CompactionProgress, theme: FooterTheme, width: number, now: number): string {
174
+ if (!compactionBlinkOn(progress.startedAt, now)) return theme.fg("dim", "░".repeat(width));
175
+ const fraction = compactionFraction(progress, width, now);
176
+ return theme.fg("accent", progressBar(fraction, width));
171
177
  }
172
178
 
173
- /** Communicates cold-start uncertainty distinctly from a learned approximate completion time. */
174
- function compactionStatusText(progress: CompactionProgress, now: number): string {
175
- const elapsedSeconds = Math.floor(Math.max(0, now - progress.startedAt) / MILLISECONDS_PER_SECOND);
179
+ /**
180
+ * A countdown, never a count-up: once a learned estimate exists it reports seconds remaining,
181
+ * ticking down toward zero in step with the draining bar. Before that (cold start, no estimate
182
+ * yet) there is nothing true to count down from, so this reports nothing at all rather than a
183
+ * fabricated elapsed count or a guessed total — the blinking, non-draining bar is the only signal.
184
+ */
185
+ function compactionStatusText(progress: CompactionProgress, now: number): string | undefined {
176
186
  if (progress.confidence === "learned" && typeof progress.estimatedMs === "number" && progress.estimatedMs > 0) {
177
187
  const remainingSeconds = Math.max(0, Math.ceil((progress.estimatedMs - (now - progress.startedAt)) / MILLISECONDS_PER_SECOND));
178
- return `compact ${elapsedSeconds}s (~${remainingSeconds}s left)`;
188
+ return `compact ~${remainingSeconds}s left`;
179
189
  }
180
- return `compact ${elapsedSeconds}s (estimating)`;
190
+ return undefined;
181
191
  }
182
192
 
183
193
  function contextSegment(
@@ -190,8 +200,9 @@ function contextSegment(
190
200
  ): string {
191
201
  const w = barWidth(width);
192
202
  if (compaction) {
193
- const fraction = compactionFraction(compaction, w, now);
194
- return `ctx ${theme.fg("accent", progressBar(fraction, w))} ${compactionBlinkGlyph(compaction, now)} ${compactionStatusText(compaction, now)}`;
203
+ const bar = compactionBarGlyph(compaction, theme, w, now);
204
+ const statusText = compactionStatusText(compaction, now);
205
+ return statusText === undefined ? `ctx ${bar}` : `ctx ${bar} ${statusText}`;
195
206
  }
196
207
  const usage = context.getContextUsage();
197
208
  const window = usage?.contextWindow ?? context.model?.contextWindow ?? 0;
@@ -211,8 +222,7 @@ function minimalContextSegment(
211
222
  ): string {
212
223
  const w = barWidth(width);
213
224
  if (compaction) {
214
- const fraction = compactionFraction(compaction, w, now);
215
- return `ctx ${theme.fg("accent", progressBar(fraction, w))} ${compactionBlinkGlyph(compaction, now)}`;
225
+ return `ctx ${compactionBarGlyph(compaction, theme, w, now)}`;
216
226
  }
217
227
  const percent = context.getContextUsage()?.percent;
218
228
  const fraction = percent === null || percent === undefined ? null : percent / 100;
@@ -228,7 +238,13 @@ function resetLabel(resetsAt: number | undefined, now: number): string | undefin
228
238
  return `resets in ${Math.max(1, Math.ceil(remaining / MILLISECONDS_PER_MINUTE))}m`;
229
239
  }
230
240
 
231
- function budgetSegment(budget: ProviderBudget | null, theme: FooterTheme, width: number, compact: boolean, now: number): string {
241
+ /**
242
+ * `undefined` means no budget signal is possible for this provider at all (see buildFooterBudget);
243
+ * the segment is omitted entirely rather than showing a placeholder that could never resolve.
244
+ * `null` means not known yet but might resolve, which still earns the `?` placeholder.
245
+ */
246
+ function budgetSegment(budget: ProviderBudget | null | undefined, theme: FooterTheme, width: number, compact: boolean, now: number): string | undefined {
247
+ if (budget === undefined) return undefined;
232
248
  const w = barWidth(width);
233
249
  if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
234
250
  const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
@@ -288,7 +304,7 @@ export function renderFooterLines(
288
304
  context: FooterContext,
289
305
  footerData: FooterData,
290
306
  theme: FooterTheme,
291
- providerBudget: ProviderBudget | null,
307
+ providerBudget: ProviderBudget | null | undefined,
292
308
  thinkingLevel: string,
293
309
  width: number,
294
310
  now = Date.now(),
@@ -325,7 +341,7 @@ export function renderFooterLines(
325
341
  }
326
342
 
327
343
  export interface IntegratedFooterState {
328
- providerBudget: ProviderBudget | null;
344
+ providerBudget: ProviderBudget | null | undefined;
329
345
  compaction?: CompactionProgress;
330
346
  requestRender?: () => void;
331
347
  }
@@ -10,12 +10,14 @@ import {
10
10
  MILLISECONDS_PER_MINUTE,
11
11
  MILLISECONDS_PER_SECOND,
12
12
  PAPYRUS_CONTEXT_INJECTION_CHANNEL,
13
+ PAPYRUS_TASK_FOCUS_CHANNEL,
13
14
  CONTEXT_EVENT_DEDUP_LIMIT,
14
15
  } from "../../src/constants.ts";
15
16
  import { CodexRecoveryPolicy, classifyCodexFailure, type CodexFailureKind, type CodexFailureMetadata } from "../../src/domain/codex-recovery.ts";
16
17
  import { CompactionTelemetry, papyrusContextMetric, validatePapyrusContextInjection } from "../../src/domain/context-telemetry.ts";
18
+ import { applyTaskFocusEvent, validateTaskFocusEvent } from "../../src/domain/task-focus.ts";
17
19
  import type { MetricObservation, StoredMetricObservation } from "../../src/domain/metric.ts";
18
- import { classifyTaskFromTools, modelRunMetrics, TASK_CLASSES, type ModelRunObservation, type ModelTaskClass } from "../../src/domain/model-observation.ts";
20
+ import { classifyTaskFromTools, modelRunMetrics, TASK_DOMAINS, TASK_TYPES, type ModelRunObservation, type ModelTaskDomain, type ModelTaskType } from "../../src/domain/model-observation.ts";
19
21
  import type { ModelCandidate } from "../../src/domain/model-ranking.ts";
20
22
  import { USAGE_PERIODS, type UsagePeriod } from "../../src/domain/usage.ts";
21
23
  import type { PolicyDecision, Route } from "../../src/policy.ts";
@@ -228,7 +230,8 @@ async function applyDecision(
228
230
  return halt(ctx, `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`);
229
231
  }
230
232
 
231
- function assistantUsageMetrics(message: unknown, observedAt: number): MetricObservation[] {
233
+ /** taskId, when a Papyrus task is currently focused in this session, tags the metric for real-time cost-per-task correlation without any new instrumentation surface. */
234
+ function assistantUsageMetrics(message: unknown, observedAt: number, taskId: string | null = null): MetricObservation[] {
232
235
  if (typeof message !== "object" || message === null || Array.isArray(message)) return [];
233
236
  const value = message as Record<string, unknown>;
234
237
  if (value["role"] !== "assistant" || typeof value["usage"] !== "object" || value["usage"] === null) return [];
@@ -236,7 +239,7 @@ function assistantUsageMetrics(message: unknown, observedAt: number): MetricObse
236
239
  const provider = typeof value["provider"] === "string" ? value["provider"] : "unknown";
237
240
  const model = typeof value["model"] === "string" ? value["model"] : "unknown";
238
241
  const scope = `${provider}:${model}`;
239
- const attributes = { provider, model };
242
+ const attributes = { provider, model, ...(taskId === null ? {} : { taskId }) };
240
243
  const metrics: MetricObservation[] = [];
241
244
  for (const [field, metric] of [["input", "input-tokens"], ["output", "output-tokens"], ["cacheRead", "cache-read-tokens"], ["cacheWrite", "cache-write-tokens"]] as const) {
242
245
  const amount = usage[field];
@@ -274,6 +277,21 @@ export function registerJittorExtension(
274
277
  // Reject malformed or stale cross-extension observations without retaining payloads.
275
278
  }
276
279
  });
280
+ // Real-time cost-per-task correlation: Jittor observes Papyrus's task-focus broadcasts (Papyrus
281
+ // never depends on Jittor) and tags newly recorded token/cost metrics with the currently focused
282
+ // task id. Scoped to this Pi session: a focus change in a different concurrent session must not
283
+ // affect this one's attribution.
284
+ let currentSessionId: string | undefined;
285
+ let focusedTaskId: string | null = null;
286
+ const stopPapyrusTaskFocus = pi.events?.on?.(PAPYRUS_TASK_FOCUS_CHANNEL, (payload) => {
287
+ try {
288
+ const event = validateTaskFocusEvent(payload);
289
+ if (event.sessionId !== undefined && event.sessionId !== currentSessionId) return;
290
+ focusedTaskId = applyTaskFocusEvent(event);
291
+ } catch {
292
+ // Reject malformed or stale cross-extension events without retaining payloads or crashing the extension.
293
+ }
294
+ });
277
295
  const recoveryPolicy = new CodexRecoveryPolicy({
278
296
  baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
279
297
  maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
@@ -285,6 +303,11 @@ export function registerJittorExtension(
285
303
  let recoveryCooldown: { until: number; attempt: number; failureKind: CodexFailureKind } | undefined;
286
304
  let lastCodexResponse: CodexFailureMetadata = {};
287
305
  let lastGoogleVertexResponse: GoogleVertexFailureMetadata = {};
306
+ // The third-party "anthropic-vertex" provider (Anthropic Claude via Google Vertex) is tracked
307
+ // separately from "google-vertex" (Pi's own, unrelated native Vertex provider): different code
308
+ // path, different account/quota pool, and its metrics must stay distinguishable -- see
309
+ // google-vertex-contracts.ts and anthropic-contracts.ts.
310
+ let lastAnthropicVertexResponse: GoogleVertexFailureMetadata = {};
288
311
  const cancelRecovery = (resetPolicy: boolean): void => {
289
312
  if (recoveryTimer !== undefined) recoveryRuntime.clearTimeout(recoveryTimer);
290
313
  recoveryTimer = undefined;
@@ -417,13 +440,25 @@ export function registerJittorExtension(
417
440
  ctx.ui.notify("No active Pi model is available for benchmark recommendations.", "warning");
418
441
  return;
419
442
  }
420
- const requestedTask = action.split(/\s+/)[1] ?? "general";
421
- if (!TASK_CLASSES.includes(requestedTask as ModelTaskClass)) {
422
- ctx.ui.notify("Usage: /jittor benchmarks [coding|research|planning|general]", "warning");
443
+ // Domain (subject matter, e.g. coding) and type (activity, e.g. research/planning) are
444
+ // independent axes -- each positional word is classified against whichever axis it
445
+ // belongs to, in either order, so "/jittor benchmarks coding research" and
446
+ // "/jittor benchmarks research coding" both work; an unmatched word is a usage error.
447
+ const requested = action.split(/\s+/).slice(1);
448
+ let requestedDomain: ModelTaskDomain | undefined;
449
+ let requestedType: ModelTaskType | undefined;
450
+ let malformed = requested.length > 2;
451
+ for (const word of requested) {
452
+ if (TASK_DOMAINS.includes(word as ModelTaskDomain) && requestedDomain === undefined) requestedDomain = word as ModelTaskDomain;
453
+ else if (TASK_TYPES.includes(word as ModelTaskType) && requestedType === undefined) requestedType = word as ModelTaskType;
454
+ else malformed = true;
455
+ }
456
+ if (malformed) {
457
+ ctx.ui.notify("Usage: /jittor benchmarks [coding|general] [research|planning|general]", "warning");
423
458
  return;
424
459
  }
425
460
  const candidates = benchmarkCandidatesFromPi(ctx.modelRegistry.getAvailable() as PiRouteModel[], pi.getThinkingLevel());
426
- await showBenchmarkPanel(ctx, client, candidates, `${ctx.model.provider}/${ctx.model.id}`, requestedTask as ModelTaskClass);
461
+ await showBenchmarkPanel(ctx, client, candidates, `${ctx.model.provider}/${ctx.model.id}`, requestedDomain ?? "general", requestedType ?? "general");
427
462
  return;
428
463
  }
429
464
  if (action === "outcome accepted" || action === "outcome rejected") {
@@ -534,6 +569,8 @@ export function registerJittorExtension(
534
569
  });
535
570
 
536
571
  pi.on("session_start", async (_event, ctx) => {
572
+ currentSessionId = ctx.sessionManager.getSessionId();
573
+ focusedTaskId = null;
537
574
  finishCompactionUi();
538
575
  compactionTelemetry = new CompactionTelemetry();
539
576
  activeLocalRun = undefined;
@@ -541,6 +578,7 @@ export function registerJittorExtension(
541
578
  cancelRecovery(true);
542
579
  lastCodexResponse = {};
543
580
  lastGoogleVertexResponse = {};
581
+ lastAnthropicVertexResponse = {};
544
582
  ctx.ui.setStatus("jittor", undefined);
545
583
  showFooter(ctx);
546
584
  try {
@@ -614,9 +652,11 @@ export function registerJittorExtension(
614
652
  });
615
653
 
616
654
  pi.on("turn_start", async (event, ctx) => {
655
+ currentSessionId = ctx.sessionManager.getSessionId();
617
656
  compactionTelemetry.observeTurn();
618
657
  lastCodexResponse = {};
619
658
  lastGoogleVertexResponse = {};
659
+ lastAnthropicVertexResponse = {};
620
660
  activeLocalRun = {
621
661
  runId: `local-${event.timestamp}-${++localRunSequence}`,
622
662
  startedAt: event.timestamp,
@@ -664,6 +704,22 @@ export function registerJittorExtension(
664
704
  }
665
705
  }
666
706
  }
707
+ if (ctx.model?.provider === "anthropic-vertex") {
708
+ // Best-effort only: unverified whether this passthrough ever forwards Anthropic's own
709
+ // rate-limit headers. If it doesn't, hasAnthropicRateLimitHeaders is false and nothing is
710
+ // recorded -- the same honest default as every other unconfirmed signal in this file.
711
+ const headers = new Headers(event.headers);
712
+ if (hasAnthropicRateLimitHeaders(headers)) {
713
+ try {
714
+ await recordMetrics(client, parseAnthropicRateLimitHeaders(headers, Date.now(), "anthropic-vertex").metrics);
715
+ } catch {
716
+ if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected Anthropic-on-Vertex telemetry schema drift. ${RECOVERY_GUIDANCE}.`, "error");
717
+ }
718
+ }
719
+ // Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
720
+ // same failure classification as google-vertex applies -- see google-vertex-contracts.ts.
721
+ lastAnthropicVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
722
+ }
667
723
  if (ctx.model?.provider === "google-vertex") {
668
724
  lastGoogleVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
669
725
  }
@@ -697,7 +753,7 @@ export function registerJittorExtension(
697
753
  provider: value["provider"],
698
754
  model: value["model"],
699
755
  thinking: pi.getThinkingLevel(),
700
- taskClass: classifyTaskFromTools(active.toolNames),
756
+ ...classifyTaskFromTools(active.toolNames),
701
757
  startedAt: active.startedAt,
702
758
  firstTokenAt: active.firstTokenAt,
703
759
  completedAt,
@@ -733,7 +789,14 @@ export function registerJittorExtension(
733
789
  }
734
790
  lastGoogleVertexResponse = {};
735
791
  }
736
- const metrics = assistantUsageMetrics(event.message, Date.now());
792
+ if (event.message.role === "assistant" && event.message.provider === "anthropic-vertex") {
793
+ if (event.message.stopReason === "error") {
794
+ const failure = classifyGoogleVertexFailure(event.message.errorMessage, lastAnthropicVertexResponse);
795
+ await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now(), "anthropic-vertex")).catch(() => undefined);
796
+ }
797
+ lastAnthropicVertexResponse = {};
798
+ }
799
+ const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId);
737
800
  if (metrics.length > 0) {
738
801
  const amount = (name: string): number => metrics.filter((metric) => metric.metric === name && typeof metric.value === "number").reduce((sum, metric) => sum + (metric.value ?? 0), 0);
739
802
  compactionTelemetry.observeProviderUsage({ input: amount("input-tokens"), output: amount("output-tokens"), cacheRead: amount("cache-read-tokens"), cacheWrite: amount("cache-write-tokens") });
@@ -746,6 +809,7 @@ export function registerJittorExtension(
746
809
  finishCompactionUi();
747
810
  if (compactionTelemetry.hasOpenCompaction()) await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "session-shutdown")]).catch(() => undefined);
748
811
  stopPapyrusContext?.();
812
+ stopPapyrusTaskFocus?.();
749
813
  cancelRecovery(true);
750
814
  lastCodexResponse = {};
751
815
  activeLocalRun = undefined;
@@ -63,7 +63,15 @@ function windowName(seconds: number): string {
63
63
  return `${Math.round(seconds / 60)}m`;
64
64
  }
65
65
 
66
- export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObservation[]): ProviderBudget | null {
66
+ /**
67
+ * `null` means "not known yet, but this provider can report a budget once data arrives" -- router
68
+ * not ready, or a supported provider whose telemetry hasn't been observed yet; the footer shows a
69
+ * placeholder that may resolve. `undefined` means "no budget signal is possible for this provider
70
+ * at all" (e.g. google-vertex, which has no documented rate-limit or quota header/endpoint Jittor
71
+ * could ever read -- see google-vertex-contracts.ts); the footer omits the segment entirely rather
72
+ * than showing a `?` that can never resolve.
73
+ */
74
+ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObservation[]): ProviderBudget | null | undefined {
67
75
  if (!status.ready || !status.currentRoute) return null;
68
76
  if (status.currentRoute.provider === "openai-codex") {
69
77
  const codex = codexWindowForModel(metrics, status.currentRoute.model);
@@ -90,6 +98,25 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
90
98
  ...(Number.isFinite(resetsAt) && resetsAt > 0 ? { resetsAt } : {}),
91
99
  };
92
100
  }
101
+ if (status.currentRoute.provider === "anthropic-vertex") {
102
+ // Best-effort only (see index.ts): these metrics only exist if Anthropic-style rate-limit
103
+ // headers were actually observed on this passthrough, which is unverified. Labeled distinctly
104
+ // ("vtok"/"vreq") from direct Anthropic's "tok"/"req" since they are a different account/quota
105
+ // pool even if the header shape is identical. If nothing was ever observed, this stays null
106
+ // (may still resolve later), not undefined (never possible) -- unlike google-vertex, this
107
+ // provider's transport has not been shown to structurally lack the signal.
108
+ const anthropicVertex = latest(metrics, (row) => row.source === "anthropic-vertex" && row.metric === "used-fraction" && row.scope === "tokens" && typeof row.value === "number")
109
+ ?? latest(metrics, (row) => row.source === "anthropic-vertex" && row.metric === "used-fraction" && row.scope === "requests" && typeof row.value === "number");
110
+ if (!anthropicVertex || typeof anthropicVertex.value !== "number") return null;
111
+ const resetsAt = Number(anthropicVertex.attributes["resetsAt"]);
112
+ return {
113
+ kind: "bounded",
114
+ label: anthropicVertex.scope === "tokens" ? "vtok" : "vreq",
115
+ remainingFraction: 1 - anthropicVertex.value,
116
+ observedAt: anthropicVertex.observedAt,
117
+ ...(Number.isFinite(resetsAt) && resetsAt > 0 ? { resetsAt } : {}),
118
+ };
119
+ }
93
120
  if (status.currentRoute.provider === "openrouter") {
94
121
  const openRouter = latest(metrics, (row) => row.source === "openrouter" && row.metric === "usage" && typeof row.value === "number");
95
122
  const remaining = latest(metrics, (row) => row.source === "openrouter" && row.metric === "remaining-fraction" && typeof row.value === "number");
@@ -106,7 +133,7 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
106
133
  if (!openRouter || typeof openRouter.value !== "number") return null;
107
134
  return { kind: "unbounded", label: "spend", valueText: `$${openRouter.value.toFixed(3)}`, observedAt: openRouter.observedAt };
108
135
  }
109
- return null;
136
+ return undefined;
110
137
  }
111
138
 
112
139
  export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[]): string {
@@ -1,14 +1,14 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT, USAGE_RENDER_MAX_SERIES, USAGE_TOKEN_QUERY_LIMIT, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
4
- import type { StoredMetricObservation } from "../../src/domain/metric.ts";
3
+ import { HUMAN_TEXT_FIELD_MAX_CHARACTERS, USAGE_CHART_HEIGHT, USAGE_MAX_DISTINCT_SCOPES, USAGE_RENDER_MAX_SERIES, USAGE_Y_AXIS_WIDTH } from "../../src/constants.ts";
5
4
  import {
6
5
  buildCostGraph,
7
6
  buildUsageGraph,
7
+ resolveUsageWindow,
8
8
  USAGE_PERIODS,
9
9
  usagePeriod,
10
- usagePeriodStart,
11
10
  type CostGraph,
11
+ type UsageAggregateRow,
12
12
  type UsageGraph,
13
13
  type UsagePeriod,
14
14
  } from "../../src/domain/usage.ts";
@@ -254,15 +254,23 @@ export function renderCostGraph(chart: CostGraph, width: number, theme: UsageThe
254
254
  export type UsageViewKind = "tokens" | "cost";
255
255
  const USAGE_VIEWS: UsageViewKind[] = ["tokens", "cost"];
256
256
 
257
- async function loadPiMetrics(client: JittorPanelClient, period: UsagePeriod, now: number): Promise<{ rows: StoredMetricObservation[]; truncated: boolean }> {
258
- const rows = await client.call("metrics.query", {
259
- source: "pi",
260
- since: usagePeriodStart(period, now),
261
- until: now,
262
- order: "desc",
263
- limit: USAGE_TOKEN_QUERY_LIMIT,
264
- }) as StoredMetricObservation[];
265
- return { rows, truncated: rows.length >= USAGE_TOKEN_QUERY_LIMIT };
257
+ /**
258
+ * One bounded round trip: the daemon discovers distinct scopes (still capped at
259
+ * USAGE_MAX_DISTINCT_SCOPES -- more scopes than that is still a real, honestly-reported
260
+ * truncation) and SQL-side aggregates every matching observation into (scope, metric, bucket)
261
+ * sums for the exact window this panel renders. Replaces a per-scope fetch of up to
262
+ * USAGE_PER_SCOPE_QUERY_LIMIT raw rows each, which fixed a *different* problem (one heavy scope
263
+ * starving *other* scopes out of a shared row budget) but could still silently truncate a single
264
+ * heavy scope's *own* older history within the same window -- a real incident: a scope logging
265
+ * tens of thousands of rows a week had its "weekly" chart built from a few minutes of its most
266
+ * recent rows alone. Aggregation has no such failure mode: result size scales with (scopes x
267
+ * metrics x buckets), never with raw event count.
268
+ */
269
+ async function loadPiMetrics(client: JittorPanelClient, window: ReturnType<typeof resolveUsageWindow>): Promise<{ rows: UsageAggregateRow[]; truncated: boolean }> {
270
+ const result = await client.call("metrics.usage_series", {
271
+ source: "pi", since: window.start, until: window.end, bucketSizeMs: window.bucketSizeMs, bucketCount: window.bucketCount, scopeLimit: USAGE_MAX_DISTINCT_SCOPES,
272
+ }) as { rows: UsageAggregateRow[]; truncated: boolean };
273
+ return result;
266
274
  }
267
275
 
268
276
  export async function showUsagePanel(
@@ -276,11 +284,12 @@ export async function showUsagePanel(
276
284
  let viewIndex = Math.max(0, USAGE_VIEWS.indexOf(initialView));
277
285
  for (;;) {
278
286
  const period = USAGE_PERIODS[periodIndex]!.id;
287
+ const window = resolveUsageWindow(period, now);
279
288
  // One bounded query serves both views: token and cost metrics share the same "pi" source rows.
280
- const { rows, truncated } = await loadPiMetrics(client, period, now);
289
+ const { rows, truncated } = await loadPiMetrics(client, window);
281
290
  const view = USAGE_VIEWS[viewIndex]!;
282
- const tokenChart = buildUsageGraph(rows, { period, now, truncated });
283
- const costChart = buildCostGraph(rows, { period, now, truncated });
291
+ const tokenChart = buildUsageGraph(rows, window, { period, truncated });
292
+ const costChart = buildCostGraph(rows, window, { period, truncated });
284
293
  const tokenBudget = budgets.getUsageTokenBudget(period);
285
294
  const renderActive = (width: number, theme: UsageTheme): string[] =>
286
295
  view === "tokens" ? renderUsageGraph(tokenChart, width, theme, tokenBudget) : renderCostGraph(costChart, width, theme);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Just-in-Time Token Optimizing Router for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package", "llm-router", "token-budget"],
@@ -22,6 +22,10 @@
22
22
  "@earendil-works/pi-tui": "*",
23
23
  "typebox": "*"
24
24
  },
25
+ "dependencies": {
26
+ "@danypops/daemon-kit": "^0.3.1",
27
+ "google-auth-library": "^10.9.0"
28
+ },
25
29
  "devDependencies": {
26
30
  "bun-types": "latest"
27
31
  },
@@ -83,7 +83,8 @@ export class OpenRouterBenchmarkIndexSource implements BenchmarkSource {
83
83
  return [
84
84
  validateBenchmarkObservation({ ...common, dimension: "quality-coding", value: requiredNumber(row["coding_index"], "coding index"), unit: "ratio", methodology }),
85
85
  validateBenchmarkObservation({ ...common, dimension: "quality-general", value: requiredNumber(row["intelligence_index"], "intelligence index"), unit: "ratio", methodology }),
86
- validateBenchmarkObservation({ ...common, dimension: "quality-planning", value: requiredNumber(row["agentic_index"], "agentic index"), unit: "ratio", methodology }),
86
+ // agentic_index measures tool-use/agentic execution style, an activity (type), not a subject-matter domain.
87
+ validateBenchmarkObservation({ ...common, dimension: "quality-type-planning", value: requiredNumber(row["agentic_index"], "agentic index"), unit: "ratio", methodology }),
87
88
  validateBenchmarkObservation({ ...common, dimension: "price-input", value: price(pricing["prompt"], "prompt pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per input token" } }),
88
89
  validateBenchmarkObservation({ ...common, dimension: "price-output", value: price(pricing["completion"], "completion pricing"), unit: "usd", methodology: { ...methodology, basis: "OpenRouter USD per output token" } }),
89
90
  ];