@danypops/pi-jittor 0.2.0 → 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.
Files changed (25) hide show
  1. package/README.md +19 -3
  2. package/docs/USAGE_PRIOR_ART.md +1 -1
  3. package/extension/src/index.ts +379 -118
  4. package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
  5. package/extension/src/observability/context-growth.ts +26 -0
  6. package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
  7. package/extension/src/observability/context-report.ts +92 -0
  8. package/extension/src/observability/context-view.ts +264 -0
  9. package/extension/src/{footer.ts → observability/footer.ts} +63 -26
  10. package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
  11. package/extension/src/observability/provider-context-snapshot.ts +246 -0
  12. package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
  13. package/extension/src/{tui.ts → observability/status.ts} +202 -72
  14. package/extension/src/observability/usage.ts +314 -0
  15. package/extension/src/optimization/model-selection-panel.ts +160 -0
  16. package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
  17. package/extension/src/service-client.ts +49 -2
  18. package/extension/src/settings-tui.ts +73 -33
  19. package/extension/src/settings.ts +40 -29
  20. package/package.json +11 -5
  21. package/extension/src/benchmark-tui.ts +0 -113
  22. package/extension/src/context-report.ts +0 -49
  23. package/extension/src/context-view.ts +0 -108
  24. package/extension/src/usage.ts +0 -324
  25. /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
@@ -1,52 +1,81 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ContextSegmentItem, ContextSnapshot } from "@danypops/jittor";
2
2
  import {
3
+ applyTaskFocusEvent,
3
4
  CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
4
- CONTEXT_HUB_CONTRIBUTION_CHANNEL,
5
- FOOTER_COMPACTION_RENDER_INTERVAL_MS,
6
- MAX_DYNAMIC_ROUTES,
7
- PAPYRUS_CONTEXT_INJECTION_CHANNEL,
8
- PAPYRUS_TASK_FOCUS_CHANNEL,
9
5
  CONTEXT_EVENT_DEDUP_LIMIT,
6
+ CONTEXT_HUB_CONTRIBUTION_CHANNEL,
10
7
  CompactionTelemetry,
11
- papyrusContextMetric,
12
- validatePapyrusContextInjection,
13
- applyTaskFocusEvent,
14
- validateTaskFocusEvent,
15
- toolLedgerSegment,
16
- TASK_DOMAINS,
17
- TASK_TYPES,
18
- USAGE_PERIODS,
19
8
  type ContextAssessment,
9
+ classifyTaskFromTools,
10
+ FOOTER_COMPACTION_RENDER_INTERVAL_MS,
11
+ HmacContextFingerprinter,
12
+ loadOpenAiTextTokenCounter,
13
+ MAX_DYNAMIC_ROUTES,
20
14
  type MetricObservation,
21
15
  type ModelCandidate,
22
16
  type ModelTaskDomain,
23
17
  type ModelTaskType,
18
+ PAPYRUS_CONTEXT_INJECTION_CHANNEL,
19
+ PAPYRUS_TASK_FOCUS_CHANNEL,
24
20
  type PolicyDecision,
21
+ papyrusContextMetric,
25
22
  type Route,
26
23
  type RouterStatus,
27
24
  type StoredMetricObservation,
25
+ TASK_DOMAINS,
26
+ TASK_TYPES,
27
+ type TextTokenCounter,
28
+ type TokenMeasurementScope,
29
+ toolLedgerSegment,
30
+ USAGE_PERIODS,
28
31
  type UsagePeriod,
32
+ validatePapyrusContextInjection,
33
+ validateTaskFocusEvent,
29
34
  } from "@danypops/jittor";
