@danypops/jittor 0.10.0 → 0.12.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.
Files changed (43) hide show
  1. package/README.md +28 -77
  2. package/package.json +11 -14
  3. package/src/adapters/sqlite-metric-store.ts +11 -2
  4. package/src/adapters/sqlite-session-identity-store.ts +45 -0
  5. package/src/cli-commands/benchmarks.ts +140 -0
  6. package/src/cli-commands/compaction.ts +17 -0
  7. package/src/cli-commands/context.ts +49 -0
  8. package/src/cli-commands/metrics.ts +296 -0
  9. package/src/cli-commands/op.ts +40 -0
  10. package/src/cli-commands/route-args.ts +15 -0
  11. package/src/cli-commands/router.ts +207 -0
  12. package/src/cli-commands/service-daemon.ts +72 -0
  13. package/src/cli-commands/session.ts +42 -0
  14. package/src/cli-commands/support.ts +33 -0
  15. package/src/cli.ts +42 -769
  16. package/src/constants.ts +7 -0
  17. package/src/daemon.ts +13 -3
  18. package/src/db.ts +15 -1
  19. package/src/index.ts +137 -0
  20. package/src/operations/benchmark-operations.ts +12 -0
  21. package/src/operations/context-operations.ts +30 -0
  22. package/src/operations/metrics-operations.ts +77 -0
  23. package/src/operations/model-ranking-operations.ts +16 -0
  24. package/src/operations/router-operations.ts +19 -0
  25. package/src/operations/session-identity-operations.ts +15 -0
  26. package/src/operations/session-scope.ts +31 -0
  27. package/src/operations/types.ts +3 -0
  28. package/src/ports/metric-store.ts +2 -0
  29. package/src/ports/router-controller.ts +9 -9
  30. package/src/ports/session-identity-store.ts +5 -0
  31. package/src/providers/telemetry-sources.ts +2 -1
  32. package/src/router.ts +124 -67
  33. package/src/service.ts +60 -118
  34. package/src/session-identity-service.ts +55 -0
  35. package/docs/USAGE_PRIOR_ART.md +0 -64
  36. package/extension/src/benchmark-tui.ts +0 -105
  37. package/extension/src/footer.ts +0 -366
  38. package/extension/src/index.ts +0 -828
  39. package/extension/src/service-client.ts +0 -26
  40. package/extension/src/settings-tui.ts +0 -153
  41. package/extension/src/settings.ts +0 -103
  42. package/extension/src/tui.ts +0 -270
  43. package/extension/src/usage.ts +0 -320
package/README.md CHANGED
@@ -1,90 +1,41 @@
1
- # Jittor
1
+ # @danypops/jittor
2
2
 
3
- **Just-in-Time Token Optimizing Router** for Pi.
4
-
5
- Jittor observes provider budgets and per-turn usage, computes whether the current burn rate is sustainable, and applies a deterministic policy before each model request:
6
-
7
- 1. continue
8
- 2. throttle
9
- 3. lower thinking
10
- 4. switch model
11
- 5. switch provider
12
- 6. halt
13
-
14
- Initial telemetry providers:
15
-
16
- - ChatGPT-authenticated Codex subscription usage
17
- - OpenRouter API key usage, response accounting, and model pricing
18
- - Anthropic official per-response rate-limit headers (requests, tokens, input/output tokens, and optional Priority Tier buckets)
19
- - Google Vertex AI classified failure pressure (quota/auth/invalid-request/overload/transport), since Vertex has no documented remaining-budget header or personal polling endpoint
20
-
21
- Jittor follows the Papyrus daemon architecture: a supervised Bun service owns SQLite and provider polling; the native Pi extension uses an authenticated loopback client and applies model/thinking decisions.
3
+ Supervised Bun daemon, router policy, provider telemetry adapters, and CLI for Jittor. See the [repo root README](../../README.md) for the two-package overview and [`@danypops/pi-jittor`](../pi-jittor) for the Pi extension that talks to this daemon.
22
4
 
23
5
  ## Architecture
24
6
 
25
- The initial service scaffold is split into domain, ports, and adapters:
26
-
27
7
  - `src/domain/metric.ts` — normalized timestamped metric observations
28
8
  - `src/ports/metric-store.ts` — storage boundary used by the application service
29
9
  - `src/adapters/sqlite-metric-store.ts` — SQLite time-series adapter
30
10
  - `src/service.ts` — authenticated operation registry
31
11
  - `src/client.ts` — operation-typed loopback client
32
12
  - `src/daemon.ts` — Bun composition root and maintenance loop
13
+ - `src/index.ts` — the package's public surface: everything `@danypops/pi-jittor` (or any other consumer) imports
33
14
 
