@danypops/jittor 0.10.0 → 0.11.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 (42) hide show
  1. package/README.md +8 -5
  2. package/extension/src/benchmark-tui.ts +4 -0
  3. package/extension/src/capabilities/codex-recovery.ts +127 -0
  4. package/extension/src/capabilities/http-headers.ts +5 -0
  5. package/extension/src/capabilities/local-run-telemetry.ts +104 -0
  6. package/extension/src/capabilities/provider-response-telemetry.ts +96 -0
  7. package/extension/src/footer.ts +10 -53
  8. package/extension/src/index.ts +92 -272
  9. package/extension/src/session-identity.ts +20 -0
  10. package/extension/src/tui.ts +20 -11
  11. package/package.json +1 -1
  12. package/src/adapters/sqlite-metric-store.ts +11 -2
  13. package/src/adapters/sqlite-session-identity-store.ts +45 -0
  14. package/src/cli-commands/benchmarks.ts +140 -0
  15. package/src/cli-commands/compaction.ts +17 -0
  16. package/src/cli-commands/context.ts +49 -0
  17. package/src/cli-commands/metrics.ts +296 -0
  18. package/src/cli-commands/op.ts +40 -0
  19. package/src/cli-commands/route-args.ts +15 -0
  20. package/src/cli-commands/router.ts +207 -0
  21. package/src/cli-commands/service-daemon.ts +72 -0
  22. package/src/cli-commands/session.ts +42 -0
  23. package/src/cli-commands/support.ts +33 -0
  24. package/src/cli.ts +42 -769
  25. package/src/constants.ts +7 -0
  26. package/src/daemon.ts +13 -3
  27. package/src/db.ts +15 -1
  28. package/src/operations/benchmark-operations.ts +12 -0
  29. package/src/operations/context-operations.ts +30 -0
  30. package/src/operations/metrics-operations.ts +77 -0
  31. package/src/operations/model-ranking-operations.ts +16 -0
  32. package/src/operations/router-operations.ts +19 -0
  33. package/src/operations/session-identity-operations.ts +15 -0
  34. package/src/operations/session-scope.ts +31 -0
  35. package/src/operations/types.ts +3 -0
  36. package/src/ports/metric-store.ts +2 -0
  37. package/src/ports/router-controller.ts +9 -9
  38. package/src/ports/session-identity-store.ts +5 -0
  39. package/src/providers/telemetry-sources.ts +2 -1
  40. package/src/router.ts +124 -67
  41. package/src/service.ts +60 -118
  42. package/src/session-identity-service.ts +55 -0
@@ -1,39 +1,33 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
- CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
4
- CODEX_RECOVERY_BASE_DELAY_MS,
5
- CODEX_RECOVERY_JITTER_RATIO,
6
- CODEX_RECOVERY_MAX_ATTEMPTS,
7
- CODEX_RECOVERY_MAX_DELAY_MS,
8
3
  FOOTER_COMPACTION_RENDER_INTERVAL_MS,
9
4
  MAX_DYNAMIC_ROUTES,
10
- MILLISECONDS_PER_MINUTE,
11
- MILLISECONDS_PER_SECOND,
12
5
  PAPYRUS_CONTEXT_INJECTION_CHANNEL,
13
6
  PAPYRUS_TASK_FOCUS_CHANNEL,
14
7
  CONTEXT_EVENT_DEDUP_LIMIT,
15
8
  } from "../../src/constants.ts";
16
- import { CodexRecoveryPolicy, classifyCodexFailure, type CodexFailureKind, type CodexFailureMetadata } from "../../src/domain/codex-recovery.ts";
17
9
  import { CompactionTelemetry, papyrusContextMetric, validatePapyrusContextInjection } from "../../src/domain/context-telemetry.ts";
18
10
  import { applyTaskFocusEvent, validateTaskFocusEvent } from "../../src/domain/task-focus.ts";
19
11
  import type { MetricObservation, StoredMetricObservation } from "../../src/domain/metric.ts";
20
- import { classifyTaskFromTools, modelRunMetrics, TASK_DOMAINS, TASK_TYPES, type ModelRunObservation, type ModelTaskDomain, type ModelTaskType } from "../../src/domain/model-observation.ts";
12
+ import { TASK_DOMAINS, TASK_TYPES, type ModelTaskDomain, type ModelTaskType } from "../../src/domain/model-observation.ts";
21
13
  import type { ModelCandidate } from "../../src/domain/model-ranking.ts";
22
14
  import { USAGE_PERIODS, type UsagePeriod } from "../../src/domain/usage.ts";
23
15
  import type { PolicyDecision, Route } from "../../src/policy.ts";
24
16
  import type { RouterStatus } from "../../src/ports/router-controller.ts";
25
- import { hasAnthropicRateLimitHeaders, parseAnthropicRateLimitHeaders } from "../../src/providers/anthropic-contracts.ts";
26
- import { parseCodexRateLimitHeaders } from "../../src/providers/codex.ts";
27
- import { classifyGoogleVertexFailure, googleVertexFailureMetrics, type GoogleVertexFailureMetadata } from "../../src/providers/google-vertex-contracts.ts";
28
17
  import { showBenchmarkPanel } from "./benchmark-tui.ts";