30
- import { showBenchmarkPanel } from "./benchmark-tui.ts";
31
- import { installIntegratedFooter, type CompactionProgress, type IntegratedFooterState } from "./footer.ts";
35
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
36
+ import {
37
+ basePromptSegment,
38
+ buildBasePromptItems,
39
+ buildMessageHistoryTree,
40
+ composeContextBreakdown,
41
+ messageHistorySegment,
42
+ type SessionEntryLike,
43
+ type SessionTreeNodeLike,
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";
32
56
  import { callJittor } from "./service-client.ts";
33
- import { persistentEnforcementControl, type CodexRecoveryControl, type EnforcementControl, type UsageBudgetControl } from "./settings.ts";
34
- import { showSettingsPanel } from "./settings-tui.ts";
35
- import { buildFooterBudget, formatFooterStatus, providerBudgetMetricQuery, showJittorPanel } from "./tui.ts";
36
57
  import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
37
- import { showUsagePanel } from "./usage.ts";
38
- import { CodexRecoveryCapability, SYSTEM_RECOVERY_RUNTIME, type CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
39
- import { ProviderResponseTelemetry } from "./capabilities/provider-response-telemetry.ts";
40
- import { LocalRunTelemetry } from "./capabilities/local-run-telemetry.ts";
41
- import { ContextHubCapability } from "./capabilities/context-hub.ts";
42
- import { basePromptSegment, buildBasePromptItems, buildMessageHistoryTree, composeContextBreakdown, messageHistorySegment, type SessionEntryLike, type SessionTreeNodeLike } from "./context-breakdown.ts";
43
- import { showContextView } from "./context-view.ts";
44
- import type { ContextSegmentItem } from "@danypops/jittor";
45
-
46
- export { formatFooterStatus } from "./tui.ts";
47
- export type { CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
58
+ import { type CodexRecoveryControl, type EnforcementControl, persistentEnforcementControl, type UsageBudgetControl } from "./settings.ts";
59
+ import { showSettingsPanel } from "./settings-tui.ts";
60
+
61
+ export { formatFooterStatus } from "./observability/status.ts";
62
+ export type { CodexRecoveryRuntime } from "./optimization/recovery/codex.ts";
48
63
 
49
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
+ }
50
79
  const RECOVERY_GUIDANCE = "Run /jittor off to disable blocking, or restart the daemon with: systemctl --user restart jittor.service";
51
80
 
52
81
  export interface JittorExtensionClient {
@@ -61,9 +90,9 @@ function usageBudgetControl(enforcement: EnforcementControl): UsageBudgetControl
61
90
  const candidate = enforcement as EnforcementControl & Partial<UsageBudgetControl>;
62
91
  return typeof candidate.getUsageTokenBudget === "function" && typeof candidate.setUsageTokenBudget === "function"
63
92
  ? {
64
- getUsageTokenBudget: (period) => candidate.getUsageTokenBudget!(period),
65
- setUsageTokenBudget: (period, tokens) => candidate.setUsageTokenBudget!(period, tokens),
66
- }
93
+ getUsageTokenBudget: (period) => candidate.getUsageTokenBudget!(period),
94
+ setUsageTokenBudget: (period, tokens) => candidate.setUsageTokenBudget!(period, tokens),
95
+ }
67
96
  : { getUsageTokenBudget: () => undefined, setUsageTokenBudget() {} };
68
97
  }
69
98
 
@@ -72,9 +101,9 @@ function recoveryControl(enforcement: EnforcementControl): CodexRecoveryControl
72
101
  const set = (candidate as Partial<CodexRecoveryControl>).setCodexRecoveryEnabled;
73
102
  return typeof candidate.isCodexRecoveryEnabled === "function" && typeof set === "function"
74
103
  ? {
75
- isCodexRecoveryEnabled: () => candidate.isCodexRecoveryEnabled!(),
76
- setCodexRecoveryEnabled: (enabled) => set.call(candidate, enabled),
77
- }
104
+ isCodexRecoveryEnabled: () => candidate.isCodexRecoveryEnabled!(),
105
+ setCodexRecoveryEnabled: (enabled) => set.call(candidate, enabled),
106
+ }
78
107
  : { isCodexRecoveryEnabled: () => false, setCodexRecoveryEnabled() {} };
79
108
  }
80
109
 
@@ -87,9 +116,9 @@ async function recordMetrics(client: JittorExtensionClient, metrics: MetricObser
87
116
  }
88
117
 
89
118
  async function refreshFooter(client: JittorExtensionClient, state: IntegratedFooterState, sessionId: string): Promise<void> {
90
- const status = await client.call("router.status", { session_id: sessionId }) as RouterStatus;
119
+ const status = (await client.call("router.status", { session_id: sessionId })) as RouterStatus;
91
120
  const query = providerBudgetMetricQuery(status);
92
- const metrics = query ? await client.call("metrics.query", query) as StoredMetricObservation[] : [];
121
+ const metrics = query ? ((await client.call("metrics.query", query)) as StoredMetricObservation[]) : [];
93
122
  state.providerBudget = buildFooterBudget(status, metrics);
94
123
  state.requestRender?.();
95
124
  }
@@ -98,10 +127,14 @@ function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
98
127
  if (milliseconds <= 0) return Promise.resolve();
99
128
  return new Promise((resolve, reject) => {
100
129
  const timer = setTimeout(resolve, milliseconds);
101
- signal?.addEventListener("abort", () => {
102
- clearTimeout(timer);
103
- reject(new Error("Jittor throttle cancelled"));
104
- }, { once: true });
130
+ signal?.addEventListener(
131
+ "abort",
132
+ () => {
133
+ clearTimeout(timer);
134
+ reject(new Error("Jittor throttle cancelled"));
135
+ },
136
+ { once: true },
137
+ );
105
138
  });
106
139
  }
107
140
 
@@ -114,7 +147,7 @@ async function applyRoute(pi: ExtensionAPI, ctx: ExtensionContext, route: Route)
114
147
  const model = ctx.modelRegistry.find(route.provider, route.model);
115
148
  if (!model) return false;
116
149
  if (!ctx.model || ctx.model.provider !== route.provider || ctx.model.id !== route.model) {
117
- if (!await pi.setModel(model)) return false;
150
+ if (!(await pi.setModel(model))) return false;
118
151
  }
119
152
  if (THINKING_LEVELS.has(route.thinking)) pi.setThinkingLevel(route.thinking as Parameters<ExtensionAPI["setThinkingLevel"]>[0]);
120
153
  return true;