34
15
  SQLite runs in WAL mode with versioned migrations, JSON validation, bounded queries, chronological indexes, pruning, and checkpoints. The database follows `XDG_DATA_HOME`; private authentication state follows `XDG_STATE_HOME`; the daemon handle follows `XDG_RUNTIME_DIR`.
35
16
 
36
17
  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
18
 
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
-
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.
43
-
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.
19
+ ## Provider telemetry
45
20
 
46
- Blocking always has a daemon-independent escape hatch. `/jittor off` immediately enters persisted monitor-only mode and never blocks provider requests. The informational footer is independently controlled with `/jittor footer on` and `/jittor footer off`, so showing status never enables enforcement. `/jittor on` only enables enforcement after telemetry polling and available-route synchronization succeed. Every fail-closed error includes these recovery commands plus the daemon restart command.
47
-
48
- ### Opt-in Codex settled-turn recovery
49
-
50
- Transient Codex recovery is securely off by default and controlled through the existing Jittor command surface:
51
-
52
- ```text
53
- /jittor recovery status
54
- /jittor recovery on
55
- /jittor recovery off
56
- /jittor recovery cancel
57
- ```
58
-
59
- The on/off choice persists privately in `$XDG_CONFIG_HOME/jittor/extension.json` (or `~/.config/jittor/extension.json`). Status reports only enabled state, cooldown, bounded attempt/window counters, and the normalized failure class. `cancel` clears the current cooldown and attempt window without changing the persisted on/off choice.
60
-
61
- Jittor observes finalized Codex assistant errors through Pi's public message lifecycle, classifies only bounded error metadata, and waits for `agent_settled` before acting. That boundary guarantees Pi's built-in retry, compaction retry, and queued follow-up work has finished. A transient concurrency, rate-limit, overload, or transport failure then schedules one hidden follow-up with Retry-After-aware capped jitter. Recovery is limited to three attempts per ten-minute window, never overlaps pending Pi messages, resets after success, and is canceled by human input or session shutdown. Quota, authentication, invalid-request, unknown, and aborted failures remain terminal. Raw provider payloads are never retained or injected.
62
-
63
- ### Settings
64
-
65
- `/jittor` is the settings and control command. Bare `/jittor` (or `/jittor settings`) opens one keyboard-navigable TUI covering routing enforcement, the informational footer, Codex recovery, and all four token-budget thresholds, with explicit ON/OFF and configured/not-configured labels, bounded rendering on narrow terminals, and confirmation for weaker enforcement/recovery changes. `/jittor status` shows the routing/pressure panel that used to be the bare command's default. Existing non-TUI subcommands (`benchmarks`, `outcome`, `recovery`, `on`/`off`, `footer on`/`off`, `context`) remain available for automation and are unchanged.
66
-
67
- ### Usage and cost graphs
68
-
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.
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
21
+ - ChatGPT-authenticated Codex subscription usage
22
+ - OpenRouter API key usage, response accounting, and model pricing
23
+ - Anthropic official per-response rate-limit headers (requests, tokens, input/output tokens, and optional Priority Tier buckets)
24
+ - Google Vertex AI classified failure pressure (quota/auth/invalid-request/overload/transport), since Vertex has no documented remaining-budget header or personal polling endpoint
74
25
 
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, and the provider/model/thinking level active at that moment, the instant 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, broken down further by which provider/model/thinking combination each task actually spent on.
26
+ 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.
76
27
 
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.
28
+ 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.
78
29
 
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`).
30
+ Blocking always has a daemon-independent escape hatch: `/jittor off` (in the extension) immediately enters persisted monitor-only mode and never blocks provider requests, regardless of daemon state.
80
31
 
81
- ### Benchmark evidence
32
+ ## Benchmark evidence
82
33
 
83
34
  Jittor can ingest bounded OpenRouter model metadata, p50 latency/throughput ordering, and Design Arena Elo rankings as provenance-bearing evidence without treating OpenRouter as model-scope authority. Enable online ingestion explicitly with `JITTOR_OPENROUTER_BENCHMARKS=1`; it is off by default. OpenRouter model metadata and operational ordering are public; Design Arena ingestion additionally uses `OPENROUTER_API_KEY` from the supervised service environment without retaining it. Snapshots preserve the upstream publisher, normalized model identities, immutable retrieval revisions, source URLs, confidence, license terms, and explicit freshness deadlines. A malformed or oversized refresh leaves the last complete snapshot visible and records only a payload-safe failure state.
84
35
 
85
- Design Arena rates models across dozens of arena/category pairs (music, video, text-to-speech, ASCII art, ...); Jittor ingests only the bounded allowlist of categories (`codecategories`, `website`, `uicomponent`, `dataviz`, `svg`) that measure frontend/UI-generation skill relevant to routing a coding agent, tagged into one `design` domain distinct from `coding`. A model with no OpenRouter-reachable identity (proprietary platforms, image/video generators) is skipped rather than fabricated into unroutable evidence. Kept on the OpenRouter passthrough rather than migrated to a direct integration: Design Arena's own native API requires a manually reviewed application (1-2 business days), unlike Artificial Analysis's instant self-serve signup below.
36
+ Design Arena rates models across dozens of arena/category pairs (music, video, text-to-speech, ASCII art, ...); Jittor ingests only the bounded allowlist of categories (`codecategories`, `website`, `uicomponent`, `dataviz`, `svg`) that measure frontend/UI-generation skill relevant to routing a coding agent, tagged into one `design` domain distinct from `coding`. A model with no OpenRouter-reachable identity (proprietary platforms, image/video generators) is skipped rather than fabricated into unroutable evidence.
86
37
 
87
- Jittor also ingests LMArena's own official Hugging Face dataset (`lmarena-ai/leaderboard-dataset`, via the public `datasets-server.huggingface.co` API -- no credential required) for its Code Arena (`webdev`) and Agent Arena human-preference battles, and, when `ARTIFICIAL_ANALYSIS_API_KEY` is configured, Artificial Analysis's own direct API (replaces the former OpenRouter passthrough to the same publisher; adds a `math` domain and measured per-model latency the passthrough never exposed). LMArena's Bradley-Terry/IPS ratings aren't on the same scale as Artificial Analysis's 0-100 indices, so they're tagged under distinct `-arena`-suffixed dimensions (`quality-coding-arena`, `quality-type-planning-arena`) instead of blended into the same average -- stored and queryable on their own, not yet part of the default ranked "quality" score.
38
+ Jittor also ingests LMArena's own official Hugging Face dataset (`lmarena-ai/leaderboard-dataset`, via the public `datasets-server.huggingface.co` API -- no credential required) for its Code Arena (`webdev`) and Agent Arena human-preference battles, and, when `ARTIFICIAL_ANALYSIS_API_KEY` is configured, Artificial Analysis's own direct API (adds a `math` domain and measured per-model latency). LMArena's Bradley-Terry/IPS ratings aren't on the same scale as Artificial Analysis's 0-100 indices, so they're tagged under distinct `-arena`-suffixed dimensions instead of blended into the same average.
88
39
 
89
40
  Use the authenticated CLI channels independently:
90
41
 
@@ -96,40 +47,42 @@ jittor benchmarks list --source openrouter-models [--model provider/model] [--di
96
47
 
97
48
  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.
98
49
 
99
- 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.
50
+ The ranking operation (`domain/model-ranking.ts`) accepts an explicit bounded candidate set and never adds identities found only in evidence. It scores quality (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.
100
51
 
101
- 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.
52
+ 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`). Prompts, responses, tool arguments/results, credentials, and project paths are never retained. Robust aggregates report sample size, median, p90, median absolute deviation, recency, and confidence without merging local observations into external benchmark facts.
102
53
 
