@danypops/pi-jittor 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,11 @@ export type ProviderBudget =
62
62
  label: string;
63
63
  valueText: string;
64
64
  observedAt?: number;
65
+ }
66
+ | {
67
+ kind: "unavailable";
68
+ label: string;
69
+ valueText: string;
65
70
  };
66
71
 
67
72
  export interface CompactionProgress {
@@ -221,9 +226,11 @@ function budgetSegment(
221
226
  if (budget === undefined) return undefined;
222
227
  const w = barWidth(width);
223
228
  if (!budget) return `budget ${theme.fg("dim", progressBar(null, w))} ?`;
229
+ if (budget.kind === "unavailable") return `${budget.label} ${theme.fg("warning", budget.valueText)}`;
224
230
  const stale = budget.observedAt !== undefined && now - budget.observedAt > TELEMETRY_STALE_AFTER_MS;
225
231
  const staleText = stale ? ` ${theme.fg("warning", "stale")}` : "";
226
232
  if (budget.kind === "unbounded") return `${budget.label} ${budget.valueText}${staleText}`;
233
+ if (budget.resetsAt !== undefined && budget.resetsAt <= now) return `${budget.label} ${theme.fg("warning", "reset pending")}`;
227
234
  const remaining = Math.min(1, Math.max(0, budget.remainingFraction));
228
235
  const bar = theme.fg(fillColor(1 - remaining), progressBar(remaining, w));
229
236
  const value = `${compact ? Math.round(remaining * 100) : (remaining * 100).toFixed(1)}% left`;
@@ -0,0 +1,246 @@
1
+ import {
2
+ CONTEXT_SNAPSHOT_MAX_SEGMENTS,
3
+ type ContextFingerprinter,
4
+ type ContextSegmentSource,
5
+ type ContextSnapshot,
6
+ countTextWithFallback,
7
+ StructuralTextTokenCounter,
8
+ type TextTokenCounter,
9
+ validateContextSnapshot,
10
+ } from "@danypops/jittor";
11
+
12
+ const CANONICAL_MAX_DEPTH = 16;
13
+ const CANONICAL_MAX_NODES = 10_000;
14
+ const CANONICAL_MAX_STRING_CHARACTERS = 65_536;
15
+ const CANONICAL_MAX_COLLECTION_ITEMS = 256;
16
+
17
+ export interface ProviderContextHistoryEntry {
18
+ id: string;
19
+ type: string;
20
+ message?: unknown;
21
+ summary?: string;
22
+ }
23
+
24
+ export interface ProviderContextHistoryNode {
25
+ entry: ProviderContextHistoryEntry;
26
+ children: ProviderContextHistoryNode[];
27
+ }
28
+
29
+ export interface ProviderContextHistory {
30
+ roots: readonly ProviderContextHistoryNode[];
31
+ /** Pi buildContextEntries(): compaction-aware entries that contribute to the request now. */
32
+ activeEntryIds: ReadonlySet<string>;
33
+ /** Pi getBranch(): raw current branch, including entries summarized away by compaction. */
34
+ branchEntryIds: ReadonlySet<string>;
35
+ }
36
+
37
+ export interface ProviderContextSnapshotInput {
38
+ /** Final provider payload. Ephemeral: this function returns only keyed fingerprints and sizes. */
39
+ payload: unknown;
40
+ captureId: string;
41
+ sessionId: string;
42
+ provider: string;
43
+ model: string;
44
+ capturedAt: number;
45
+ fingerprinter: ContextFingerprinter;
46
+ counters?: readonly TextTokenCounter[];
47
+ history?: ProviderContextHistory;
48
+ }
49
+
50
+ interface CanonicalValue {
51
+ text: string;
52
+ truncated: boolean;
53
+ }
54
+
55
+ interface CanonicalState {
56
+ nodes: number;
57
+ truncated: boolean;
58
+ seen: WeakSet<object>;
59
+ }
60
+
61
+ function boundedString(value: string, state: CanonicalState): string {
62
+ if (value.length <= CANONICAL_MAX_STRING_CHARACTERS) return JSON.stringify(value);
63
+ state.truncated = true;
64
+ const half = Math.floor(CANONICAL_MAX_STRING_CHARACTERS / 2);
65
+ return JSON.stringify(`${value.slice(0, half)}…[${value.length} chars]…${value.slice(-half)}`);
66
+ }
67
+
68
+ function canonical(value: unknown, depth: number, state: CanonicalState): string {
69
+ state.nodes += 1;
70
+ if (state.nodes > CANONICAL_MAX_NODES || depth > CANONICAL_MAX_DEPTH) {
71
+ state.truncated = true;
72
+ return '"[bounded]"';
73
+ }
74
+ if (value === null) return "null";
75
+ if (typeof value === "string") return boundedString(value, state);
76
+ if (typeof value === "number") return Number.isFinite(value) ? JSON.stringify(value) : '"[non-finite]"';
77
+ if (typeof value === "boolean") return value ? "true" : "false";
78
+ if (typeof value === "bigint") return JSON.stringify(`${value.toString()}n`);
79
+ if (typeof value !== "object") return JSON.stringify(`[${typeof value}]`);
80
+ if (state.seen.has(value)) {
81
+ state.truncated = true;
82
+ return '"[cycle]"';
83
+ }
84
+ state.seen.add(value);
85
+ try {
86
+ if (Array.isArray(value)) {
87
+ if (value.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
88
+ const shown = value.slice(0, CANONICAL_MAX_COLLECTION_ITEMS).map((item) => canonical(item, depth + 1, state));
89
+ if (value.length > shown.length) shown.push(JSON.stringify(`[${value.length - shown.length} omitted]`));
90
+ return `[${shown.join(",")}]`;
91
+ }
92
+ const record = value as Record<string, unknown>;
93
+ const keys = Object.keys(record).sort();
94
+ if (keys.length > CANONICAL_MAX_COLLECTION_ITEMS) state.truncated = true;
95
+ const shown = keys
96
+ .slice(0, CANONICAL_MAX_COLLECTION_ITEMS)
97
+ .map((key) => `${JSON.stringify(key)}:${canonical(record[key], depth + 1, state)}`);
98
+ if (keys.length > shown.length) shown.push(`${JSON.stringify("[omitted]")}:${keys.length - shown.length}`);
99
+ return `{${shown.join(",")}}`;
100
+ } finally {
101
+ state.seen.delete(value);
102
+ }
103
+ }
104
+
105
+ function boundedCanonical(value: unknown): CanonicalValue {
106
+ const state: CanonicalState = { nodes: 0, truncated: false, seen: new WeakSet() };
107
+ return { text: canonical(value, 0, state), truncated: state.truncated };
108
+ }
109
+
110
+ function objectRecord(value: unknown): Record<string, unknown> | null {
111
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
112
+ }
113
+
114
+ function blockSource(block: unknown, role: unknown): ContextSegmentSource {
115
+ if (role === "tool" || role === "function") return "tool-result";
116
+ const record = objectRecord(block);
117
+ const type = typeof record?.type === "string" ? record.type.toLowerCase() : "";
118
+ if (type.includes("thinking") || type.includes("reasoning")) return "thinking";
119
+ if (type.includes("tool_result") || type.includes("tool-result") || type.includes("function_call_output")) return "tool-result";
120
+ if (type.includes("tool_use") || type.includes("tool-use") || type.includes("function_call")) return "tool-call";
121
+ if (role === "system" || role === "developer") return "base-prompt";
122
+ return "conversation-message";
123
+ }
124
+
125
+ function imageLike(value: unknown): boolean {
126
+ const record = objectRecord(value);
127
+ if (!record || typeof record.type !== "string") return false;
128
+ return record.type.toLowerCase().includes("image");
129
+ }
130
+
131
+ /**
132
+ * Extracts an ordered, provider-neutral structural view from common chat/responses payload shapes.
133
+ * Unknown shapes remain one conversation segment; the payload is never returned or retained.
134
+ */
135
+ export function captureProviderContextSnapshot(input: ProviderContextSnapshotInput): ContextSnapshot {
136
+ const segments: ContextSnapshot["segments"] = [];
137
+ let truncated = false;
138
+ const fallback = new StructuralTextTokenCounter();
139
+ const push = (
140
+ value: unknown,
141
+ source: ContextSegmentSource,
142
+ logicalPath: string,
143
+ state: ContextSnapshot["segments"][number]["state"] = "active",
144
+ ): void => {
145
+ if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
146
+ truncated = true;
147
+ return;
148
+ }
149
+ const encoded = boundedCanonical(value);
150
+ truncated ||= encoded.truncated;
151
+ const measurement = imageLike(value)
152
+ ? null
153
+ : countTextWithFallback(
154
+ { text: encoded.text, scope: "context-item", provider: input.provider, model: input.model },
155
+ input.counters ?? [],
156
+ fallback,
157
+ );
158
+ segments.push({
159
+ id: input.fingerprinter.fingerprint(`segment:${source}:${logicalPath}`),
160
+ fingerprint: input.fingerprinter.fingerprint(`value:${encoded.text}`),
161
+ source,
162
+ tokens: measurement?.tokens ?? 0,
163
+ state,
164
+ requestPosition: state === "active" ? segments.filter((segment) => segment.requestPosition !== null).length : null,
165
+ });
166
+ };
167
+
168
+ const payload = objectRecord(input.payload);
169
+ if (!payload) {
170
+ push(input.payload, "conversation-message", "payload");
171
+ } else {
172
+ for (const key of ["instructions", "system", "system_instruction"] as const) {
173
+ if (payload[key] !== undefined) push(payload[key], "base-prompt", key);
174
+ }
175
+ const tools = Array.isArray(payload.tools) ? payload.tools : Array.isArray(payload.functions) ? payload.functions : [];
176
+ for (let index = 0; index < tools.length; index++) push(tools[index], "tool-definitions", `tools/${index}`);
177
+
178
+ const conversation = Array.isArray(payload.messages)
179
+ ? payload.messages
180
+ : Array.isArray(payload.input)
181
+ ? payload.input
182
+ : Array.isArray(payload.contents)
183
+ ? payload.contents
184
+ : payload.input === undefined
185
+ ? []
186
+ : [payload.input];
187
+ for (let messageIndex = 0; messageIndex < conversation.length; messageIndex++) {
188
+ const message = conversation[messageIndex];
189
+ const record = objectRecord(message);
190
+ const role = record?.role;
191
+ const content = record?.content ?? record?.parts;
192
+ if (Array.isArray(content)) {
193
+ for (let blockIndex = 0; blockIndex < content.length; blockIndex++) {
194
+ push(content[blockIndex], blockSource(content[blockIndex], role), `messages/${messageIndex}/content/${blockIndex}`);
195
+ }
196
+ } else if (content !== undefined) {
197
+ push(content, blockSource(message, role), `messages/${messageIndex}/content`);
198
+ } else {
199
+ push(message, blockSource(message, role), `messages/${messageIndex}`);
200
+ }
201
+ }
202
+ }
203
+
204
+ if (input.history) {
205
+ const visited = new Set<string>();
206
+ const stack = [...input.history.roots].reverse();
207
+ while (stack.length > 0) {
208
+ if (segments.length >= CONTEXT_SNAPSHOT_MAX_SEGMENTS) {
209
+ truncated = true;
210
+ break;
211
+ }
212
+ const node = stack.pop()!;
213
+ if (visited.has(node.entry.id)) {
214
+ truncated = true;
215
+ continue;
216
+ }
217
+ visited.add(node.entry.id);
218
+ stack.push(...[...node.children].reverse());
219
+ if (input.history.activeEntryIds.has(node.entry.id)) continue;
220
+ const state = input.history.branchEntryIds.has(node.entry.id) ? "compacted" : "inactive";
221
+ const message = objectRecord(node.entry.message);
222
+ const role = message?.role;
223
+ const content = message?.content ?? message?.parts;
224
+ if (Array.isArray(content)) {
225
+ for (let index = 0; index < content.length; index++)
226
+ push(content[index], blockSource(content[index], role), `history/${node.entry.id}/${index}`, state);
227
+ } else if (content !== undefined) {
228
+ push(content, blockSource(node.entry.message, role), `history/${node.entry.id}`, state);
229
+ } else if (node.entry.summary !== undefined) {
230
+ push(node.entry.summary, "conversation-message", `history/${node.entry.id}/summary`, state);
231
+ }
232
+ }
233
+ if (stack.length > 0) truncated = true;
234
+ }
235
+
236
+ return validateContextSnapshot({
237
+ version: 1,
238
+ snapshotId: input.fingerprinter.fingerprint(`snapshot:${input.sessionId}:${input.captureId}`),
239
+ sessionId: input.fingerprinter.fingerprint(`session:${input.sessionId}`),
240
+ provider: input.provider,
241
+ model: input.model,
242
+ capturedAt: input.capturedAt,
243
+ truncated,
244
+ segments,
245
+ });
246
+ }
@@ -7,7 +7,7 @@ import {
7
7
  parseAnthropicRateLimitHeaders,
8
8
  parseCodexRateLimitHeaders,
9
9
  } from "@danypops/jittor";
10
- import { headerValue } from "./http-headers.ts";
10
+ import { headerValue } from "../observability/http-headers.ts";
11
11
 
12
12
  export interface ProviderTelemetryClient {
13
13
  call(operation: string, input: unknown): Promise<any>;
@@ -6,11 +6,12 @@ import {
6
6
  type Route,
7
7
  type RouterStatus,
8
8
  type StoredMetricObservation,
9
+ TELEMETRY_STALE_AFTER_MS,
9
10
  } from "@danypops/jittor";
10
11
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
12
  import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
13
+ import { sessionSecretField } from "../session-identity.ts";
12
14
  import type { ProviderBudget } from "./footer.ts";
13
- import { sessionSecretField } from "./session-identity.ts";
14
15
 
15
16
  export interface JittorPanelClient {
16
17
  call(operation: string, input: unknown): Promise<any>;
@@ -104,20 +105,56 @@ function windowName(seconds: number): string {
104
105
  * could ever read -- see google-vertex-contracts.ts); the footer omits the segment entirely rather
105
106
  * than showing a `?` that can never resolve.
106
107
  */
107
- export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObservation[]): ProviderBudget | null | undefined {
108
- if (!status.ready || !status.currentRoute) return null;
108
+ type CodexTelemetryState = "available" | "missing" | "failed" | "stale";
109
+
110
+ function codexTelemetryState(status: RouterStatus, now: number): CodexTelemetryState {
111
+ const source = status.sources.find((candidate) => candidate.id === "codex-subscription");
112
+ if (!source) return "missing";
113
+ if (!source.ok) return "failed";
114
+ if (source.observedAt !== undefined && now - source.observedAt > TELEMETRY_STALE_AFTER_MS) return "stale";
115
+ return "available";
116
+ }
117
+
118
+ function unavailableCodexBudget(
119
+ label: string,
120
+ reason: "reset pending" | "telemetry unavailable" | "telemetry failed" | "telemetry stale",
121
+ ): ProviderBudget {
122
+ return { kind: "unavailable", label, valueText: reason };
123
+ }
124
+
125
+ export function buildFooterBudget(
126
+ status: RouterStatus,
127
+ metrics: StoredMetricObservation[],
128
+ now = Date.now(),
129
+ ): ProviderBudget | null | undefined {
130
+ if (!status.currentRoute) return null;
109
131
  if (status.currentRoute.provider === "openai-codex") {
110
132
  const codex = codexWindowForModel(metrics, status.currentRoute.model);
111
- if (!codex || typeof codex.value !== "number") return null;
133
+ const sourceState = codexTelemetryState(status, now);
134
+ if (!codex || typeof codex.value !== "number") {
135
+ if (sourceState === "missing") return unavailableCodexBudget("Codex", "telemetry unavailable");
136
+ if (sourceState === "failed") return unavailableCodexBudget("Codex", "telemetry failed");
137
+ if (sourceState === "stale") return unavailableCodexBudget("Codex", "telemetry stale");
138
+ return null;
139
+ }
140
+ const label = compactWindowName(Number(codex.attributes.windowSeconds ?? 0));
112
141
  const resetsAtSeconds = Number(codex.attributes.resetsAt);
142
+ const resetsAt = Number.isFinite(resetsAtSeconds) && resetsAtSeconds > 0 ? resetsAtSeconds * 1_000 : undefined;
143
+ if (resetsAt !== undefined && resetsAt <= now) return unavailableCodexBudget(label, "reset pending");
144
+ if (now - codex.observedAt > TELEMETRY_STALE_AFTER_MS) {
145
+ if (sourceState === "missing") return unavailableCodexBudget(label, "telemetry unavailable");
146
+ if (sourceState === "failed") return unavailableCodexBudget(label, "telemetry failed");
147
+ return unavailableCodexBudget(label, "telemetry stale");
148
+ }
113
149
  return {
114
150
  kind: "bounded",
115
- label: compactWindowName(Number(codex.attributes.windowSeconds ?? 0)),
151
+ label,
116
152
  remainingFraction: 1 - codex.value,
117
153
  observedAt: codex.observedAt,
118
- ...(Number.isFinite(resetsAtSeconds) && resetsAtSeconds > 0 ? { resetsAt: resetsAtSeconds * 1_000 } : {}),
154
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
119
155
  };
120
156
  }
157
+ if (!status.ready) return null;
121
158
  if (status.currentRoute.provider === "anthropic") {
122
159
  const anthropic =
123
160
  latest(
@@ -188,10 +225,12 @@ export function buildFooterBudget(status: RouterStatus, metrics: StoredMetricObs
188
225
  return undefined;
189
226
  }
190
227
 
191
- export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[]): string {
192
- const budget = buildFooterBudget(status, metrics);
228
+ export function formatFooterStatus(status: RouterStatus, metrics: StoredMetricObservation[], now = Date.now()): string {
229
+ const budget = buildFooterBudget(status, metrics, now);
193
230
  if (!budget) return "";
194
- return budget.kind === "unbounded" ? budget.valueText : `${budget.label} ${(budget.remainingFraction * 100).toFixed(1)}% left`;
231
+ if (budget.kind === "unbounded") return budget.valueText;
232
+ if (budget.kind === "unavailable") return `${budget.label} ${budget.valueText}`;
233
+ return `${budget.label} ${(budget.remainingFraction * 100).toFixed(1)}% left`;
195
234
  }
196
235
 
197
236
  function nextAction(action: PolicyAction | undefined): string {
@@ -238,10 +277,14 @@ function burnLine(rows: StoredMetricObservation[], current: StoredMetricObservat
238
277
  export function buildStatusView(status: RouterStatus, metrics: StoredMetricObservation[], now = Date.now()): string[] {
239
278
  const lines = [status.ready ? "Ready" : "Not ready"];
240
279
  const codex = status.currentRoute?.provider === "openai-codex" ? codexWindowForModel(metrics, status.currentRoute.model) : undefined;
241
- if (codex && typeof codex.value === "number") {
280
+ const budget = buildFooterBudget(status, metrics, now);
281
+ if (codex && typeof codex.value === "number" && budget?.kind === "bounded") {
242
282
  const seconds = Number(codex.attributes.windowSeconds ?? 0);
243
283
  lines.push(`Codex ${windowName(seconds)}: ${((1 - codex.value) * 100).toFixed(1)}% left`);
244
284
  lines.push(burnLine(metrics, codex, now));
285
+ } else if (status.currentRoute?.provider === "openai-codex" && budget?.kind === "unavailable") {
286
+ const seconds = Number(codex?.attributes.windowSeconds ?? 0);
287
+ lines.push(`Codex ${codex ? windowName(seconds) : "subscription"}: ${budget.valueText}`);
245
288
  }
246
289
  const openRouter =
247
290
  status.currentRoute?.provider === "openrouter"
@@ -270,6 +313,8 @@ export function buildStatusView(status: RouterStatus, metrics: StoredMetricObser
270
313
  lines.push(`Next: ${nextAction(status.lastDecision?.action)}`);
271
314
  lines.push("Telemetry:");
272
315
  const providerSources = status.sources.filter((source) => source.provider === status.currentRoute?.provider);
316
+ if (status.currentRoute?.provider === "openai-codex" && providerSources.length === 0)
317
+ lines.push(" codex-subscription: unavailable · not configured by active daemon");
273
318
  for (const source of providerSources.slice(0, HUMAN_STATUS_MAX_SOURCES)) {
274
319
  const freshness = !source.ok ? "failed" : source.observedAt !== undefined && now - source.observedAt > 120_000 ? "stale" : "fresh";
275
320
  lines.push(` ${sanitizedText(source.id)}: ${freshness} · ${source.metrics} metrics`);