29
18
  import { installIntegratedFooter, type CompactionProgress, type IntegratedFooterState } from "./footer.ts";
30
19
  import { callJittor } from "./service-client.ts";
31
20
  import { persistentEnforcementControl, type CodexRecoveryControl, type EnforcementControl, type UsageBudgetControl } from "./settings.ts";
32
21
  import { showSettingsPanel } from "./settings-tui.ts";
33
- import { buildFooterBudget, formatFooterStatus, showJittorPanel } from "./tui.ts";
22
+ import { buildFooterBudget, formatFooterStatus, providerBudgetMetricQuery, showJittorPanel } from "./tui.ts";
23
+ import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
34
24
  import { showUsagePanel } from "./usage.ts";
25
+ import { CodexRecoveryCapability, SYSTEM_RECOVERY_RUNTIME, type CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
26
+ import { ProviderResponseTelemetry } from "./capabilities/provider-response-telemetry.ts";
27
+ import { LocalRunTelemetry } from "./capabilities/local-run-telemetry.ts";
35
28
 
36
29
  export { formatFooterStatus } from "./tui.ts";
30
+ export type { CodexRecoveryRuntime } from "./capabilities/codex-recovery.ts";
37
31
 
38
32
  const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
39
33
  const RECOVERY_GUIDANCE = "Run /jittor off to disable blocking, or restart the daemon with: systemctl --user restart jittor.service";
@@ -46,20 +40,6 @@ const daemonClient: JittorExtensionClient = {
46
40
  call: (operation, input) => callJittor(operation as Parameters<typeof callJittor>[0], input as never),
47
41
  };
48
42
 
49
- export interface CodexRecoveryRuntime {
50
- now(): number;
51
- random(): number;
52
- setTimeout(callback: () => void | Promise<void>, delayMs: number): unknown;
53
- clearTimeout(handle: unknown): void;
54
- }
55
-
56
- const SYSTEM_RECOVERY_RUNTIME: CodexRecoveryRuntime = {
57
- now: Date.now,
58
- random: Math.random,
59
- setTimeout(callback, delayMs) { return setTimeout(() => { void callback(); }, delayMs); },
60
- clearTimeout(handle) { clearTimeout(handle as ReturnType<typeof setTimeout>); },
61
- };
62
-
63
43
  function usageBudgetControl(enforcement: EnforcementControl): UsageBudgetControl {
64
44
  const candidate = enforcement as EnforcementControl & Partial<UsageBudgetControl>;
65
45
  return typeof candidate.getUsageTokenBudget === "function" && typeof candidate.setUsageTokenBudget === "function"
@@ -81,31 +61,17 @@ function recoveryControl(enforcement: EnforcementControl): CodexRecoveryControl
81
61
  : { isCodexRecoveryEnabled: () => false, setCodexRecoveryEnabled() {} };
82
62
  }
83
63
 
84
- function header(headers: Record<string, string>, name: string): string | undefined {
85
- const expected = name.toLowerCase();
86
- return Object.entries(headers).find(([key]) => key.toLowerCase() === expected)?.[1];
87
- }
88
-
89
64
  async function recordMetrics(client: JittorExtensionClient, metrics: MetricObservation[]): Promise<void> {
90
- for (const metric of metrics) await client.call("metrics.record", metric);
91
- }
92
-
93
- interface ActiveLocalModelRun {
94
- runId: string;
95
- startedAt: number;
96
- firstTokenAt: number | null;
97
- providerResponses: number;
98
- toolNames: string[];
99
- toolCalls: number;
100
- toolFailures: number;
65
+ if (metrics.length === 0) return;
66
+ // One atomic transaction rather than a per-metric RPC loop: a later observation in the same
67
+ // event failing validation, or the connection dropping mid-loop, must not leave this event's
68
+ // metrics partially persisted.
69
+ await client.call("metrics.record_batch", { observations: metrics });
101
70
  }
102
71
 
103
- async function refreshFooter(client: JittorExtensionClient, state: IntegratedFooterState): Promise<void> {
104
- const status = await client.call("router.status", {}) as RouterStatus;
105
- const provider = status.currentRoute?.provider;
106
- const query = provider === "openai-codex"
107
- ? { source: "codex-subscription", metric: "used-fraction", limit: 100, order: "desc" }
108
- : provider === "openrouter" ? { source: "openrouter", limit: 20, order: "desc" } : null;
72
+ async function refreshFooter(client: JittorExtensionClient, state: IntegratedFooterState, sessionId: string): Promise<void> {
73
+ const status = await client.call("router.status", { session_id: sessionId }) as RouterStatus;
74
+ const query = providerBudgetMetricQuery(status);
109
75
  const metrics = query ? await client.call("metrics.query", query) as StoredMetricObservation[] : [];
110
76
  state.providerBudget = buildFooterBudget(status, metrics);
111
77
  state.requestRender?.();
@@ -168,32 +134,41 @@ export function benchmarkCandidatesFromPi(models: PiRouteModel[], thinking: stri
168
134
  }
169
135
 
170
136
  export function routesFromPi(models: PiRouteModel[], current: PiRouteModel, thinking: string): Route[] {
171
- const sameProvider = models
172
- .filter((model) => model.provider === current.provider)
173
- .filter((model, index, rows) => rows.findIndex((candidate) => candidate.id === model.id) === index);
174
- if (!sameProvider.some((model) => model.id === current.id)) sameProvider.push(current);
175
- const routes: Route[] = [{ provider: current.provider, model: current.id, thinking }];
137
+ const catalog = models
138
+ .filter((model) => model.provider.length > 0 && model.id.length > 0)
139
+ .filter((model, index, rows) => rows.findIndex((candidate) => candidate.provider === model.provider && candidate.id === model.id) === index);
140
+ if (!catalog.some((model) => model.provider === current.provider && model.id === current.id)) catalog.push(current);
176
141
  const currentLevel = THINKING_DESCENDING.indexOf(thinking as typeof THINKING_DESCENDING[number]);
177
142
  const lowerLevels = THINKING_DESCENDING.slice(currentLevel >= 0 ? currentLevel + 1 : 0);
143
+ const routes: Route[] = [];
144
+ const add = (route: Route): void => {
145
+ if (routes.length >= MAX_DYNAMIC_ROUTES || routes.some((candidate) => candidate.provider === route.provider && candidate.model === route.model && candidate.thinking === route.thinking)) return;
146
+ routes.push(route);
147
+ };
148
+ add({ provider: current.provider, model: current.id, thinking });
178
149
  for (const level of lowerLevels) {
179
- if (supportsThinking(current, level)) routes.push({ provider: current.provider, model: current.id, thinking: level });
150
+ if (supportsThinking(current, level)) add({ provider: current.provider, model: current.id, thinking: level });
180
151
  }
181
- const alternatives = sameProvider
182
- .filter((model) => model.id !== current.id)
183
- .sort((left, right) => modelCost(left) - modelCost(right) || left.id.localeCompare(right.id));
152
+ const alternatives = catalog
153
+ .filter((model) => model.provider !== current.provider || model.id !== current.id)
154
+ .sort((left, right) => {
155
+ const providerPriority = Number(left.provider !== current.provider) - Number(right.provider !== current.provider);
156
+ return providerPriority || modelCost(left) - modelCost(right) || left.provider.localeCompare(right.provider) || left.id.localeCompare(right.id);
157
+ });
184
158
  for (const model of alternatives) {
185
159
  const level = [thinking, ...lowerLevels].find((candidate) => supportsThinking(model, candidate)) ?? "off";
186
- routes.push({ provider: model.provider, model: model.id, thinking: level });
187
- if (routes.length >= MAX_DYNAMIC_ROUTES) break;
160
+ add({ provider: model.provider, model: model.id, thinking: level });
188
161
  }
189
162
  return routes;
190
163
  }
191
164
 
192
165
  async function syncAvailableRoutes(pi: ExtensionAPI, client: JittorExtensionClient, ctx: ExtensionContext): Promise<void> {
193
- if (!ctx.model) { await client.call("router.available_routes", { routes: [] }); return; }
166
+ const session_id = ctx.sessionManager.getSessionId();
167
+ const secret = sessionSecretField(session_id);
168
+ if (!ctx.model) { await client.call("router.available_routes", { routes: [], session_id, ...secret }); return; }
194
169
  const models = ctx.modelRegistry.getAvailable() as PiRouteModel[];
195
170
  const routes = routesFromPi(models, ctx.model as PiRouteModel, pi.getThinkingLevel());
196
- await client.call("router.available_routes", { routes });
171
+ await client.call("router.available_routes", { routes, session_id, ...secret });
197
172
  }
198
173
 
199
174
  async function syncCurrentRoute(
@@ -204,7 +179,8 @@ async function syncCurrentRoute(
204
179
  thinking = pi.getThinkingLevel(),
205
180
  ): Promise<void> {
206
181
  if (!model) return;
207
- await client.call("router.current_route", { provider: model.provider, model: model.id, thinking });
182
+ const session_id = ctx.sessionManager.getSessionId();
183
+ await client.call("router.current_route", { provider: model.provider, model: model.id, thinking, session_id, ...sessionSecretField(session_id) });
208
184
  }
209
185
 
210
186
  function halt(ctx: ExtensionContext, reason: string): false {
@@ -225,7 +201,7 @@ async function applyDecision(
225
201
  if (!decision.route || await applyRoute(pi, ctx, decision.route)) return true;
226
202
  if (allowResync) {
227
203
  await syncAvailableRoutes(pi, client, ctx);
228
- return applyDecision(pi, client, ctx, await client.call("router.decide", {}) as PolicyDecision, false);
204
+ return applyDecision(pi, client, ctx, await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision, false);
229
205
  }
230
206
  return halt(ctx, `Jittor could not apply any authenticated Pi route after ${decision.route.provider}/${decision.route.model} became unavailable`);
231
207
  }
@@ -264,9 +240,9 @@ export function registerJittorExtension(
264
240
  const footerState: IntegratedFooterState = { providerBudget: null };
265
241
  const usageBudgets = usageBudgetControl(enforcement);
266
242
  let compactionTelemetry = new CompactionTelemetry();
267
- let localRunSequence = 0;
268
- let activeLocalRun: ActiveLocalModelRun | undefined;
269
- let lastCompletedLocalRun: ModelRunObservation | undefined;
243
+ const localRunTelemetry = new LocalRunTelemetry();
244
+ const providerResponseTelemetry = new ProviderResponseTelemetry();
245
+ const codexRecoveryCapability = new CodexRecoveryCapability(pi, codexRecovery, recoveryRuntime);
270
246
  const contextObservations = new Set<string>();
271
247
  const stopPapyrusContext = pi.events?.on?.(PAPYRUS_CONTEXT_INJECTION_CHANNEL, (payload) => {
272
248
  try {
@@ -296,71 +272,9 @@ export function registerJittorExtension(
296
272
  // Reject malformed or stale cross-extension events without retaining payloads or crashing the extension.
297
273
  }
298
274
  });
299
- const recoveryPolicy = new CodexRecoveryPolicy({
300
- baseDelayMs: CODEX_RECOVERY_BASE_DELAY_MS,
301
- maxDelayMs: CODEX_RECOVERY_MAX_DELAY_MS,
302
- maxAttempts: CODEX_RECOVERY_MAX_ATTEMPTS,
303
- attemptWindowMs: CODEX_RECOVERY_ATTEMPT_WINDOW_MS,
304
- jitterRatio: CODEX_RECOVERY_JITTER_RATIO,
305
- }, recoveryRuntime.random);
306
- let recoveryTimer: unknown;
307
- let recoveryCooldown: { until: number; attempt: number; failureKind: CodexFailureKind } | undefined;
308
- let lastCodexResponse: CodexFailureMetadata = {};
309
- let lastGoogleVertexResponse: GoogleVertexFailureMetadata = {};
310
- // The third-party "anthropic-vertex" provider (Anthropic Claude via Google Vertex) is tracked
311
- // separately from "google-vertex" (Pi's own, unrelated native Vertex provider): different code
312
- // path, different account/quota pool, and its metrics must stay distinguishable -- see
313
- // google-vertex-contracts.ts and anthropic-contracts.ts.
314
- let lastAnthropicVertexResponse: GoogleVertexFailureMetadata = {};
315
- const cancelRecovery = (resetPolicy: boolean): void => {
316
- if (recoveryTimer !== undefined) recoveryRuntime.clearTimeout(recoveryTimer);
317
- recoveryTimer = undefined;
318
- recoveryCooldown = undefined;
319
- if (resetPolicy) recoveryPolicy.cancel();
320
- };
321
- const recoveryStatusText = (): string => {
322
- const now = recoveryRuntime.now();
323
- const state = recoveryPolicy.state(now);
324
- const enabled = codexRecovery.isCodexRecoveryEnabled();
325
- const attempt = recoveryCooldown?.attempt ?? (state.pending ? state.attempts + 1 : state.attempts);
326
- const phase = recoveryCooldown
327
- ? `cooldown ${Math.ceil(Math.max(0, recoveryCooldown.until - now) / MILLISECONDS_PER_SECOND)}s`
328
- : state.pending ? "pending"
329
- : state.attempts >= CODEX_RECOVERY_MAX_ATTEMPTS ? "exhausted"
330
- : state.attempts > 0 ? "waiting" : "idle";
331
- const failureKind = recoveryCooldown?.failureKind ?? state.lastFailureKind;
332
- return [
333
- `Codex recovery: ${enabled ? "on" : "off"}`,
334
- phase,
335
- `attempt ${attempt}/${CODEX_RECOVERY_MAX_ATTEMPTS}`,
336
- `window ${CODEX_RECOVERY_ATTEMPT_WINDOW_MS / MILLISECONDS_PER_MINUTE}m`,
337
- ...(failureKind ? [failureKind] : []),
338
- ].join(" · ");
339
- };
340
- const scheduleCodexRecovery = (ctx: ExtensionContext): void => {
341
- if (!codexRecovery.isCodexRecoveryEnabled() || recoveryTimer !== undefined || !ctx.isIdle() || ctx.hasPendingMessages()) return;
342
- const plan = recoveryPolicy.plan(recoveryRuntime.now());
343
- if (plan.action === "exhausted") {
344
- recoveryPolicy.abandonFailure();
345
- if (ctx.hasUI) ctx.ui.notify(`Jittor Codex recovery stopped: ${plan.reason}.`, "warning");
346
- return;
347
- }
348
- if (plan.action !== "schedule") return;
349
- recoveryCooldown = { until: recoveryRuntime.now() + plan.delayMs, attempt: plan.attempt, failureKind: plan.failureKind };
350
- recoveryTimer = recoveryRuntime.setTimeout(async () => {
351
- recoveryTimer = undefined;
352
- recoveryCooldown = undefined;
353
- if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
354
- const attempt = recoveryPolicy.recordAttempt(recoveryRuntime.now());
355
- if (!attempt) return;
356
- pi.sendMessage({
357
- customType: "jittor-codex-recovery",
358
- content: `Retry the previous Codex request after a transient ${attempt.failureKind} failure. Automatic recovery attempt ${attempt.attempt} of ${CODEX_RECOVERY_MAX_ATTEMPTS}.`,
359
- display: false,
360
- details: { attempt: attempt.attempt, failureKind: attempt.failureKind },
361
- }, { triggerTurn: true, deliverAs: "followUp" });
362
- }, plan.delayMs);
363
- };
275
+ const cancelRecovery = (resetPolicy: boolean): void => codexRecoveryCapability.cancel(resetPolicy);
276
+ const recoveryStatusText = (): string => codexRecoveryCapability.statusText();
277
+ const scheduleCodexRecovery = (ctx: ExtensionContext): void => codexRecoveryCapability.scheduleIfIdle(ctx);
364
278
  let compactionTimer: ReturnType<typeof setInterval> | undefined;
365
279
  const finishCompactionUi = (): void => {
366
280
  if (compactionTimer) clearInterval(compactionTimer);
@@ -406,11 +320,11 @@ export function registerJittorExtension(
406
320
  await syncCurrentRoute(pi, client, ctx);
407
321
  await syncAvailableRoutes(pi, client, ctx);
408
322
  await client.call("telemetry.poll", {});
409
- const readinessDecision = await client.call("router.decide", {}) as PolicyDecision;
323
+ const readinessDecision = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
410
324
  if (readinessDecision.action === "halt") throw new Error(readinessDecision.reason);
411
325
  enforcement.setEnabled(true);
412
326
  showFooter(ctx);
413
- await refreshFooter(client, footerState);
327
+ await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
414
328
  ctx.ui.notify("Jittor enforcement enabled.", "info");
415
329
  } catch (error) {
416
330
  enforcement.setEnabled(false);
@@ -430,7 +344,7 @@ export function registerJittorExtension(
430
344
  setFooter: async (enabled) => {
431
345
  enforcement.setFooterEnabled(enabled);
432
346
  showFooter(ctx);
433
- if (enabled) await refreshFooter(client, footerState).catch(() => undefined);
347
+ if (enabled) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
434
348
  },
435
349
  setRecovery: (enabled) => {
436
350
  if (!enabled) cancelRecovery(true);
@@ -466,12 +380,12 @@ export function registerJittorExtension(
466
380
  return;
467
381
  }
468
382
  if (action === "outcome accepted" || action === "outcome rejected") {
469
- if (!lastCompletedLocalRun) {
383
+ const explicitOutcome = action.endsWith("accepted") ? "accepted" as const : "rejected" as const;
384
+ const outcomeMetric = localRunTelemetry.explicitOutcomeMetric(explicitOutcome);
385
+ if (!outcomeMetric) {
470
386
  ctx.ui.notify("No completed local model run is available for an explicit outcome.", "warning");
471
387
  return;
472
388
  }
473
- const explicitOutcome = action.endsWith("accepted") ? "accepted" as const : "rejected" as const;
474
- const outcomeMetric = modelRunMetrics({ ...lastCompletedLocalRun, explicitOutcome }).find((metric) => metric.metric === "outcome-accepted")!;
475
389
  outcomeMetric.observedAt = Date.now();
476
390
  await recordMetrics(client, [outcomeMetric]);
477
391
  ctx.ui.notify(`Recorded explicit ${explicitOutcome} outcome for the latest local model run.`, "info");
@@ -508,7 +422,7 @@ export function registerJittorExtension(
508
422
  if (action === "footer on" || action === "footer enable") {
509
423
  enforcement.setFooterEnabled(true);
510
424
  showFooter(ctx);
511
- await refreshFooter(client, footerState).catch(() => undefined);
425
+ await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
512
426
  ctx.ui.notify("Jittor informational footer enabled; routing enforcement is unchanged.", "info");
513
427
  return;
514
428
  }
@@ -577,19 +491,25 @@ export function registerJittorExtension(
577
491
  focusedTaskId = null;
578
492
  finishCompactionUi();
579
493
  compactionTelemetry = new CompactionTelemetry();
580
- activeLocalRun = undefined;
581
- lastCompletedLocalRun = undefined;
494
+ localRunTelemetry.reset();
582
495
  cancelRecovery(true);
583
- lastCodexResponse = {};
584
- lastGoogleVertexResponse = {};
585
- lastAnthropicVertexResponse = {};
496
+ providerResponseTelemetry.resetTurn();
586
497
  ctx.ui.setStatus("jittor", undefined);
587
498
  showFooter(ctx);
499
+ // Registered before any router-mutating call could plausibly happen, closing most of the
500
+ // first-touch registration window; best-effort -- a registration failure leaves this session
501
+ // unarmored (opt-in armor), never blocked.
502
+ try {
503
+ const { secret } = await client.call("session.register", { session_id: currentSessionId });
504
+ cacheSessionSecret(currentSessionId, secret);
505
+ } catch {
506
+ // Unarmored for this session; every router.* call still works exactly as before.
507
+ }
588
508
  try {
589
509
  await syncCurrentRoute(pi, client, ctx);
590
510
  await syncAvailableRoutes(pi, client, ctx);
591
511
  await client.call("telemetry.poll", {});
592
- await refreshFooter(client, footerState);
512
+ await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
593
513
  } catch {
594
514
  footerState.providerBudget = null;
595
515
  footerState.requestRender?.();
@@ -623,7 +543,7 @@ export function registerJittorExtension(
623
543
  try {
624
544
  await syncCurrentRoute(pi, client, ctx);
625
545
  await syncAvailableRoutes(pi, client, ctx);
626
- await refreshFooter(client, footerState);
546
+ await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
627
547
  } catch {
628
548
  footerState.providerBudget = null;
629
549
  footerState.requestRender?.();
@@ -634,7 +554,7 @@ export function registerJittorExtension(
634
554
  if (event.source !== "extension") cancelRecovery(true);
635
555
  if (event.source === "extension" || !enforcement.isEnabled()) return { action: "continue" as const };
636
556
  try {
637
- const next = await client.call("router.decide", {}) as PolicyDecision;
557
+ const next = await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision;
638
558
  if (next.action === "halt") {
639
559
  ctx.ui.notify(`Jittor blocked input: ${next.reason}. ${RECOVERY_GUIDANCE}.`, "warning");
640
560
  return { action: "handled" as const };
@@ -648,7 +568,7 @@ export function registerJittorExtension(
648
568
 
649
569
  pi.on("model_select", async (event, ctx) => {
650
570
  await syncCurrentRoute(pi, client, ctx, event.model).then(() => syncAvailableRoutes(pi, client, ctx)).catch(() => undefined);
651
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState).catch(() => undefined);
571
+ if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
652
572
  });
653
573
 
654
574
  pi.on("thinking_level_select", async (event, ctx) => {
@@ -658,147 +578,45 @@ export function registerJittorExtension(
658
578
  pi.on("turn_start", async (event, ctx) => {
659
579
  currentSessionId = ctx.sessionManager.getSessionId();
660
580
  compactionTelemetry.observeTurn();
661
- lastCodexResponse = {};
662
- lastGoogleVertexResponse = {};
663
- lastAnthropicVertexResponse = {};
664
- activeLocalRun = {
665
- runId: `local-${event.timestamp}-${++localRunSequence}`,
666
- startedAt: event.timestamp,
667
- firstTokenAt: null,
668
- providerResponses: 0,
669
- toolNames: [],
670
- toolCalls: 0,
671
- toolFailures: 0,
672
- };
581
+ codexRecoveryCapability.resetTurn();
582
+ providerResponseTelemetry.resetTurn();
583
+ localRunTelemetry.beginTurn(event.timestamp);
673
584
  if (!enforcement.isEnabled()) return;
674
585
  try {
675
586
  await syncCurrentRoute(pi, client, ctx);
676
587
  await syncAvailableRoutes(pi, client, ctx);
677
- await applyDecision(pi, client, ctx, await client.call("router.decide", {}) as PolicyDecision);
678
- await refreshFooter(client, footerState);
588
+ await applyDecision(pi, client, ctx, await client.call("router.decide", { session_id: ctx.sessionManager.getSessionId() }) as PolicyDecision);
589
+ await refreshFooter(client, footerState, ctx.sessionManager.getSessionId());
679
590
  } catch {
680
591
  halt(ctx, "Jittor could not verify or apply a safe route");
681
592
  }
682
593
  });
683
594
 
684
595
  pi.on("message_update", async (event) => {
685
- if (!activeLocalRun || activeLocalRun.firstTokenAt !== null) return;
686
- if (["text_delta", "thinking_delta", "toolcall_delta"].includes(event.assistantMessageEvent.type)) activeLocalRun.firstTokenAt = Date.now();
596
+ localRunTelemetry.onMessageUpdate(event.assistantMessageEvent.type);
687
597
  });
688
598
 
689
599
  pi.on("tool_execution_end", async (event) => {
690
- if (!activeLocalRun) return;
691
- activeLocalRun.toolCalls += 1;
692
- if (event.isError) activeLocalRun.toolFailures += 1;
693
- if (activeLocalRun.toolNames.length < 100) activeLocalRun.toolNames.push(event.toolName);
600
+ localRunTelemetry.onToolExecutionEnd(event.toolName, event.isError);
694
601
  });
695
602
 
696
603
  pi.on("after_provider_response", async (event, ctx) => {
697
- if (activeLocalRun) activeLocalRun.providerResponses += 1;
698
- if (ctx.model?.provider === "openai-codex") {
699
- lastCodexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
700
- }
701
- if (ctx.model?.provider === "anthropic") {
702
- const headers = new Headers(event.headers);
703
- if (hasAnthropicRateLimitHeaders(headers)) {
704
- try {
705
- await recordMetrics(client, parseAnthropicRateLimitHeaders(headers, Date.now()).metrics);
706
- } catch {
707
- if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected Anthropic telemetry schema drift. ${RECOVERY_GUIDANCE}.`, "error");
708
- }
709
- }
710
- }
711
- if (ctx.model?.provider === "anthropic-vertex") {
712
- // Best-effort only: unverified whether this passthrough ever forwards Anthropic's own
713
- // rate-limit headers. If it doesn't, hasAnthropicRateLimitHeaders is false and nothing is
714
- // recorded -- the same honest default as every other unconfirmed signal in this file.
715
- const headers = new Headers(event.headers);
716
- if (hasAnthropicRateLimitHeaders(headers)) {
717
- try {
718
- await recordMetrics(client, parseAnthropicRateLimitHeaders(headers, Date.now(), "anthropic-vertex").metrics);
719
- } catch {
720
- if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected Anthropic-on-Vertex telemetry schema drift. ${RECOVERY_GUIDANCE}.`, "error");
721
- }
722
- }
723
- // Well-evidenced regardless of headers: GCP's own quota system fronts this transport, so the
724
- // same failure classification as google-vertex applies -- see google-vertex-contracts.ts.
725
- lastAnthropicVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
726
- }
727
- if (ctx.model?.provider === "google-vertex") {
728
- lastGoogleVertexResponse = { status: event.status, ...(header(event.headers, "retry-after") ? { retryAfter: header(event.headers, "retry-after") } : {}) };
729
- }
730
- if (Object.keys(event.headers).some((name) => name.toLowerCase().startsWith("x-codex-"))) {
731
- try {
732
- const updates = parseCodexRateLimitHeaders(new Headers(event.headers), Date.now());
733
- await recordMetrics(client, updates.flatMap((update) => update.metrics));
734
- } catch {
735
- if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected Codex telemetry schema drift. ${RECOVERY_GUIDANCE}.`, "error");
736
- }
737
- }
738
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState).catch(() => undefined);
604
+ localRunTelemetry.onProviderResponse();
605
+ if (ctx.model?.provider === "openai-codex") codexRecoveryCapability.notifyResponse(event.status, event.headers);
606
+ const notifySchemaDrift = (message: string) => { if (enforcement.isEnabled()) ctx.ui.notify(`Jittor detected ${message}. ${RECOVERY_GUIDANCE}.`, "error"); };
607
+ await providerResponseTelemetry.handleProviderResponse(client, ctx.model?.provider, event.status, event.headers, notifySchemaDrift);
608
+ if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
739
609
  });
740
610
 
741
611
  pi.on("turn_end", async (event) => {
742
- const active = activeLocalRun;
743
- activeLocalRun = undefined;
744
- const message = event.message as unknown;
745
- if (!active || typeof message !== "object" || message === null || Array.isArray(message)) return;
746
- const value = message as Record<string, unknown>;
747
- if (value["role"] !== "assistant" || typeof value["provider"] !== "string" || typeof value["model"] !== "string") return;
748
- const usage = typeof value["usage"] === "object" && value["usage"] !== null ? value["usage"] as Record<string, unknown> : {};
749
- const amount = (name: string): number => typeof usage[name] === "number" && Number.isFinite(usage[name]) ? usage[name] as number : 0;
750
- const cost = typeof usage["cost"] === "object" && usage["cost"] !== null && typeof (usage["cost"] as Record<string, unknown>)["total"] === "number"
751
- ? (usage["cost"] as Record<string, number>)["total"] ?? 0 : 0;
752
- const stopReason = ["stop", "length", "toolUse", "error", "aborted"].includes(String(value["stopReason"]))
753
- ? value["stopReason"] as ModelRunObservation["stopReason"] : "unknown";
754
- const completedAt = Math.max(Date.now(), active.firstTokenAt ?? active.startedAt, active.startedAt);
755
- lastCompletedLocalRun = {
756
- runId: active.runId,
757
- provider: value["provider"],
758
- model: value["model"],
759
- thinking: pi.getThinkingLevel(),
760
- ...classifyTaskFromTools(active.toolNames),
761
- startedAt: active.startedAt,
762
- firstTokenAt: active.firstTokenAt,
763
- completedAt,
764
- inputTokens: amount("input"),
765
- outputTokens: amount("output"),
766
- cacheReadTokens: amount("cacheRead"),
767
- cacheWriteTokens: amount("cacheWrite"),
768
- costUsd: Number.isFinite(cost) && cost >= 0 ? cost : 0,
769
- providerResponses: Math.max(1, active.providerResponses),
770
- toolCalls: active.toolCalls,
771
- toolFailures: active.toolFailures,
772
- stopReason,
773
- explicitOutcome: "unknown",
774
- };
775
- await recordMetrics(client, modelRunMetrics(lastCompletedLocalRun)).catch(() => undefined);
612
+ const metrics = localRunTelemetry.completeTurn(event.message, pi.getThinkingLevel());
613
+ await recordMetrics(client, metrics).catch(() => undefined);
776
614
  });
777
615
 
778
- pi.on("message_end", async (event, _ctx) => {
779
- if (event.message.role === "assistant" && event.message.provider === "openai-codex") {
780
- if (event.message.stopReason === "error") {
781
- const failure = classifyCodexFailure(event.message.errorMessage, lastCodexResponse);
782
- if (codexRecovery.isCodexRecoveryEnabled() && failure.transient) recoveryPolicy.observeFailure(failure, recoveryRuntime.now());
783
- else cancelRecovery(true);
784
- } else if (event.message.stopReason !== "aborted") {
785
- cancelRecovery(true);
786
- }
787
- lastCodexResponse = {};
788
- }
789
- if (event.message.role === "assistant" && event.message.provider === "google-vertex") {
790
- if (event.message.stopReason === "error") {
791
- const failure = classifyGoogleVertexFailure(event.message.errorMessage, lastGoogleVertexResponse);
792
- await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now())).catch(() => undefined);
793
- }
794
- lastGoogleVertexResponse = {};
795
- }
796
- if (event.message.role === "assistant" && event.message.provider === "anthropic-vertex") {
797
- if (event.message.stopReason === "error") {
798
- const failure = classifyGoogleVertexFailure(event.message.errorMessage, lastAnthropicVertexResponse);
799
- await recordMetrics(client, googleVertexFailureMetrics(failure, Date.now(), "anthropic-vertex")).catch(() => undefined);
800
- }
801
- lastAnthropicVertexResponse = {};
616
+ pi.on("message_end", async (event, ctx) => {
617
+ if (event.message.role === "assistant") {
618
+ if (event.message.provider === "openai-codex") codexRecoveryCapability.notifyMessageEnd(event.message.stopReason, event.message.errorMessage);
619
+ await providerResponseTelemetry.handleMessageEnd(client, event.message.provider, event.message.stopReason, event.message.errorMessage);
802
620
  }
803
621
  const metrics = assistantUsageMetrics(event.message, Date.now(), focusedTaskId, pi.getThinkingLevel());
804
622
  if (metrics.length > 0) {
@@ -806,7 +624,7 @@ export function registerJittorExtension(
806
624
  compactionTelemetry.observeProviderUsage({ input: amount("input-tokens"), output: amount("output-tokens"), cacheRead: amount("cache-read-tokens"), cacheWrite: amount("cache-write-tokens") });
807
625
  await recordMetrics(client, metrics).catch(() => undefined);
808
626
  }
809
- if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState).catch(() => undefined);
627
+ if (enforcement.isFooterEnabled()) await refreshFooter(client, footerState, ctx.sessionManager.getSessionId()).catch(() => undefined);
810
628
  });
811
629
 
812
630
  pi.on("session_shutdown", async (_event, ctx) => {
@@ -815,9 +633,11 @@ export function registerJittorExtension(
815
633
  stopPapyrusContext?.();
816
634
  stopPapyrusTaskFocus?.();
817
635
  cancelRecovery(true);
818
- lastCodexResponse = {};
819
- activeLocalRun = undefined;
820
- lastCompletedLocalRun = undefined;
636
+ localRunTelemetry.reset();
637
+ const session_id = ctx.sessionManager.getSessionId();
638
+ const secret = sessionSecretField(session_id);
639
+ if (secret.session_secret) await client.call("session.release", { session_id, ...secret }).catch(() => undefined);
640
+ forgetSessionSecret(session_id);
821
641
  ctx.ui.setStatus("jittor", undefined);
822
642
  ctx.ui.setFooter(undefined);
823
643
  });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Client-side cache for this extension's own session_secret, registered with the daemon at
3
+ * session_start and released at session_shutdown (see index.ts). Keyed by sessionId rather
4
+ * than a single "current" variable, matching every router call site's own explicit sessionId.
5
+ */
6
+ const secretsBySessionId = new Map<string, string>();
7
+
8
+ export function cacheSessionSecret(sessionId: string, secret: string): void {
9
+ secretsBySessionId.set(sessionId, secret);
10
+ }
11
+
12
+ export function forgetSessionSecret(sessionId: string): void {
13
+ secretsBySessionId.delete(sessionId);
14
+ }
15
+
16
+ /** Spread into any router-mutating request body alongside session_id -- empty object when no secret is cached for this sessionId (unregistered, or registration hasn't completed yet), matching the daemon's opt-in-armor default. */
17
+ export function sessionSecretField(sessionId: string | undefined): { session_secret?: string } {
18
+ const secret = sessionId ? secretsBySessionId.get(sessionId) : undefined;
19
+ return secret ? { session_secret: secret } : {};
20
+ }