103
- ### Context pressure
54
+ ## Context pressure
104
55
 
105
56
  Papyrus emits content-free prompt-injection observations through Pi's shared extension event bus. Jittor validates and records their exact Rule/Task character sizes, prompt share, fingerprint repetition, and explicitly estimated token size. Jittor also records completed, aborted, and unmatched Pi compactions with duration, reason, retry state, pre-compaction context usage, and bounded turns/injection/provider/cache usage since the previous compaction.
106
57
 
107
- Run `/jittor context` for the in-session summary, or `jittor context [--since <epoch-ms>] [--until <epoch-ms>] [--json]` through the authenticated daemon client. The assessment reports bounded average/p95/max injection, Rule/Task mix, unchanged rate, compaction frequency/duration/reasons, and between-compaction provider/cache facts. Repeated prompt content is not labeled billed waste: provider-reported input/cache usage and an injection-disabled control are required before making cost or compaction-causality claims.
58
+ `jittor context [--since <epoch-ms>] [--until <epoch-ms>] [--json]` reports bounded average/p95/max injection, Rule/Task mix, unchanged rate, compaction frequency/duration/reasons, and between-compaction provider/cache facts. Repeated prompt content is not labeled billed waste: provider-reported input/cache usage and an injection-disabled control are required before making cost or compaction-causality claims.
108
59
 
109
- ### CLI operations
60
+ ## CLI operations
110
61
 
