@danypops/pi-jittor 0.2.1 → 0.3.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 +19 -3
- package/docs/USAGE_PRIOR_ART.md +1 -1
- package/extension/src/index.ts +173 -43
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +200 -27
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{context-report.ts → observability/context-report.ts} +35 -3
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +7 -0
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +1 -1
- package/extension/src/{tui.ts → observability/status.ts} +55 -10
- package/extension/src/{usage.ts → observability/usage.ts} +74 -166
- package/extension/src/{benchmark-tui.ts → optimization/model-selection-panel.ts} +61 -29
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +2 -2
- package/extension/src/service-client.ts +48 -1
- package/extension/src/settings-tui.ts +57 -32
- package/extension/src/settings.ts +27 -19
- package/package.json +9 -3
- package/extension/src/context-view.ts +0 -140
- /package/extension/src/{capabilities → observability}/context-hub.ts +0 -0
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
- /package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# @danypops/pi-jittor
|
|
2
2
|
|
|
3
|
-
The Pi extension for Jittor:
|
|
3
|
+
The Pi extension for Jittor: token, cost, context, provider-budget, and model-run observation plus routing, recovery, and model-selection controls, all through an authenticated loopback client to the [`@danypops/jittor`](../jittor) daemon. See the [repo root README](../../README.md) for the two-package overview.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
- `extension/src/observability/` collects and presents token, cost, context, provider-budget, and model-run observations.
|
|
8
|
+
- `extension/src/optimization/` contains routing-adjacent recovery and model-selection controls.
|
|
9
|
+
- `extension/src/index.ts` composes Pi lifecycle handlers; daemon transport, settings, and session identity remain thin boundary modules.
|
|
4
10
|
|
|
5
11
|
## Behavior
|
|
6
12
|
|
|
7
|
-
The extension
|
|
13
|
+
The extension records context composition, response headers, finalized usage, cost, and model-run behavior through the daemon. It also preflights provider turns, applies model/thinking decisions, 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. Mutable route state is scoped by Pi session, so concurrent sessions cannot replace each other's active provider or footer budget selection. Each session registers an opaque secret with the daemon at `session_start` (best-effort; a registration failure leaves that session unarmored rather than blocked) and presents it on every router-mutating call for the rest of its lifetime; an unregistered `session_id` continues to mutate exactly as before, so this is additive armor, not a breaking change for other callers of the same API. A configured required budget source still fails closed; a provider with no enforceable budget window continues explicitly monitor-only.
|
|
8
14
|
|
|
9
15
|
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 from its captured fill 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). It never renders a timer. Until enough evidence exists, the bar does not drain; it blinks in place at its captured fill. Run `jittor compaction estimate [--json]` (via the core CLI) 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.
|
|
10
16
|
|
|
@@ -37,7 +43,7 @@ The extension observes finalized Codex assistant errors through Pi's public mess
|
|
|
37
43
|
|
|
38
44
|
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.
|
|
39
45
|
|
|
40
|
-
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.
|
|
46
|
+
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. A stable provider/model identity hash keeps each model's color fixed across token and cost graphics, period switches, refreshes, ranking changes, and extension restarts. The hash also selects an optional bold variant as a second visual channel, reducing collisions when more model identities exist than terminal hues. Multiple models active within the same cumulative time frame are rendered as one bar stacked by color, not separate bars.
|
|
41
47
|
|
|
42
48
|
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`).
|
|
43
49
|
|
|
@@ -51,6 +57,16 @@ The extension observes Papyrus's task-focus lifecycle in real time over a shared
|
|
|
51
57
|
|
|
52
58
|
Run `/jittor benchmarks [coding|general] [research|planning|general]` (either order, either or both omitted) for the responsive recommendation panel over the core daemon's benchmark ranking operation (see `@danypops/jittor`'s README for ingestion sources). 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. `/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.
|
|
53
59
|
|
|
60
|
+
### Context Hub inspection
|
|
61
|
+
|
|
62
|
+
Run `/context` for a tree-aware breakdown of the current context window. The TUI supports `/` live search, `f` scope cycling (**all**, **active**, **historical/compacted**), `m` minimum-size thresholds, `g`/`G` top/bottom navigation, and arrow-key scrolling. Search and filters preserve ancestor rows so a matching message block never loses its segment, turn, or branch context.
|
|
63
|
+
|
|
64
|
+
Conversation entries drill into text, thinking, tool-call arguments, shell commands, and outputs. For conservatively mapped OpenAI-family models, `/context` lazily loads the corresponding `gpt-tokenizer` BPE and marks those individual values `tokenizer-exact-text`, naming the encoding. Unsupported/unknown models and character-only structural inputs remain visibly marked `≈ char/4`; failure to load a tokenizer falls back rather than blocking a request or guessing exactness. Base-prompt aggregates still drill into tool snippets, skills, project context files, guidelines, and custom/appended prompt inputs with structural provenance.
|
|
65
|
+
|
|
66
|
+
Assistant turns also expose the authoritative **provider-reported aggregate request context** (`input + cacheRead + cacheWrite`). The aggregate stays separate from individual item costs, with an explicit unattributed residual rather than proportional allocation. Images, message envelopes, chat templates, tool schemas, cache-control serialization, and provider rewrites remain provider/model-specific, so exact local text counts are never described as exact requests or billing. Measurements retain only bounded provenance/identity metadata; tokenizer input and prompt/response content are never persisted. See [`../jittor/docs/TOKEN_MEASUREMENT.md`](../jittor/docs/TOKEN_MEASUREMENT.md).
|
|
67
|
+
|
|
68
|
+
At Pi's `before_provider_request` boundary, Jittor also records a bounded content-free snapshot of final request structure plus compaction-aware/inactive session history. `/context` shows the latest stable-prefix size, first changed segment, lifecycle counts, per-source growth, and truncation state. These keyed-HMAC correlations are structural evidence, never proof of provider cache behavior. See [`../jittor/docs/CONTEXT_SNAPSHOTS.md`](../jittor/docs/CONTEXT_SNAPSHOTS.md).
|
|
69
|
+
|
|
54
70
|
### Context pressure
|
|
55
71
|
|
|
56
72
|
Run `/jittor context` for the in-session summary of Papyrus prompt-injection and Pi compaction telemetry; see `@danypops/jittor`'s README for what is recorded and the equivalent CLI command.
|
package/docs/USAGE_PRIOR_ART.md
CHANGED
|
@@ -55,7 +55,7 @@ OpenRouter's authenticated web dashboard is not distributed as reusable terminal
|
|
|
55
55
|
|
|
56
56
|
Jittor combines the best applicable patterns:
|
|
57
57
|
|
|
58
|
-
1. Pure
|
|
58
|
+
1. Pure observation projection in `src/observability/usage.ts`.
|
|
59
59
|
2. Explicit `24h`, `7d`, `30d`, and `90d` windows.
|
|
60
60
|
3. Provider/model-preserving series and input/output/cache totals.
|
|
61
61
|
4. Vertically scaled, colored, stacked Unicode bars with fractional top blocks.
|
package/extension/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContextSegmentItem } from "@danypops/jittor";
|
|
1
|
+
import type { ContextSegmentItem, ContextSnapshot } from "@danypops/jittor";
|
|
2
2
|
import {
|
|
3
3
|
applyTaskFocusEvent,
|
|
4
4
|
CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
CONTEXT_HUB_CONTRIBUTION_CHANNEL,
|
|
7
7
|
CompactionTelemetry,
|
|
8
8
|
type ContextAssessment,
|
|
9
|
+
classifyTaskFromTools,
|
|
9
10
|
FOOTER_COMPACTION_RENDER_INTERVAL_MS,
|
|
11
|
+
HmacContextFingerprinter,
|
|
12
|
+
loadOpenAiTextTokenCounter,
|
|
10
13
|
MAX_DYNAMIC_ROUTES,
|
|
11
14
|
type MetricObservation,
|
|
12
15
|
type ModelCandidate,
|
|
@@ -21,6 +24,8 @@ import {
|
|
|
21
24
|
type StoredMetricObservation,
|
|
22
25
|
TASK_DOMAINS,
|
|
23
26
|
TASK_TYPES,
|
|
27
|
+
type TextTokenCounter,
|
|
28
|
+
type TokenMeasurementScope,
|
|
24
29
|
toolLedgerSegment,
|
|
25
30
|
USAGE_PERIODS,
|
|
26
31
|
type UsagePeriod,
|
|
@@ -28,11 +33,6 @@ import {
|
|
|
28
33
|
validateTaskFocusEvent,
|
|
29
34
|
} from "@danypops/jittor";
|
|
30
35
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
|
-
import { showBenchmarkPanel } from "./benchmark-tui.ts";
|
|
32
|
-
import { CodexRecoveryCapability, type CodexRecoveryRuntime, SYSTEM_RECOVERY_RUNTIME } from "./capabilities/codex-recovery.ts";
|
|
33
|
-
import { ContextHubCapability } from "./capabilities/context-hub.ts";
|
|
34
|
-
import { LocalRunTelemetry } from "./capabilities/local-run-telemetry.ts";
|
|
35
|
-
import { ProviderResponseTelemetry } from "./capabilities/provider-response-telemetry.ts";
|
|
36
36
|
import {
|
|
37
37
|
basePromptSegment,
|
|
38
38
|
buildBasePromptItems,
|
|
@@ -41,20 +41,41 @@ import {
|
|
|
41
41
|
messageHistorySegment,
|
|
42
42
|
type SessionEntryLike,
|
|
43
43
|
type SessionTreeNodeLike,
|
|
44
|
-
} from "./context-breakdown.ts";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
44
|
+
} from "./observability/context-breakdown.ts";
|
|
45
|
+
import { ContextGrowthCapability } from "./observability/context-growth.ts";
|
|
46
|
+
import { ContextHubCapability } from "./observability/context-hub.ts";
|
|
47
|
+
import { showContextView } from "./observability/context-view.ts";
|
|
48
|
+
import { type CompactionProgress, type IntegratedFooterState, installIntegratedFooter } from "./observability/footer.ts";
|
|
49
|
+
import { LocalRunTelemetry } from "./observability/model-run.ts";
|
|
50
|
+
import { captureProviderContextSnapshot } from "./observability/provider-context-snapshot.ts";
|
|
51
|
+
import { ProviderResponseTelemetry } from "./observability/provider-response.ts";
|
|
52
|
+
import { buildFooterBudget, providerBudgetMetricQuery, showJittorPanel } from "./observability/status.ts";
|
|
53
|
+
import { showUsagePanel } from "./observability/usage.ts";
|
|
54
|
+
import { showBenchmarkPanel } from "./optimization/model-selection-panel.ts";
|
|
55
|
+
import { CodexRecoveryCapability, type CodexRecoveryRuntime, SYSTEM_RECOVERY_RUNTIME } from "./optimization/recovery/codex.ts";
|
|
47
56
|
import { callJittor } from "./service-client.ts";
|
|
48
57
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
49
58
|
import { type CodexRecoveryControl, type EnforcementControl, persistentEnforcementControl, type UsageBudgetControl } from "./settings.ts";
|
|
50
59
|
import { showSettingsPanel } from "./settings-tui.ts";
|
|
51
|
-
import { buildFooterBudget, providerBudgetMetricQuery, showJittorPanel } from "./tui.ts";
|
|
52
|
-
import { showUsagePanel } from "./usage.ts";
|
|
53
60
|
|
|
54
|
-
export
|
|
55
|
-
export {
|
|
61
|
+
export { formatFooterStatus } from "./observability/status.ts";
|
|
62
|
+
export type { CodexRecoveryRuntime } from "./optimization/recovery/codex.ts";
|
|
56
63
|
|
|
57
64
|
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
65
|
+
const textTokenCounterPromises = new Map<string, Promise<readonly TextTokenCounter[]>>();
|
|
66
|
+
|
|
67
|
+
function textTokenCounters(provider: string | undefined, model: string | undefined): Promise<readonly TextTokenCounter[]> {
|
|
68
|
+
if (!provider || !model) return Promise.resolve([]);
|
|
69
|
+
const key = `${provider}\u0000${model}`;
|
|
70
|
+
let counters = textTokenCounterPromises.get(key);
|
|
71
|
+
if (!counters) {
|
|
72
|
+
counters = loadOpenAiTextTokenCounter(provider, model)
|
|
73
|
+
.then((counter) => (counter ? [counter] : []))
|
|
74
|
+
.catch(() => []);
|
|
75
|
+
textTokenCounterPromises.set(key, counters);
|
|
76
|
+
}
|
|
77
|
+
return counters;
|
|
78
|
+
}
|
|
58
79
|
const RECOVERY_GUIDANCE = "Run /jittor off to disable blocking, or restart the daemon with: systemctl --user restart jittor.service";
|
|
59
80
|
|
|
60
81
|
export interface JittorExtensionClient {
|
|
@@ -286,6 +307,9 @@ function assistantUsageMetrics(
|
|
|
286
307
|
const value = message as Record<string, unknown>;
|
|
287
308
|
if (value.role !== "assistant" || typeof value.usage !== "object" || value.usage === null) return [];
|
|
288
309
|
const usage = value.usage as Record<string, unknown>;
|
|
310
|
+
const messageTimestamp = value.timestamp;
|
|
311
|
+
const metricObservedAt =
|
|
312
|
+
typeof messageTimestamp === "number" && Number.isSafeInteger(messageTimestamp) && messageTimestamp >= 0 ? messageTimestamp : observedAt;
|
|
289
313
|
const provider = typeof value.provider === "string" ? value.provider : "unknown";
|
|
290
314
|
const model = typeof value.model === "string" ? value.model : "unknown";
|
|
291
315
|
const scope = `${provider}:${model}`;
|
|
@@ -296,19 +320,37 @@ function assistantUsageMetrics(
|
|
|
296
320
|
...(thinking === null || thinking.length === 0 ? {} : { thinking }),
|
|
297
321
|
};
|
|
298
322
|
const metrics: MetricObservation[] = [];
|
|
299
|
-
for (const [field, metric] of [
|
|
300
|
-
["input", "input-tokens"],
|
|
301
|
-
["output", "output-tokens"],
|
|
302
|
-
["cacheRead", "cache-read-tokens"],
|
|
303
|
-
["cacheWrite", "cache-write-tokens"],
|
|
304
|
-
] as const) {
|
|
323
|
+
for (const [field, metric, tokenScope] of [
|
|
324
|
+
["input", "input-tokens", "request-input"],
|
|
325
|
+
["output", "output-tokens", "response-output"],
|
|
326
|
+
["cacheRead", "cache-read-tokens", "cache-read"],
|
|
327
|
+
["cacheWrite", "cache-write-tokens", "cache-write"],
|
|
328
|
+
] as const satisfies ReadonlyArray<readonly [string, string, TokenMeasurementScope]>) {
|
|
305
329
|
const amount = usage[field];
|
|
306
|
-
if (typeof amount === "number" && Number.
|
|
307
|
-
metrics.push({
|
|
330
|
+
if (typeof amount === "number" && Number.isSafeInteger(amount) && amount >= 0)
|
|
331
|
+
metrics.push({
|
|
332
|
+
source: "pi",
|
|
333
|
+
scope,
|
|
334
|
+
metric,
|
|
335
|
+
value: amount,
|
|
336
|
+
unit: "tokens",
|
|
337
|
+
observedAt: metricObservedAt,
|
|
338
|
+
attributes: {
|
|
339
|
+
...attributes,
|
|
340
|
+
tokenMeasurement: {
|
|
341
|
+
tokens: amount,
|
|
342
|
+
scope: tokenScope,
|
|
343
|
+
provenance: "provider-reported",
|
|
344
|
+
method: "pi-assistant-usage",
|
|
345
|
+
provider,
|
|
346
|
+
model,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
});
|
|
308
350
|
}
|
|
309
351
|
const cost = typeof usage.cost === "object" && usage.cost !== null ? (usage.cost as Record<string, unknown>).total : undefined;
|
|
310
352
|
if (typeof cost === "number" && Number.isFinite(cost))
|
|
311
|
-
metrics.push({ source: "pi", scope, metric: "cost", value: cost, unit: "usd", observedAt, attributes });
|
|
353
|
+
metrics.push({ source: "pi", scope, metric: "cost", value: cost, unit: "usd", observedAt: metricObservedAt, attributes });
|
|
312
354
|
return metrics;
|
|
313
355
|
}
|
|
314
356
|
|
|
@@ -318,14 +360,70 @@ export function registerJittorExtension(
|
|
|
318
360
|
enforcement: EnforcementControl = persistentEnforcementControl(),
|
|
319
361
|
codexRecovery: CodexRecoveryControl = recoveryControl(enforcement),
|
|
320
362
|
recoveryRuntime: CodexRecoveryRuntime = SYSTEM_RECOVERY_RUNTIME,
|
|
363
|
+
contextGrowth: ContextGrowthCapability = new ContextGrowthCapability(),
|
|
321
364
|
): void {
|
|
322
365
|
const footerState: IntegratedFooterState = { providerBudget: null };
|
|
323
366
|
const usageBudgets = usageBudgetControl(enforcement);
|
|
324
367
|
let compactionTelemetry = new CompactionTelemetry();
|
|
368
|
+
let contextGrowthTurn = 0;
|
|
325
369
|
const localRunTelemetry = new LocalRunTelemetry();
|
|
326
370
|
const providerResponseTelemetry = new ProviderResponseTelemetry();
|
|
327
371
|
const codexRecoveryCapability = new CodexRecoveryCapability(pi, codexRecovery, recoveryRuntime);
|
|
328
372
|
const contextHub = new ContextHubCapability();
|
|
373
|
+
const contextFingerprintKey = new Uint8Array(32);
|
|
374
|
+
crypto.getRandomValues(contextFingerprintKey);
|
|
375
|
+
const contextFingerprinter = new HmacContextFingerprinter(contextFingerprintKey);
|
|
376
|
+
let contextCaptureSequence = 0;
|
|
377
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
378
|
+
try {
|
|
379
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
380
|
+
let history:
|
|
381
|
+
| {
|
|
382
|
+
roots: SessionTreeNodeLike[];
|
|
383
|
+
activeEntryIds: Set<string>;
|
|
384
|
+
branchEntryIds: Set<string>;
|
|
385
|
+
}
|
|
386
|
+
| undefined;
|
|
387
|
+
try {
|
|
388
|
+
history = {
|
|
389
|
+
roots: ctx.sessionManager.getTree() as SessionTreeNodeLike[],
|
|
390
|
+
activeEntryIds: new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id)),
|
|
391
|
+
branchEntryIds: new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id)),
|
|
392
|
+
};
|
|
393
|
+
} catch {
|
|
394
|
+
// Older/custom SessionManager implementations may not expose tree projections.
|
|
395
|
+
}
|
|
396
|
+
const captureInput = {
|
|
397
|
+
payload: event.payload,
|
|
398
|
+
captureId: `${++contextCaptureSequence}`,
|
|
399
|
+
sessionId,
|
|
400
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
401
|
+
model: ctx.model?.id ?? "unknown",
|
|
402
|
+
capturedAt: Date.now(),
|
|
403
|
+
fingerprinter: contextFingerprinter,
|
|
404
|
+
};
|
|
405
|
+
let snapshot: ContextSnapshot;
|
|
406
|
+
try {
|
|
407
|
+
snapshot = captureProviderContextSnapshot({ ...captureInput, ...(history ? { history } : {}) });
|
|
408
|
+
} catch {
|
|
409
|
+
// A custom SessionManager tree shape must not suppress the real request-payload snapshot.
|
|
410
|
+
snapshot = captureProviderContextSnapshot(captureInput);
|
|
411
|
+
}
|
|
412
|
+
// Observation must never alter or abort the provider request. Both local writes are detached;
|
|
413
|
+
// they receive only bounded token sizes and keyed fingerprints.
|
|
414
|
+
const requestTokens = snapshot.segments
|
|
415
|
+
.filter((segment) => segment.requestPosition !== null)
|
|
416
|
+
.reduce((sum, segment) => sum + segment.tokens, 0);
|
|
417
|
+
const compactionMetrics = compactionTelemetry.observeContextSnapshot(requestTokens, "structural-estimate", snapshot.capturedAt, {
|
|
418
|
+
provider: snapshot.provider,
|
|
419
|
+
model: snapshot.model,
|
|
420
|
+
});
|
|
421
|
+
void client.call("context.snapshot", snapshot).catch(() => undefined);
|
|
422
|
+
if (compactionMetrics.length > 0) void recordMetrics(client, compactionMetrics).catch(() => undefined);
|
|
423
|
+
} catch {
|
|
424
|
+
// Snapshot collection is strictly failure-isolated from provider delivery.
|
|
425
|
+
}
|
|
426
|
+
});
|
|
329
427
|
const stopContextHub = pi.events?.on?.(CONTEXT_HUB_CONTRIBUTION_CHANNEL, (payload) => contextHub.observe(payload));
|
|
330
428
|
// Cached from the most recent before_agent_start observation: Pi's own base system prompt is
|
|
331
429
|
// only ever visible transiently inside that hook's event, so /context reuses this rather than
|
|
@@ -402,8 +500,8 @@ export function registerJittorExtension(
|
|
|
402
500
|
if (enforcement.isFooterEnabled()) installIntegratedFooter(ctx, footerState, () => pi.getThinkingLevel());
|
|
403
501
|
else ctx.ui.setFooter(undefined);
|
|
404
502
|
};
|
|
405
|
-
const disable = (ctx: ExtensionContext): void => {
|
|
406
|
-
enforcement.setEnabled(false);
|
|
503
|
+
const disable = async (ctx: ExtensionContext): Promise<void> => {
|
|
504
|
+
await enforcement.setEnabled(false);
|
|
407
505
|
ctx.ui.setStatus("jittor", undefined);
|
|
408
506
|
showFooter(ctx);
|
|
409
507
|
ctx.ui.notify(
|
|
@@ -418,12 +516,12 @@ export function registerJittorExtension(
|
|
|
418
516
|
await client.call("telemetry.poll", {});
|
|
419
517
|
const readinessDecision = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
|
|
420
518
|
if (readinessDecision.action === "halt") throw new Error(readinessDecision.reason);
|
|
421
|
-
enforcement.setEnabled(true);
|
|
519
|
+
await enforcement.setEnabled(true);
|
|
422
520
|
showFooter(ctx);
|
|
423
521
|
await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
|
|
424
522
|
ctx.ui.notify("Jittor enforcement enabled.", "info");
|
|
425
523
|
} catch (error) {
|
|
426
|
-
enforcement.setEnabled(false);
|
|
524
|
+
await enforcement.setEnabled(false);
|
|
427
525
|
showFooter(ctx);
|
|
428
526
|
const reason = error instanceof Error ? error.message : "readiness failed";
|
|
429
527
|
ctx.ui.notify(`Jittor remains monitor-only: ${reason}. ${RECOVERY_GUIDANCE}.`, "error");
|
|
@@ -438,13 +536,13 @@ export function registerJittorExtension(
|
|
|
438
536
|
await showSettingsPanel(ctx, enforcement, codexRecovery, usageBudgets, {
|
|
439
537
|
setEnforcement: async (enabled) => (enabled ? enable(ctx) : disable(ctx)),
|
|
440
538
|
setFooter: async (enabled) => {
|
|
441
|
-
enforcement.setFooterEnabled(enabled);
|
|
539
|
+
await enforcement.setFooterEnabled(enabled);
|
|
442
540
|
showFooter(ctx);
|
|
443
541
|
if (enabled) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
444
542
|
},
|
|
445
|
-
setRecovery: (enabled) => {
|
|
543
|
+
setRecovery: async (enabled) => {
|
|
446
544
|
if (!enabled) cancelRecovery(true);
|
|
447
|
-
codexRecovery.setCodexRecoveryEnabled(enabled);
|
|
545
|
+
await codexRecovery.setCodexRecoveryEnabled(enabled);
|
|
448
546
|
},
|
|
449
547
|
});
|
|
450
548
|
return;
|
|
@@ -499,13 +597,13 @@ export function registerJittorExtension(
|
|
|
499
597
|
return;
|
|
500
598
|
}
|
|
501
599
|
if (action === "recovery on" || action === "recovery enable") {
|
|
502
|
-
codexRecovery.setCodexRecoveryEnabled(true);
|
|
600
|
+
await codexRecovery.setCodexRecoveryEnabled(true);
|
|
503
601
|
ctx.ui.notify("Jittor Codex recovery enabled; bounded retries begin only after transient failures fully settle.", "info");
|
|
504
602
|
return;
|
|
505
603
|
}
|
|
506
604
|
if (action === "recovery off" || action === "recovery disable") {
|
|
507
605
|
cancelRecovery(true);
|
|
508
|
-
codexRecovery.setCodexRecoveryEnabled(false);
|
|
606
|
+
await codexRecovery.setCodexRecoveryEnabled(false);
|
|
509
607
|
ctx.ui.notify("Jittor Codex recovery disabled and pending recovery cleared.", "info");
|
|
510
608
|
return;
|
|
511
609
|
}
|
|
@@ -518,7 +616,7 @@ export function registerJittorExtension(
|
|
|
518
616
|
return;
|
|
519
617
|
}
|
|
520
618
|
if (action === "off" || action === "disable") {
|
|
521
|
-
disable(ctx);
|
|
619
|
+
await disable(ctx);
|
|
522
620
|
return;
|
|
523
621
|
}
|
|
524
622
|
if (action === "on" || action === "enable") {
|
|
@@ -526,13 +624,13 @@ export function registerJittorExtension(
|
|
|
526
624
|
return;
|
|
527
625
|
}
|
|
528
626
|
if (action === "footer off" || action === "footer disable") {
|
|
529
|
-
enforcement.setFooterEnabled(false);
|
|
627
|
+
await enforcement.setFooterEnabled(false);
|
|
530
628
|
ctx.ui.setFooter(undefined);
|
|
531
629
|
ctx.ui.notify("Jittor footer disabled; routing enforcement is unchanged.", "info");
|
|
532
630
|
return;
|
|
533
631
|
}
|
|
534
632
|
if (action === "footer on" || action === "footer enable") {
|
|
535
|
-
enforcement.setFooterEnabled(true);
|
|
633
|
+
await enforcement.setFooterEnabled(true);
|
|
536
634
|
showFooter(ctx);
|
|
537
635
|
await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
538
636
|
ctx.ui.notify("Jittor informational footer enabled; routing enforcement is unchanged.", "info");
|
|
@@ -548,6 +646,7 @@ export function registerJittorExtension(
|
|
|
548
646
|
`Papyrus injection: ${summary.injection.runs} runs · avg ${average} chars · p95 ${p95} chars · unchanged ${summary.injection.unchangedRate === null ? "unknown" : `${(summary.injection.unchangedRate * 100).toFixed(1)}%`}`,
|
|
549
647
|
`Mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens`,
|
|
550
648
|
`Compactions: ${summary.compaction.completed} completed · ${summary.compaction.aborted} aborted · ${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`,
|
|
649
|
+
`Effectiveness: ${summary.compaction.effectivenessSamples} samples · avg reduction ${summary.compaction.averageReductionRatio === null ? "unknown" : `${(summary.compaction.averageReductionRatio * 100).toFixed(1)}%`} · mechanisms Pi/provider/extension ${summary.compaction.mechanisms["pi-native"]}/${summary.compaction.mechanisms["provider-side"]}/${summary.compaction.mechanisms.extension}`,
|
|
551
650
|
`Completeness: ${summary.completeness}`,
|
|
552
651
|
].join("\n"),
|
|
553
652
|
"info",
|
|
@@ -577,7 +676,10 @@ export function registerJittorExtension(
|
|
|
577
676
|
// path including everything a real compaction has already summarized away.
|
|
578
677
|
const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id));
|
|
579
678
|
const branchEntryIds = new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id));
|
|
580
|
-
const messageHistory = buildMessageHistoryTree(tree, activeEntryIds, branchEntryIds
|
|
679
|
+
const messageHistory = buildMessageHistoryTree(tree, activeEntryIds, branchEntryIds, {
|
|
680
|
+
...(ctx.model ? { provider: ctx.model.provider, model: ctx.model.id } : {}),
|
|
681
|
+
counters: await textTokenCounters(ctx.model?.provider, ctx.model?.id),
|
|
682
|
+
});
|
|
581
683
|
const usage = ctx.getContextUsage();
|
|
582
684
|
const ownSegments = [
|
|
583
685
|
basePromptSegment(lastObservedBasePromptTokens, lastObservedBasePromptItems),
|
|
@@ -589,7 +691,9 @@ export function registerJittorExtension(
|
|
|
589
691
|
contextWindow: ctx.model?.contextWindow ?? null,
|
|
590
692
|
segments: [...ownSegments, ...contextHub.contributedSegments()],
|
|
591
693
|
});
|
|
592
|
-
|
|
694
|
+
const opaqueSessionId = contextFingerprinter.fingerprint(`session:${ctx.sessionManager.getSessionId()}`);
|
|
695
|
+
const delta = await client.call("context.delta", { session_id: opaqueSessionId }).catch(() => null);
|
|
696
|
+
await showContextView(ctx, breakdown, delta);
|
|
593
697
|
},
|
|
594
698
|
});
|
|
595
699
|
|
|
@@ -615,7 +719,7 @@ export function registerJittorExtension(
|
|
|
615
719
|
return;
|
|
616
720
|
}
|
|
617
721
|
if (valueText === "off" || valueText === "clear") {
|
|
618
|
-
usageBudgets.setUsageTokenBudget(period, undefined);
|
|
722
|
+
await usageBudgets.setUsageTokenBudget(period, undefined);
|
|
619
723
|
ctx.ui.notify(`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget cleared.`, "info");
|
|
620
724
|
return;
|
|
621
725
|
}
|
|
@@ -624,7 +728,7 @@ export function registerJittorExtension(
|
|
|
624
728
|
ctx.ui.notify("Usage: /usage budget <hourly|daily|weekly|monthly|quarterly> <positive-tokens|off>", "warning");
|
|
625
729
|
return;
|
|
626
730
|
}
|
|
627
|
-
usageBudgets.setUsageTokenBudget(period, tokens);
|
|
731
|
+
await usageBudgets.setUsageTokenBudget(period, tokens);
|
|
628
732
|
ctx.ui.notify(
|
|
629
733
|
`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget set to ${tokens.toLocaleString()} tokens.`,
|
|
630
734
|
"info",
|
|
@@ -644,6 +748,8 @@ export function registerJittorExtension(
|
|
|
644
748
|
focusedTaskId = null;
|
|
645
749
|
finishCompactionUi();
|
|
646
750
|
compactionTelemetry = new CompactionTelemetry();
|
|
751
|
+
contextGrowthTurn = 0;
|
|
752
|
+
contextGrowth.reset();
|
|
647
753
|
localRunTelemetry.reset();
|
|
648
754
|
contextHub.reset();
|
|
649
755
|
cancelRecovery(true);
|
|
@@ -682,20 +788,40 @@ export function registerJittorExtension(
|
|
|
682
788
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
683
789
|
beginCompactionUi(ctx, event.signal);
|
|
684
790
|
const usage = ctx.getContextUsage();
|
|
791
|
+
const preparationTokens = event.preparation?.tokensBefore;
|
|
685
792
|
const metric = compactionTelemetry.begin({
|
|
686
793
|
reason: event.reason,
|
|
687
794
|
willRetry: event.willRetry,
|
|
795
|
+
mechanism: "pi-native",
|
|
796
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
797
|
+
model: ctx.model?.id ?? "unknown",
|
|
688
798
|
...(usage?.percent === null || usage?.percent === undefined ? {} : { contextPercent: usage.percent }),
|
|
689
|
-
...(
|
|
799
|
+
...(typeof preparationTokens === "number" && Number.isSafeInteger(preparationTokens) && preparationTokens > 0
|
|
800
|
+
? { contextTokens: preparationTokens, contextProvenance: "structural-estimate" as const }
|
|
801
|
+
: usage?.tokens === null || usage?.tokens === undefined
|
|
802
|
+
? {}
|
|
803
|
+
: { contextTokens: usage.tokens, contextProvenance: "provider-reported" as const }),
|
|
690
804
|
});
|
|
691
805
|
await recordMetrics(client, [metric]).catch(() => undefined);
|
|
692
806
|
});
|
|
693
807
|
|
|
694
808
|
pi.on("session_compact", async (event) => {
|
|
695
809
|
finishCompactionUi();
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
810
|
+
contextGrowth.reset();
|
|
811
|
+
const summary = event.compactionEntry?.summary;
|
|
812
|
+
await recordMetrics(client, [
|
|
813
|
+
compactionTelemetry.complete({
|
|
814
|
+
reason: event.reason,
|
|
815
|
+
willRetry: event.willRetry,
|
|
816
|
+
mechanism: event.fromExtension ? "extension" : "pi-native",
|
|
817
|
+
...(typeof summary === "string"
|
|
818
|
+
? {
|
|
819
|
+
summaryTokens: Math.ceil(summary.length / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN),
|
|
820
|
+
summaryProvenance: "structural-estimate" as const,
|
|
821
|
+
}
|
|
822
|
+
: {}),
|
|
823
|
+
}),
|
|
824
|
+
]).catch(() => undefined);
|
|
699
825
|
});
|
|
700
826
|
|
|
701
827
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
@@ -771,6 +897,8 @@ export function registerJittorExtension(
|
|
|
771
897
|
|
|
772
898
|
pi.on("tool_execution_end", async (event) => {
|
|
773
899
|
localRunTelemetry.onToolExecutionEnd(event.toolName, event.isError);
|
|
900
|
+
const classification = classifyTaskFromTools([event.toolName]);
|
|
901
|
+
compactionTelemetry.observeToolClass(`${classification.domain}-${classification.type}`, event.isError);
|
|
774
902
|
});
|
|
775
903
|
|
|
776
904
|
pi.on("after_provider_response", async (event, ctx) => {
|
|
@@ -783,7 +911,9 @@ export function registerJittorExtension(
|
|
|
783
911
|
if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
|
|
784
912
|
});
|
|
785
913
|
|
|
786
|
-
pi.on("turn_end", async (event) => {
|
|
914
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
915
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
916
|
+
if (typeof tokens === "number" && Number.isFinite(tokens)) contextGrowth.observe(++contextGrowthTurn, tokens);
|
|
787
917
|
const metrics = localRunTelemetry.completeTurn(event.message, pi.getThinkingLevel());
|
|
788
918
|
await recordMetrics(client, metrics).catch(() => undefined);
|
|
789
919
|
});
|