@@ -142,7 +175,12 @@ function modelCost(model: PiRouteModel): number {
142
175
  export function benchmarkCandidatesFromPi(models: PiRouteModel[], thinking: string): ModelCandidate[] {
143
176
  const candidates: ModelCandidate[] = [];
144
177
  for (const model of models) {
145
- if (!model.provider || !model.id || candidates.some((candidate) => candidate.provider === model.provider && candidate.model === model.id)) continue;
178
+ if (
179
+ !model.provider ||
180
+ !model.id ||
181
+ candidates.some((candidate) => candidate.provider === model.provider && candidate.model === model.id)
182
+ )
183
+ continue;
146
184
  const level = supportsThinking(model, thinking) ? thinking : "off";
147
185
  candidates.push({ provider: model.provider, model: model.id, thinking: level });
148
186
  if (candidates.length >= MAX_DYNAMIC_ROUTES) break;
@@ -153,13 +191,21 @@ export function benchmarkCandidatesFromPi(models: PiRouteModel[], thinking: stri
153
191
  export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thinking: string): Route[] {
154
192
  const catalog = models
155
193
  .filter((model) => model.provider.length > 0 && model.id.length > 0)
156
- .filter((model, index, rows) => rows.findIndex((candidate) => candidate.provider === model.provider && candidate.id === model.id) === index);
194
+ .filter(
195
+ (model, index, rows) => rows.findIndex((candidate) => candidate.provider === model.provider && candidate.id === model.id) === index,
196
+ );
157
197
  if (!catalog.some((model) => model.provider === current.provider && model.id === current.id)) catalog.push(current);
158
- const currentLevel = THINKING_DESCENDING.indexOf(thinking as typeof THINKING_DESCENDING[number]);
198
+ const currentLevel = THINKING_DESCENDING.indexOf(thinking as (typeof THINKING_DESCENDING)[number]);
159
199
  const lowerLevels = THINKING_DESCENDING.slice(currentLevel >= 0 ? currentLevel + 1 : 0);
160
200
  const routes: Route[] = [];
161
201
  const add = (route: Route): void => {
162
- if (routes.length >= MAX_DYNAMIC_ROUTES || routes.some((candidate) => candidate.provider === route.provider && candidate.model === route.model && candidate.thinking === route.thinking)) return;
202
+ if (
203
+ routes.length >= MAX_DYNAMIC_ROUTES ||
204
+ routes.some(
205
+ (candidate) => candidate.provider === route.provider && candidate.model === route.model && candidate.thinking === route.thinking,
206
+ )
207
+ )
208
+ return;
163
209
  routes.push(route);
164
210
  };
165
211
  add({ provider: current.provider, model: current.id, thinking });
@@ -170,7 +216,12 @@ export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thin
170
216
  .filter((model) => model.provider !== current.provider || model.id !== current.id)
171
217
  .sort((left, right) => {
172
218
  const providerPriority = Number(left.provider !== current.provider) - Number(right.provider !== current.provider);
173
- return providerPriority || modelCost(left) - modelCost(right) || left.provider.localeCompare(right.provider) || left.id.localeCompare(right.id);
219
+ return (
220
+ providerPriority ||
221
+ modelCost(left) - modelCost(right) ||
222
+ left.provider.localeCompare(right.provider) ||
223
+ left.id.localeCompare(right.id)
224
+ );
174
225
  });
175
226
  for (const model of alternatives) {
176
227
  const level = [thinking, ...lowerLevels].find((candidate) => supportsThinking(model, candidate)) ?? "off";
@@ -182,7 +233,10 @@ export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thin
182
233
  async function syncAvailableRoutes(pi: ExtensionAPI, client: JittorExtensionClient, ctx: ExtensionContext): Promise<void> {
183
234
  const session_id = ctx.sessionManager.getSessionId();
184
235
  const secret = sessionSecretField(session_id);
185
- if (!ctx.model) { await client.call("router.available_routes", { routes: [], session_id, ...secret }); return; }
236
+ if (!ctx.model) {
237
+ await client.call("router.available_routes", { routes: [], session_id, ...secret });
238
+ return;
239
+ }
186
240
  const models = ctx.modelRegistry.getAvailable() as PiRouteModel[];
187
241
  const routes = routesFromPi(models, ctx.model as PiRouteModel, pi.getThinkingLevel());
188
242
  await client.call("router.available_routes", { routes, session_id, ...secret });
@@ -197,7 +251,13 @@ async function syncCurrentRoute(
197
251
  ): Promise<void> {
198
252
  if (!model) return;
199
253
  const session_id = ctx.sessionManager.getSessionId();
200
- await client.call("router.current_route", { provider: model.provider, model: model.id, thinking, session_id, ...sessionSecretField(session_id) });
254
+ await client.call("router.current_route", {
255
+ provider: model.provider,
256
+ model: model.id,
257
+ thinking,
258
+ session_id,
259
+ ...sessionSecretField(session_id),
260
+ });
201
261
  }
202
262
 
203
263
  function halt(ctx: ExtensionContext, reason: string): false {
@@ -215,12 +275,21 @@ async function applyDecision(
215
275
  ): Promise<boolean> {
216
276
  if (decision.action === "halt") return halt(ctx, `Jittor blocked this provider request: ${decision.reason}`);
217
277
  if (decision.action === "throttle") await delay(decision.delayMs ?? 0, ctx.signal);
218
- if (!decision.route || await applyRoute(pi, ctx, decision.route)) return true;
278
+ if (!decision.route || (await applyRoute(pi, ctx, decision.route))) return true;
219
279
  if (allowResync) {
220
280
  await syncAvailableRoutes(pi, client, ctx);
221
- return applyDecision(pi, client, ctx, await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision, false);
281
+ return applyDecision(
282
+ pi,
283
+ client,
284
+ ctx,
285
+ (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision,
286
+ false,
287
+ );
222
288
  }
223
- return halt(ctx, `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`);
289
+ return halt(
290
+ ctx,
291
+ `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`,
292
+ );
224
293
  }
225
294
 
226
295
  /**
@@ -228,22 +297,60 @@ async function applyDecision(
228
297
  * comes from pi.getThinkingLevel() at message_end time, not from the message itself -- AssistantMessage
229
298
  * has no thinking field of its own, and the level can't have changed mid-message.
230
299
  */
231
- function assistantUsageMetrics(message: unknown, observedAt: number, taskId: string | null = null, thinking: string | null = null): MetricObservation[] {
300
+ function assistantUsageMetrics(
301
+ message: unknown,
302
+ observedAt: number,
303
+ taskId: string | null = null,
304
+ thinking: string | null = null,
305
+ ): MetricObservation[] {
232
306
  if (typeof message !== "object" || message === null || Array.isArray(message)) return [];
233
307
  const value = message as Record<string, unknown>;
234
- if (value["role"] !== "assistant" || typeof value["usage"] !== "object" || value["usage"] === null) return [];
235
- const usage = value["usage"] as Record<string, unknown>;
236
- const provider = typeof value["provider"] === "string" ? value["provider"] : "unknown";
237
- const model = typeof value["model"] === "string" ? value["model"] : "unknown";
308
+ if (value.role !== "assistant" || typeof value.usage !== "object" || value.usage === null) return [];
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;
313
+ const provider = typeof value.provider === "string" ? value.provider : "unknown";
314
+ const model = typeof value.model === "string" ? value.model : "unknown";
238
315
  const scope = `${provider}:${model}`;
239
- const attributes = { provider, model, ...(taskId === null ? {} : { taskId }), ...(thinking === null || thinking.length === 0 ? {} : { thinking }) };
316
+ const attributes = {
317
+ provider,
318
+ model,
319
+ ...(taskId === null ? {} : { taskId }),
320
+ ...(thinking === null || thinking.length === 0 ? {} : { thinking }),
321
+ };
240
322
  const metrics: MetricObservation[] = [];
241
- for (const [field, metric] of [["input", "input-tokens"], ["output", "output-tokens"], ["cacheRead", "cache-read-tokens"], ["cacheWrite", "cache-write-tokens"]] 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]>) {
242
329
  const amount = usage[field];
243
- if (typeof amount === "number" && Number.isFinite(amount)) metrics.push({ source: "pi", scope, metric, value: amount, unit: "tokens", observedAt, attributes });
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
+ });
244
350
  }
245
- const cost = typeof usage["cost"] === "object" && usage["cost"] !== null ? (usage["cost"] as Record<string, unknown>)["total"] : undefined;
246
- if (typeof cost === "number" && Number.isFinite(cost)) metrics.push({ source: "pi", scope, metric: "cost", value: cost, unit: "usd", observedAt, attributes });
351
+ const cost = typeof usage.cost === "object" && usage.cost !== null ? (usage.cost as Record<string, unknown>).total : undefined;
352
+ if (typeof cost === "number" && Number.isFinite(cost))
353
+ metrics.push({ source: "pi", scope, metric: "cost", value: cost, unit: "usd", observedAt: metricObservedAt, attributes });
247
354
  return metrics;
248
355
  }
249
356
 
@@ -253,14 +360,70 @@ export function registerJittorExtension(
253
360
  enforcement: EnforcementControl = persistentEnforcementControl(),
254
361
  codexRecovery: CodexRecoveryControl = recoveryControl(enforcement),
255
362
  recoveryRuntime: CodexRecoveryRuntime = SYSTEM_RECOVERY_RUNTIME,
363
+ contextGrowth: ContextGrowthCapability = new ContextGrowthCapability(),
256
364
  ): void {
257
365
  const footerState: IntegratedFooterState = { providerBudget: null };
258
366
  const usageBudgets = usageBudgetControl(enforcement);
259
367
  let compactionTelemetry = new CompactionTelemetry();
368
+ let contextGrowthTurn = 0;
260
369
  const localRunTelemetry = new LocalRunTelemetry();
261
370
  const providerResponseTelemetry = new ProviderResponseTelemetry();
262
371
  const codexRecoveryCapability = new CodexRecoveryCapability(pi, codexRecovery, recoveryRuntime);
263
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
+ });
264
427
  const stopContextHub = pi.events?.on?.(CONTEXT_HUB_CONTRIBUTION_CHANNEL, (payload) => contextHub.observe(payload));
265
428
  // Cached from the most recent before_agent_start observation: Pi's own base system prompt is
266
429
  // only ever visible transiently inside that hook's event, so /context reuses this rather than
@@ -320,11 +483,14 @@ export function registerJittorExtension(
320
483
  // Non-blocking: compaction UI starts immediately as cold-start; if a learned estimate resolves
321
484
  // before this compaction finishes (and this is still the active compaction, not a later one),
322
485
  // upgrade the same progress object in place so the drain bar and status text switch to "learned".
323
- void client.call("compaction.estimate", {}).then((estimate) => {
324
- if (footerState.compaction !== compaction || estimate.confidence !== "learned" || estimate.ms === null) return;
325
- footerState.compaction = { ...compaction, estimatedMs: estimate.ms, confidence: "learned" };
326
- footerState.requestRender?.();
327
- }).catch(() => undefined);
486
+ void client
487
+ .call("compaction.estimate", {})
488
+ .then((estimate) => {
489
+ if (footerState.compaction !== compaction || estimate.confidence !== "learned" || estimate.ms === null) return;
490
+ footerState.compaction = { ...compaction, estimatedMs: estimate.ms, confidence: "learned" };
491
+ footerState.requestRender?.();
492
+ })
493
+ .catch(() => undefined);
328
494
  compactionTimer = setInterval(() => footerState.requestRender?.(), FOOTER_COMPACTION_RENDER_INTERVAL_MS);
329
495
  signal.addEventListener("abort", finishCompactionUi, { once: true });
330
496
  if (signal.aborted) finishCompactionUi();
@@ -334,25 +500,28 @@ export function registerJittorExtension(
334
500
  if (enforcement.isFooterEnabled()) installIntegratedFooter(ctx, footerState, () => pi.getThinkingLevel());
335
501
  else ctx.ui.setFooter(undefined);
336
502
  };
337
- const disable = (ctx: ExtensionContext): void => {
338
- enforcement.setEnabled(false);
503
+ const disable = async (ctx: ExtensionContext): Promise<void> => {
504
+ await enforcement.setEnabled(false);
339
505
  ctx.ui.setStatus("jittor", undefined);
340
506
  showFooter(ctx);
341
- ctx.ui.notify("Jittor enforcement is off (monitor-only); the informational footer remains independent and provider requests will not be blocked.", "warning");
507
+ ctx.ui.notify(
508
+ "Jittor enforcement is off (monitor-only); the informational footer remains independent and provider requests will not be blocked.",
509
+ "warning",
510
+ );
342
511
  };
343
512
  const enable = async (ctx: ExtensionContext): Promise<void> => {
344
513
  try {
345
514
  await syncCurrentRoute(pi, client, ctx);
346
515
  await syncAvailableRoutes(pi, client, ctx);
347
516
  await client.call("telemetry.poll", {});
348
- const readinessDecision = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
517
+ const readinessDecision = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
349
518
  if (readinessDecision.action === "halt") throw new Error(readinessDecision.reason);
350
- enforcement.setEnabled(true);
519
+ await enforcement.setEnabled(true);
351
520
  showFooter(ctx);
352
521
  await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
353
522
  ctx.ui.notify("Jittor enforcement enabled.", "info");
354
523
  } catch (error) {
355
- enforcement.setEnabled(false);
524
+ await enforcement.setEnabled(false);
356
525
  showFooter(ctx);
357
526
  const reason = error instanceof Error ? error.message : "readiness failed";
358
527
  ctx.ui.notify(`Jittor remains monitor-only: ${reason}. ${RECOVERY_GUIDANCE}.`, "error");
@@ -365,15 +534,15 @@ export function registerJittorExtension(
365
534
  const action = args.trim().toLowerCase();
366
535
  if (action === "" || action === "settings") {
367
536
  await showSettingsPanel(ctx, enforcement, codexRecovery, usageBudgets, {
368
- setEnforcement: async (enabled) => enabled ? enable(ctx) : disable(ctx),
537
+ setEnforcement: async (enabled) => (enabled ? enable(ctx) : disable(ctx)),
369
538
  setFooter: async (enabled) => {
370
- enforcement.setFooterEnabled(enabled);
539
+ await enforcement.setFooterEnabled(enabled);
371
540
  showFooter(ctx);
372
541
  if (enabled) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
373
542
  },
374
- setRecovery: (enabled) => {
543
+ setRecovery: async (enabled) => {
375
544
  if (!enabled) cancelRecovery(true);
376
- codexRecovery.setCodexRecoveryEnabled(enabled);
545
+ await codexRecovery.setCodexRecoveryEnabled(enabled);
377
546
  },
378
547
  });
379
548
  return;
@@ -401,11 +570,18 @@ export function registerJittorExtension(
401
570
  return;
402
571
  }
403
572
  const candidates = benchmarkCandidatesFromPi(ctx.modelRegistry.getAvailable() as PiRouteModel[], pi.getThinkingLevel());
404
- await showBenchmarkPanel(ctx, client, candidates, `${ctx.model.provider}/${ctx.model.id}`, requestedDomain ?? "general", requestedType ?? "general");
573
+ await showBenchmarkPanel(
574
+ ctx,
575
+ client,
576
+ candidates,
577
+ `${ctx.model.provider}/${ctx.model.id}`,
578
+ requestedDomain ?? "general",
579
+ requestedType ?? "general",
580
+ );
405
581
  return;
406
582
  }
407
583
  if (action === "outcome accepted" || action === "outcome rejected") {
408
- const explicitOutcome = action.endsWith("accepted") ? "accepted" as const : "rejected" as const;
584
+ const explicitOutcome = action.endsWith("accepted") ? ("accepted" as const) : ("rejected" as const);
409
585
  const outcomeMetric = localRunTelemetry.explicitOutcomeMetric(explicitOutcome);
410
586
  if (!outcomeMetric) {
411
587
  ctx.ui.notify("No completed local model run is available for an explicit outcome.", "warning");
@@ -421,46 +597,60 @@ export function registerJittorExtension(
421
597
  return;
422
598
  }
423
599
  if (action === "recovery on" || action === "recovery enable") {
424
- codexRecovery.setCodexRecoveryEnabled(true);
600
+ await codexRecovery.setCodexRecoveryEnabled(true);
425
601
  ctx.ui.notify("Jittor Codex recovery enabled; bounded retries begin only after transient failures fully settle.", "info");
426
602
  return;
427
603
  }
428
604
  if (action === "recovery off" || action === "recovery disable") {
429
605
  cancelRecovery(true);
430
- codexRecovery.setCodexRecoveryEnabled(false);
606
+ await codexRecovery.setCodexRecoveryEnabled(false);
431
607
  ctx.ui.notify("Jittor Codex recovery disabled and pending recovery cleared.", "info");
432
608
  return;
433
609
  }
434
610
  if (action === "recovery cancel") {
435
611
  cancelRecovery(true);
436
- ctx.ui.notify(`Jittor Codex recovery cooldown and attempt window cleared; recovery remains ${codexRecovery.isCodexRecoveryEnabled() ? "on" : "off"}.`, "info");
612
+ ctx.ui.notify(
613
+ `Jittor Codex recovery cooldown and attempt window cleared; recovery remains ${codexRecovery.isCodexRecoveryEnabled() ? "on" : "off"}.`,
614
+ "info",
615
+ );
616
+ return;
617
+ }
618
+ if (action === "off" || action === "disable") {
619
+ await disable(ctx);
620
+ return;
621
+ }
622
+ if (action === "on" || action === "enable") {
623
+ await enable(ctx);
437
624
  return;
438
625
  }
439
- if (action === "off" || action === "disable") { disable(ctx); return; }
440
- if (action === "on" || action === "enable") { await enable(ctx); return; }
441
626
  if (action === "footer off" || action === "footer disable") {
442
- enforcement.setFooterEnabled(false);
627
+ await enforcement.setFooterEnabled(false);
443
628
  ctx.ui.setFooter(undefined);
444
629
  ctx.ui.notify("Jittor footer disabled; routing enforcement is unchanged.", "info");
445
630
  return;
446
631
  }
447
632
  if (action === "footer on" || action === "footer enable") {
448
- enforcement.setFooterEnabled(true);
633
+ await enforcement.setFooterEnabled(true);
449
634
  showFooter(ctx);
450
635
  await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
451
636
  ctx.ui.notify("Jittor informational footer enabled; routing enforcement is unchanged.", "info");
452
637
  return;
453
638
  }
454
639
  if (action === "context") {
455
- const summary = await client.call("context.assess", {}) as ContextAssessment;
456
- const average = summary.injection.averageCharacters === null ? "unknown" : Math.round(summary.injection.averageCharacters).toLocaleString();
640
+ const summary = (await client.call("context.assess", {})) as ContextAssessment;
641
+ const average =
642
+ summary.injection.averageCharacters === null ? "unknown" : Math.round(summary.injection.averageCharacters).toLocaleString();
457
643
  const p95 = summary.injection.p95Characters === null ? "unknown" : Math.round(summary.injection.p95Characters).toLocaleString();
458
- ctx.ui.notify([
459
- `Papyrus injection: ${summary.injection.runs} runs · avg ${average} chars · p95 ${p95} chars · unchanged ${summary.injection.unchangedRate === null ? "unknown" : `${(summary.injection.unchangedRate * 100).toFixed(1)}%`}`,
460
- `Mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens`,
461
- `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`,
462
- `Completeness: ${summary.completeness}`,
463
- ].join("\n"), "info");
644
+ ctx.ui.notify(
645
+ [
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)}%`}`,
647
+ `Mix: rules ${summary.injection.ruleCharacters.toLocaleString()} chars · tasks ${summary.injection.taskCharacters.toLocaleString()} chars · estimated ${summary.injection.estimatedTokens.toLocaleString()} tokens`,
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}`,
650
+ `Completeness: ${summary.completeness}`,
651
+ ].join("\n"),
652
+ "info",
653
+ );
464
654
  return;
465
655
  }
466
656
  // Reached only for the explicit "status" keyword or any other unrecognized text; bare "" is
@@ -474,7 +664,8 @@ export function registerJittorExtension(
474
664
  });
475
665
 
476
666
  pi.registerCommand("context", {
477
- description: "Context Hub: real usage plus every segment's estimated size (base prompt, message history, tool schemas by owning extension, and whatever other extensions contributed), each tagged with how it was attributed",
667
+ description:
668
+ "Context Hub: real usage plus every segment's estimated size (base prompt, message history, tool schemas by owning extension, and whatever other extensions contributed), each tagged with how it was attributed",
478
669
  handler: async (_args, ctx) => {
479
670
  const activeToolNames = new Set(pi.getActiveTools());
480
671
  const toolSegment = toolLedgerSegment(pi.getAllTools().filter((tool) => activeToolNames.has(tool.name)));
@@ -485,15 +676,24 @@ export function registerJittorExtension(
485
676
  // path including everything a real compaction has already summarized away.
486
677
  const activeEntryIds = new Set((ctx.sessionManager.buildContextEntries() as SessionEntryLike[]).map((entry) => entry.id));
487
678
  const branchEntryIds = new Set((ctx.sessionManager.getBranch() as SessionEntryLike[]).map((entry) => entry.id));
488
- 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
+ });
489
683
  const usage = ctx.getContextUsage();
490
- const ownSegments = [basePromptSegment(lastObservedBasePromptTokens, lastObservedBasePromptItems), messageHistorySegment(messageHistory), toolSegment];
684
+ const ownSegments = [
685
+ basePromptSegment(lastObservedBasePromptTokens, lastObservedBasePromptItems),
686
+ messageHistorySegment(messageHistory),
687
+ toolSegment,
688
+ ];
491
689
  const breakdown = composeContextBreakdown({
492
690
  totalTokens: usage?.tokens ?? null,
493
691
  contextWindow: ctx.model?.contextWindow ?? null,
494
692
  segments: [...ownSegments, ...contextHub.contributedSegments()],
495
693
  });
496
- await showContextView(ctx, breakdown);
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);
497
697
  },
498
698
  });
499
699
 
@@ -503,18 +703,23 @@ export function registerJittorExtension(
503
703
  const action = args.trim().toLowerCase();
504
704
  if (action === "budget" || action.startsWith("budget ")) {
505
705
  const [, periodText, valueText] = action.split(/\s+/);
506
- const period = USAGE_PERIODS.some((candidate) => candidate.id === periodText) ? periodText as UsagePeriod : undefined;
706
+ const period = USAGE_PERIODS.some((candidate) => candidate.id === periodText) ? (periodText as UsagePeriod) : undefined;
507
707
  if (!period) {
508
- const values = USAGE_PERIODS.map(({ id, label }) => `${label}: ${usageBudgets.getUsageTokenBudget(id)?.toLocaleString() ?? "not configured"}`).join(" · ");
708
+ const values = USAGE_PERIODS.map(
709
+ ({ id, label }) => `${label}: ${usageBudgets.getUsageTokenBudget(id)?.toLocaleString() ?? "not configured"}`,
710
+ ).join(" · ");
509
711
  ctx.ui.notify(`Token budgets · ${values}`, "info");
510
712
  return;
511
713
  }
512
714
  if (valueText === undefined) {
513
- ctx.ui.notify(`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget: ${usageBudgets.getUsageTokenBudget(period)?.toLocaleString() ?? "not configured"}`, "info");
715
+ ctx.ui.notify(
716
+ `${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget: ${usageBudgets.getUsageTokenBudget(period)?.toLocaleString() ?? "not configured"}`,
717
+ "info",
718
+ );
514
719
  return;
515
720
  }
516
721
  if (valueText === "off" || valueText === "clear") {
517
- usageBudgets.setUsageTokenBudget(period, undefined);
722
+ await usageBudgets.setUsageTokenBudget(period, undefined);
518
723
  ctx.ui.notify(`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget cleared.`, "info");
519
724
  return;
520
725
  }
@@ -523,8 +728,11 @@ export function registerJittorExtension(
523
728
  ctx.ui.notify("Usage: /usage budget <hourly|daily|weekly|monthly|quarterly> <positive-tokens|off>", "warning");
524
729
  return;
525
730
  }
526
- usageBudgets.setUsageTokenBudget(period, tokens);
527
- ctx.ui.notify(`${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget set to ${tokens.toLocaleString()} tokens.`, "info");
731
+ await usageBudgets.setUsageTokenBudget(period, tokens);
732
+ ctx.ui.notify(
733
+ `${USAGE_PERIODS.find((candidate) => candidate.id === period)!.label} token budget set to ${tokens.toLocaleString()} tokens.`,
734
+ "info",
735
+ );
528
736
  return;
529
737
  }
530
738
  if (action !== "" && action !== "cost" && action !== "tokens") {
@@ -540,6 +748,8 @@ export function registerJittorExtension(
540
748
  focusedTaskId = null;
541
749
  finishCompactionUi();
542
750
  compactionTelemetry = new CompactionTelemetry();
751
+ contextGrowthTurn = 0;
752
+ contextGrowth.reset();
543
753
  localRunTelemetry.reset();
544
754
  contextHub.reset();
545
755
  cancelRecovery(true);
@@ -578,24 +788,47 @@ export function registerJittorExtension(
578
788
  pi.on("session_before_compact", async (event, ctx) => {
579
789
  beginCompactionUi(ctx, event.signal);
580
790
  const usage = ctx.getContextUsage();
791
+ const preparationTokens = event.preparation?.tokensBefore;
581
792
  const metric = compactionTelemetry.begin({
582
793
  reason: event.reason,
583
794
  willRetry: event.willRetry,
795
+ mechanism: "pi-native",
796
+ provider: ctx.model?.provider ?? "unknown",
797
+ model: ctx.model?.id ?? "unknown",
584
798
  ...(usage?.percent === null || usage?.percent === undefined ? {} : { contextPercent: usage.percent }),
585
- ...(usage?.tokens === null || usage?.tokens === undefined ? {} : { contextTokens: usage.tokens }),
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 }),
586
804
  });
587
805
  await recordMetrics(client, [metric]).catch(() => undefined);
588
806
  });
589
807
 
590
808
  pi.on("session_compact", async (event) => {
591
809
  finishCompactionUi();
592
- await recordMetrics(client, [compactionTelemetry.complete({ reason: event.reason, willRetry: event.willRetry })]).catch(() => undefined);
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);
593
825
  });
594
826
 
595
827
  pi.on("agent_settled", async (_event, ctx) => {
596
828
  if (footerState.compaction) {
597
829
  finishCompactionUi();
598
- if (compactionTelemetry.hasOpenCompaction()) await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "agent-settled-without-completion")]).catch(() => undefined);
830
+ if (compactionTelemetry.hasOpenCompaction())
831
+ await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "agent-settled-without-completion")]).catch(() => undefined);
599
832
  }
600
833
  scheduleCodexRecovery(ctx);
601
834
  if (!enforcement.isFooterEnabled()) return;
@@ -613,7 +846,7 @@ export function registerJittorExtension(
613
846
  if (event.source !== "extension") cancelRecovery(true);
614
847
  if (event.source === "extension" || !enforcement.isEnabled()) return { action: "continue" as const };
615
848
  try {
616
- const next = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
849
+ const next = (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision;
617
850
  if (next.action === "halt") {
618
851
  ctx.ui.notify(`Jittor blocked input: ${next.reason}. ${RECOVERY_GUIDANCE}.`, "warning");
619
852
  return { action: "handled" as const };
@@ -626,7 +859,9 @@ export function registerJittorExtension(
626
859
  });
627
860
 
628
861
  pi.on("model_select", async (event, ctx) => {
629
- await syncCurrentRoute(pi, client, ctx, event.model).then(() => syncAvailableRoutes(pi, client, ctx)).catch(() => undefined);
862
+ await syncCurrentRoute(pi, client, ctx, event.model)
863
+ .then(() => syncAvailableRoutes(pi, client, ctx))
864
+ .catch(() => undefined);
630
865
  if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
631
866
  });
632
867
 
@@ -644,7 +879,12 @@ export function registerJittorExtension(
644
879
  try {
645
880
  await syncCurrentRoute(pi, client, ctx);
646
881
  await syncAvailableRoutes(pi, client, ctx);
647
- await applyDecision(pi, client, ctx, await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision);
882
+ await applyDecision(
883
+ pi,
884
+ client,
885
+ ctx,
886
+ (await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() })) as PolicyDecision,
887
+ );
648
888
  await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
649
889
  } catch {
650
890
  halt(ctx, "Jittor could not verify or apply a safe route");
@@ -657,30 +897,50 @@ export function registerJittorExtension(
657
897
 
658
898
  pi.on("tool_execution_end", async (event) => {
659
899
  localRunTelemetry.onToolExecutionEnd(event.toolName, event.isError);
900
+ const classification = classifyTaskFromTools([event.toolName]);
901
+ compactionTelemetry.observeToolClass(`${classification.domain}-${classification.type}`, event.isError);
660
902
  });
661
903
 
662
904
  pi.on("after_provider_response", async (event, ctx) => {
663
905
  localRunTelemetry.onProviderResponse();
664
906
  if (ctx.model?.provider === "openai-codex") codexRecoveryCapability.notifyResponse(event.status, event.headers);
665
- const notifySchemaDrift = (message: string) => { if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected ${message}. ${RECOVERY_GUIDANCE}.`, "error"); };
907
+ const notifySchemaDrift = (message: string) => {
908
+ if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected ${message}. ${RECOVERY_GUIDANCE}.`, "error");
909
+ };
666
910
  await providerResponseTelemetry.handleProviderResponse(client, ctx.model?.provider, event.status, event.headers, notifySchemaDrift);
667
911
  if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
668
912
  });
669
913
 
670
- 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);
671
917
  const metrics = localRunTelemetry.completeTurn(event.message, pi.getThinkingLevel());
672
918
  await recordMetrics(client, metrics).catch(() => undefined);
673
919
  });
674
920
 
675
921
  pi.on("message_end", async (event, ctx) => {
676
922
  if (event.message.role === "assistant") {
677
- if (event.message.provider === "openai-codex") codexRecoveryCapability.notifyMessageEnd(event.message.stopReason, event.message.errorMessage);
678
- await providerResponseTelemetry.handleMessageEnd(client, event.message.provider, event.message.stopReason, event.message.errorMessage);
923
+ if (event.message.provider === "openai-codex")
924
+ codexRecoveryCapability.notifyMessageEnd(event.message.stopReason, event.message.errorMessage);
925
+ await providerResponseTelemetry.handleMessageEnd(
926
+ client,
927
+ event.message.provider,
928
+ event.message.stopReason,
929
+ event.message.errorMessage,
930
+ );
679
931
  }
680
932
  const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId, pi.getThinkingLevel());
681
933
  if (metrics.length > 0) {
682
- const amount = (name: string): number => metrics.filter((metric) => metric.metric === name && typeof metric.value === "number").reduce((sum, metric) => sum + (metric.value ?? 0), 0);
683
- compactionTelemetry.observeProviderUsage({ input: amount("input-tokens"), output: amount("output-tokens"), cacheRead: amount("cache-read-tokens"), cacheWrite: amount("cache-write-tokens") });
934
+ const amount = (name: string): number =>
935
+ metrics
936
+ .filter((metric) => metric.metric === name && typeof metric.value === "number")
937
+ .reduce((sum, metric) => sum + (metric.value ?? 0), 0);
938
+ compactionTelemetry.observeProviderUsage({
939
+ input: amount("input-tokens"),
940
+ output: amount("output-tokens"),
941
+ cacheRead: amount("cache-read-tokens"),
942
+ cacheWrite: amount("cache-write-tokens"),
943
+ });
684
944
  await recordMetrics(client, metrics).catch(() => undefined);
685
945
  }
686
946
  if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
@@ -688,7 +948,8 @@ export function registerJittorExtension(
688
948
 
689
949
  pi.on("session_shutdown", async (_event, ctx) => {
690
950
  finishCompactionUi();
691
- if (compactionTelemetry.hasOpenCompaction()) await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "session-shutdown")]).catch(() => undefined);
951
+ if (compactionTelemetry.hasOpenCompaction())
952
+ await recordMetrics(client, [compactionTelemetry.abort(Date.now(), "session-shutdown")]).catch(() => undefined);
692
953
  stopPapyrusContext?.();
693
954
  stopPapyrusTaskFocus?.();
694
955
  stopContextHub?.();