111
62
  Every daemon operation is reachable from the CLI through the authenticated typed client only — no command reads the SQLite store or a provider adapter directly. Each command supports `--json` for stable machine output; without it, a purpose-built human presenter renders the same result, per [`docs/OUTPUT_CHANNELS.md`](docs/OUTPUT_CHANNELS.md).
112
63
 
113
64
  ```text
114
65
  jittor metrics record --source <s> --scope <s> --metric <s> --value <number|null> --unit <unit> [--observed-at <ms>] [--attributes <json>] [--json]
66
+ jittor metrics record-batch --observations <json-array, max 100> [--json]
115
67
  jittor metrics query [--source <s>] [--scope <s>] [--metric <s>] [--since <ms>] [--until <ms>] [--limit <n>] [--order asc|desc] [--json]
116
- jittor metrics prune --before <ms> [--json]
68
+ jittor metrics prune --before <ms> [--force] [--json] # force required if before is newer than 24h ago
117
69
  jittor metrics distinct-scopes --source <s> --since <ms> --until <ms> [--limit 1..40] [--json]
118
70
  jittor metrics cost-by-task --since <ms> --until <ms> [--json]
119
- jittor metrics prune --before <ms> [--force] [--json] # force required if before is newer than 24h ago
120
71
  jittor service checkpoint [--json]
121
72
  jittor telemetry poll [--json]
122
73
  jittor compaction estimate [--json]
123
- jittor router status|decide|pause|resume|clear-override [--json]
124
- jittor router override --route <provider/model@thinking> [--expires-at <ms>] [--json]
125
- jittor router current-route --route <provider/model@thinking> [--json]
126
- jittor router available-routes [--route <provider/model@thinking> ...] [--json]
74
+ jittor session register --session-id <id> [--json]
75
+ jittor session release --session-id <id> [--session-secret <secret>] [--json]
76
+ jittor router status|decide|pause|resume|clear-override [--session-id <id>] [--session-secret <secret>] [--json]
77
+ jittor router override --route <provider/model@thinking> [--expires-at <ms>] [--session-id <id>] [--session-secret <secret>] [--json]
78
+ jittor router current-route --route <provider/model@thinking> [--session-id <id>] [--session-secret <secret>] [--json]
79
+ jittor router available-routes [--route <provider/model@thinking> ...] [--session-id <id>] [--session-secret <secret>] [--json]
127
80
  jittor op <operation> [--input <json>]
128
81
  ```
129
82
 
130
83
  `jittor op` is a raw escape hatch restricted to the daemon's own `EXPECTED_OPERATION_NAMES`; it rejects an unrecognized operation name before ever reaching the daemon rather than forwarding it blindly. Human-readable metric listings and router status are bounded (at most 50 metric rows and 20 telemetry sources are printed; `--json` output is bounded independently by the daemon's own query and response-size limits). No command prints the daemon bearer token, a provider API key, or an OAuth credential; a daemon-unavailable error stays actionable ("install or start jittor.service") without ever including the token used to reach it.
131
84
 
132
- See [`docs/CALIBRATION.md`](docs/CALIBRATION.md) for thresholds and rollback, and [`docs/USAGE_PRIOR_ART.md`](docs/USAGE_PRIOR_ART.md) for the chart design research.
85
+ See [`docs/CALIBRATION.md`](docs/CALIBRATION.md) for routing thresholds and rollback, and [`docs/PROVIDER_RESEARCH.md`](docs/PROVIDER_RESEARCH.md) for verified provider API boundaries and caveats.
133
86
 
134
87
  ```bash
135
88
  bun test
@@ -138,5 +91,3 @@ bun run service:install
138
91
  ```
139
92
 
140
93
  The systemd user unit binds only to `127.0.0.1`, discovers a 256-bit token without logging it, restarts on failure, and exposes authenticated health and operation endpoints.
141
-
142
- See [`docs/PROVIDER_RESEARCH.md`](docs/PROVIDER_RESEARCH.md) for verified API boundaries and caveats.
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.10.0",
4
- "description": "Just-in-Time Token Optimizing Router for Pi",
3
+ "version": "0.12.0",
4
+ "description": "Just-in-Time Token Optimizing Router for Pi -- supervised daemon, router policy, and CLI",
5
5
  "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
6
8
  "keywords": ["pi-package", "llm-router", "token-budget"],
7
9
  "bin": {
8
10
  "jittor": "src/cli.ts"
@@ -11,16 +13,7 @@
11
13
  "test": "bun test",
12
14
  "typecheck": "tsc --noEmit",
13
15
  "serve": "bun src/cli.ts serve",
14
- "service:install": "bun src/cli.ts service install",
15
- "guard:install": "git config core.hooksPath .githooks"
16
- },
17
- "pi": {
18
- "extensions": ["extension/src/index.ts"]
19
- },
20
- "peerDependencies": {
21
- "@earendil-works/pi-coding-agent": "*",
22
- "@earendil-works/pi-tui": "*",
23
- "typebox": "*"
16
+ "service:install": "bun src/cli.ts service install"
24
17
  },
