@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.
- package/README.md +19 -3
- package/docs/USAGE_PRIOR_ART.md +1 -1
- package/extension/src/index.ts +379 -118
- package/extension/src/{context-breakdown.ts → observability/context-breakdown.ts} +251 -47
- package/extension/src/observability/context-growth.ts +26 -0
- package/extension/src/{capabilities → observability}/context-hub.ts +1 -5
- package/extension/src/observability/context-report.ts +92 -0
- package/extension/src/observability/context-view.ts +264 -0
- package/extension/src/{footer.ts → observability/footer.ts} +63 -26
- package/extension/src/{capabilities/local-run-telemetry.ts → observability/model-run.ts} +14 -10
- package/extension/src/observability/provider-context-snapshot.ts +246 -0
- package/extension/src/{capabilities/provider-response-telemetry.ts → observability/provider-response.ts} +21 -7
- package/extension/src/{tui.ts → observability/status.ts} +202 -72
- package/extension/src/observability/usage.ts +314 -0
- package/extension/src/optimization/model-selection-panel.ts +160 -0
- package/extension/src/{capabilities/codex-recovery.ts → optimization/recovery/codex.ts} +41 -25
- package/extension/src/service-client.ts +49 -2
- package/extension/src/settings-tui.ts +73 -33
- package/extension/src/settings.ts +40 -29
- package/package.json +11 -5
- package/extension/src/benchmark-tui.ts +0 -113
- package/extension/src/context-report.ts +0 -49
- package/extension/src/context-view.ts +0 -108
- package/extension/src/usage.ts +0 -324
- /package/extension/src/{capabilities → observability}/http-headers.ts +0 -0
package/extension/src/index.ts
CHANGED
|
@@ -1,52 +1,81 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
31
|
-
import {
|
|
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 {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
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(
|
|
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 (
|
|
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
|
|
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) {
|
|
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", {
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
235
|
-
const usage = value
|
|
236
|
-
const
|
|
237
|
-
const
|
|
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 = {
|
|
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
|
|
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.
|
|
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
|
|
246
|
-
if (typeof cost === "number" && Number.isFinite(cost))
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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:
|
|
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 = [
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
...(
|
|
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
|
-
|
|
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())
|
|
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)
|
|
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(
|
|
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) => {
|
|
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")
|
|
678
|
-
|
|
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 =>
|
|
683
|
-
|
|
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())
|
|
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?.();
|