25
18
  "dependencies": {
26
19
  "@danypops/daemon-kit": "^0.3.1",
@@ -31,7 +24,11 @@
31
24
  },
32
25
  "repository": {
33
26
  "type": "git",
34
- "url": "git+https://github.com/DanyPops/jittor.git"
27
+ "url": "git+https://github.com/DanyPops/jittor.git",
28
+ "directory": "packages/jittor"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
35
32
  },
36
- "files": ["src", "extension", "docs", "README.md"]
33
+ "files": ["src", "docs", "README.md"]
37
34
  }
@@ -34,7 +34,17 @@ export class SQLiteMetricStore implements MetricStore {
34
34
 
35
35
  record(input: MetricObservation): StoredMetricObservation {
36
36
  const observation = validateMetricObservation(input);
37
- const result = this.db.query(`
37
+ const result = this.insert(observation);
38
+ return this.get(Number(result.lastInsertRowid));
39
+ }
40
+
41
+ recordBatch(inputs: MetricObservation[]): StoredMetricObservation[] {
42
+ const observations = inputs.map((input) => validateMetricObservation(input));
43
+ return this.db.transaction((rows: typeof observations) => rows.map((observation) => this.get(Number(this.insert(observation).lastInsertRowid))))(observations);
44
+ }
45
+
46
+ private insert(observation: MetricObservation): { lastInsertRowid: number | bigint } {
47
+ return this.db.query(`
38
48
  INSERT INTO metric_observations (source, scope, metric, value, unit, observed_at, attributes)
39
49
  VALUES (?, ?, ?, ?, ?, ?, ?)
40
50
  `).run(
@@ -46,7 +56,6 @@ export class SQLiteMetricStore implements MetricStore {
46
56
  observation.observedAt,
47
57
  JSON.stringify(observation.attributes ?? {}),
48
58
  );
49
- return this.get(Number(result.lastInsertRowid));
50
59
  }
51
60
 
52
61
  query(filter: MetricQuery = {}): StoredMetricObservation[] {
@@ -0,0 +1,45 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { SESSION_IDENTITY_MAX_ROWS } from "../constants.ts";
3
+ import type { SessionIdentityRecord, SessionIdentityStore } from "../ports/session-identity-store.ts";
4
+
5
+ export class SQLiteSessionIdentityStore implements SessionIdentityStore {
6
+ constructor(private readonly db: Database) {}
7
+
8
+ find(sessionId: string): SessionIdentityRecord | undefined {
9
+ const row = this.db.query("SELECT session_id, secret_hash, registered_at, last_seen_at FROM session_identities WHERE session_id = ?").get(sessionId) as
10
+ | { session_id: string; secret_hash: string; registered_at: string; last_seen_at: string }
11
+ | null;
12
+ return row ? { sessionId: row.session_id, secretHash: row.secret_hash, registeredAt: row.registered_at, lastSeenAt: row.last_seen_at } : undefined;
13
+ }
14
+
15
+ upsert(record: SessionIdentityRecord): void {
16
+ this.db.transaction(() => {
17
+ this.evictOldestBeyondCap(record.sessionId);
18
+ this.db.query(`
19
+ INSERT INTO session_identities (session_id, secret_hash, registered_at, last_seen_at)
20
+ VALUES (?, ?, ?, ?)
21
+ ON CONFLICT(session_id) DO UPDATE SET secret_hash = excluded.secret_hash, registered_at = excluded.registered_at, last_seen_at = excluded.last_seen_at
22
+ `).run(record.sessionId, record.secretHash, record.registeredAt, record.lastSeenAt);
23
+ })();
24
+ }
25
+
26
+ remove(sessionId: string): void {
27
+ this.db.query("DELETE FROM session_identities WHERE session_id = ?").run(sessionId);
28
+ }
29
+
30
+ touch(sessionId: string, lastSeenAt: string): void {
31
+ this.db.query("UPDATE session_identities SET last_seen_at = ? WHERE session_id = ?").run(lastSeenAt, sessionId);
32
+ }
33
+
34
+ count(): number {
35
+ return (this.db.query("SELECT COUNT(*) AS count FROM session_identities").get() as { count: number }).count;
36
+ }
37
+
38
+ /** Bounds distinct registered session identities; evicts the least-recently-seen beyond the cap. */
39
+ private evictOldestBeyondCap(sessionId: string): void {
40
+ const exists = this.db.query("SELECT 1 FROM session_identities WHERE session_id = ?").get(sessionId);
41
+ if (exists) return;
42
+ if (this.count() < SESSION_IDENTITY_MAX_ROWS) return;
43
+ this.db.exec("DELETE FROM session_identities WHERE session_id = (SELECT session_id FROM session_identities ORDER BY last_seen_at ASC LIMIT 1)");
44
+ }
45
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ BENCHMARK_MAX_QUERY_LIMIT,
3
+ MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
4
+ MODEL_RANKING_DEFAULT_COST_WEIGHT,
5
+ MODEL_RANKING_DEFAULT_LATENCY_WEIGHT,
6
+ MODEL_RANKING_DEFAULT_QUALITY_WEIGHT,
7
+ MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
8
+ MODEL_RANKING_MAX_SOURCES,
9
+ } from "../constants.ts";
10
+ import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "../domain/benchmark.ts";
11
+ import type { ModelRecommendationInput } from "../domain/model-ranking-service.ts";
12
+ import type { ModelCandidate, ModelRankingResult, ScopeAuthority, UtilityWeights } from "../domain/model-ranking.ts";
13
+ import { TASK_DOMAINS, TASK_TYPES, type ModelTaskDomain, type ModelTaskType } from "../domain/model-observation.ts";
14
+ import { humanField, type CliDependencies } from "./support.ts";
15
+ import { parseCandidate } from "./route-args.ts";
16
+
17
+ export const BENCHMARKS_USAGE_LINES = [" benchmarks <status|refresh|list|rank> [options] [--json]"];
18
+
19
+ interface BenchmarkArgs {
20
+ action: "status" | "refresh" | "list" | "rank";
21
+ json: boolean;
22
+ force: boolean;
23
+ query?: BenchmarkQuery;
24
+ recommendation?: ModelRecommendationInput & { session_id?: string; session_secret?: string };
25
+ }
26
+
27
+ function parseBenchmarkArgs(action: string | undefined, args: string[]): BenchmarkArgs | null {
28
+ if (action !== "status" && action !== "refresh" && action !== "list" && action !== "rank") return null;
29
+ let json = false;
30
+ let force = false;
31
+ const query: Partial<BenchmarkQuery> = {};
32
+ const candidates: ModelCandidate[] = [];
33
+ const sourceIds: string[] = [];
34
+ let scopeAuthority: ScopeAuthority = "available-models";
35
+ let domain: ModelTaskDomain = "general";
36
+ let type: ModelTaskType = "general";
37
+ let budgetPressure = 0;
38
+ let sessionId: string | undefined;
39
+ let sessionSecret: string | undefined;
40
+ const weights: UtilityWeights = {
41
+ quality: MODEL_RANKING_DEFAULT_QUALITY_WEIGHT, cost: MODEL_RANKING_DEFAULT_COST_WEIGHT,
42
+ latency: MODEL_RANKING_DEFAULT_LATENCY_WEIGHT, context: MODEL_RANKING_DEFAULT_CONTEXT_WEIGHT,
43
+ reliability: MODEL_RANKING_DEFAULT_RELIABILITY_WEIGHT,
44
+ };
45
+ for (let index = 0; index < args.length; index += 1) {
46
+ const argument = args[index];
47
+ if (argument === "--json") { json = true; continue; }
48
+ if (argument === "--force" && action === "refresh") { force = true; continue; }
49
+ const allowed = action === "list" ? ["--source", "--model", "--dimension", "--limit"]
50
+ : action === "rank" ? ["--candidate", "--source", "--domain", "--type", "--scope", "--budget", "--weight-quality", "--weight-cost", "--weight-latency", "--weight-context", "--weight-reliability", "--session-id", "--session-secret"] : [];
51
+ if (!allowed.includes(argument ?? "")) return null;
52
+ const raw = args[++index];
53
+ if (raw === undefined || raw.length === 0) return null;
54
+ if (action === "list") {
55
+ if (argument === "--limit") {
56
+ const limit = Number(raw);
57
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > BENCHMARK_MAX_QUERY_LIMIT) return null;
58
+ query.limit = limit;
59
+ } else if (argument === "--source") query.sourceId = raw;
60
+ else if (argument === "--model") query.model = raw;
61
+ else query.dimension = raw;
62
+ continue;
63
+ }
64
+ if (argument === "--candidate") {
65
+ const candidate = parseCandidate(raw);
66
+ if (!candidate) return null;
67
+ candidates.push(candidate);
68
+ } else if (argument === "--source") sourceIds.push(raw);
69
+ else if (argument === "--domain") {
70
+ if (!TASK_DOMAINS.includes(raw as ModelTaskDomain)) return null;
71
+ domain = raw as ModelTaskDomain;
72
+ } else if (argument === "--type") {
73
+ if (!TASK_TYPES.includes(raw as ModelTaskType)) return null;
74
+ type = raw as ModelTaskType;
75
+ } else if (argument === "--scope") {
76
+ if (raw !== "exact-session" && raw !== "available-models") return null;
77
+ scopeAuthority = raw;
78
+ } else if (argument === "--budget") budgetPressure = Number(raw);
79
+ else if (argument === "--session-id") sessionId = raw;
80
+ else if (argument === "--session-secret") sessionSecret = raw;
81
+ else {
82
+ const weight = Number(raw);
83
+ if (!Number.isFinite(weight) || weight < 0 || weight > 10) return null;
84
+ weights[argument!.slice("--weight-".length) as keyof UtilityWeights] = weight;
85
+ }
86
+ }
87
+ if (action === "list" && query.sourceId === undefined) return null;
88
+ if (action === "rank" && (candidates.length === 0 || sourceIds.length > MODEL_RANKING_MAX_SOURCES || !Number.isFinite(budgetPressure) || budgetPressure < 0 || budgetPressure > 2)) return null;
89
+ return {
90
+ action, json, force,
91
+ ...(action === "list" ? { query: query as BenchmarkQuery } : {}),
92
+ ...(action === "rank" ? { recommendation: { candidates, sourceIds: [...new Set(sourceIds)], scopeAuthority, domain, type, budgetPressure, weights, ...(sessionId ? { session_id: sessionId } : {}), ...(sessionSecret ? { session_secret: sessionSecret } : {}) } } : {}),
93
+ };
94
+ }
95
+
96
+ export function formatBenchmarkStatus(result: BenchmarkRefreshResult): string {
97
+ if (result.sources.length === 0) return "Benchmark sources: none configured";
98
+ return ["Benchmark sources:", ...result.sources.map((source) => {
99
+ const state = source.ok === null ? "not refreshed" : source.ok ? "ready" : "refresh failed";
100
+ return `- ${source.id}: ${state} · ${source.observations.toLocaleString()} observations · ${source.hasEvidence ? "evidence retained" : "no evidence"}`;
101
+ })].join("\n");
102
+ }
103
+
104
+ export function formatBenchmarkQuery(result: BenchmarkQueryResult): string {
105
+ return [
106
+ `Benchmark evidence: ${humanField(result.sourceId)} · ${result.completeness} · ${result.freshness} · ${result.observations.length.toLocaleString()} observations`,
107
+ ...result.observations.map((observation) => `- ${humanField(observation.model.canonical)} · ${humanField(observation.dimension)} ${observation.value.toLocaleString()} ${observation.unit} · ${humanField(observation.provenance.publisher)} · confidence ${(observation.provenance.confidence * 100).toFixed(0)}%`),
108
+ ].join("\n");
109
+ }
110
+
111
+ export function formatModelRanking(result: ModelRankingResult): string {
112
+ return [
113
+ `Model ranking: ${result.completeness} · scope ${result.scopeAuthority}${result.scopeWarning ? " · advisory only" : ""}`,
114
+ ...result.ranked.map((item, index) => `${index + 1}. ${humanField(item.identity)} · utility ${item.utility === null ? "unknown" : item.utility.toFixed(3)} · confidence ${(item.confidence * 100).toFixed(0)}%`),
115
+ ...(result.scopeWarning ? [result.scopeWarning] : []),
116
+ ].join("\n");
117
+ }
118
+
119
+ export async function runBenchmarksCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
120
+ const parsed = parseBenchmarkArgs(action, rest);
121
+ if (!parsed) return usage();
122
+ try {
123
+ if (parsed.action === "list") {
124
+ const result = await deps.client.call("benchmark.query", parsed.query!);
125
+ deps.stdout(parsed.json ? JSON.stringify(result) : formatBenchmarkQuery(result));
126
+ } else if (parsed.action === "rank") {
127
+ const result = await deps.client.call("models.rank", parsed.recommendation!);
128
+ deps.stdout(parsed.json ? JSON.stringify(result) : formatModelRanking(result));
129
+ } else {
130
+ const result = parsed.action === "refresh"
131
+ ? await deps.client.call("benchmark.refresh", { force: parsed.force })
132
+ : await deps.client.call("benchmark.status", {});
133
+ deps.stdout(parsed.json ? JSON.stringify(result) : formatBenchmarkStatus(result));
134
+ }
135
+ return 0;
136
+ } catch (error) {
137
+ deps.stderr(error instanceof Error ? error.message : String(error));
138
+ return 1;
139
+ }
140
+ }
@@ -0,0 +1,17 @@
1
+ import type { CompactionDurationEstimate } from "../domain/context-telemetry.ts";
2
+ import { callAndPrint, type CliDependencies } from "./support.ts";
3
+ import { parseJsonOnlyArgs } from "./router.ts";
4
+
5
+ export function formatCompactionEstimate(estimate: CompactionDurationEstimate): string {
6
+ if (estimate.confidence === "cold-start" || estimate.ms === null) {
7
+ return `Compaction duration: cold-start (${estimate.sampleSize.toLocaleString()} sample(s), not enough evidence yet)`;
8
+ }
9
+ return `Compaction duration: ~${estimate.ms.toLocaleString()}ms learned from ${estimate.sampleSize.toLocaleString()} sample(s)`;
10
+ }
11
+
12
+ export async function runCompactionCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
13
+ if (action !== "estimate") return usage();
14
+ const parsed = parseJsonOnlyArgs(rest);
15
+ if (!parsed) return usage();
16
+ return callAndPrint(deps, "compaction.estimate", {}, parsed.json, formatCompactionEstimate);
17
+ }
@@ -0,0 +1,49 @@
1
+ import type { ContextAssessment } from "../domain/context-telemetry.ts";
2
+ import type { CliDependencies } from "./support.ts";
3
+
4
+ export const CONTEXT_USAGE_LINES = [" context [--since <ms>] [--until <ms>] [--json]"];
5
+
6
+ function parseContextArgs(args: string[]): { input: { since?: number; until?: number }; json: boolean } | null {
7
+ const input: { since?: number; until?: number } = {};
8
+ let json = false;
9
+ for (let index = 0; index < args.length; index += 1) {
10
+ const argument = args[index];
11
+ if (argument === "--json") { json = true; continue; }
12
+ if (argument !== "--since" && argument !== "--until") return null;
13
+ const raw = args[++index];
14
+ const value = raw === undefined ? Number.NaN : Number(raw);
15
+ if (!Number.isSafeInteger(value) || value < 0) return null;
16
+ if (argument === "--since") input.since = value;
17
+ else input.until = value;
18
+ }
19
+ if (input.since !== undefined && input.until !== undefined && input.until < input.since) return null;
20
+ return { input, json };
21
+ }
22
+
23
+ function value(value: number | null, suffix = ""): string {
24
+ return value === null ? "unknown" : `${Math.round(value).toLocaleString()}${suffix}`;
25
+ }
26
+
27
+ export function formatContextAssessment(summary: ContextAssessment): string {
28
+ return [
29
+ `Context assessment: ${summary.completeness}`,
30
+ `Papyrus injection: ${summary.injection.runs} runs · avg ${value(summary.injection.averageCharacters, " chars")} · p95 ${value(summary.injection.p95Characters, " chars")} · max ${value(summary.injection.maxCharacters, " chars")}`,
31
+ `Injection mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens · unchanged ${summary.injection.unchangedRate === null ? "unknown" : `${(summary.injection.unchangedRate * 100).toFixed(1)}%`}`,
32
+ `Compactions: ${summary.compaction.completed} completed · ${summary.compaction.aborted} aborted · avg ${value(summary.compaction.averageDurationMs, "ms")} · ${summary.compaction.perRun === null ? "unknown" : summary.compaction.perRun.toFixed(3)} per agent run · ${summary.compaction.perTurn === null ? "unknown" : summary.compaction.perTurn.toFixed(3)} per turn`,
33
+ `Between compactions: ${value(summary.compaction.averageTurnsBetween, " turns")} · ${value(summary.compaction.averageProviderTokensBetween, " provider tokens")} · ${value(summary.compaction.averageCacheReadTokensBetween, " cache-read tokens")}`,
34
+ `Reasons: threshold ${summary.compaction.reasons.threshold} · overflow ${summary.compaction.reasons.overflow} · manual ${summary.compaction.reasons.manual}`,
35
+ ].join("\n");
36
+ }
37
+
38
+ export async function runContextCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
39
+ const parsed = parseContextArgs([...(action === undefined ? [] : [action]), ...rest]);
40
+ if (!parsed) return usage();
41
+ try {
42
+ const summary = await deps.client.call("context.assess", parsed.input);
43
+ deps.stdout(parsed.json ? JSON.stringify(summary) : formatContextAssessment(summary));
44
+ return 0;
45
+ } catch (error) {
46
+ deps.stderr(error instanceof Error ? error.message : String(error));
47
+ return 1;
48
+ }
49
+ }