@danypops/jittor 0.9.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 (43) hide show
  1. package/README.md +9 -6
  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 +100 -276
  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 -766
  25. package/src/constants.ts +7 -0
  26. package/src/daemon.ts +13 -3
  27. package/src/db.ts +15 -1
  28. package/src/domain/task-cost.ts +44 -9
  29. package/src/operations/benchmark-operations.ts +12 -0
  30. package/src/operations/context-operations.ts +30 -0
  31. package/src/operations/metrics-operations.ts +77 -0
  32. package/src/operations/model-ranking-operations.ts +16 -0
  33. package/src/operations/router-operations.ts +19 -0
  34. package/src/operations/session-identity-operations.ts +15 -0
  35. package/src/operations/session-scope.ts +31 -0
  36. package/src/operations/types.ts +3 -0
  37. package/src/ports/metric-store.ts +2 -0
  38. package/src/ports/router-controller.ts +9 -9
  39. package/src/ports/session-identity-store.ts +5 -0
  40. package/src/providers/telemetry-sources.ts +2 -1
  41. package/src/router.ts +124 -67
  42. package/src/service.ts +60 -118
  43. package/src/session-identity-service.ts +55 -0
package/src/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ROUTER_MAX_SESSION_SCOPES, ROUTER_SESSION_ID_MAX_CHARACTERS } from "./constants.ts";
1
2
  import type { MetricStore } from "./ports/metric-store.ts";
2
3
  import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult, TelemetrySourceStatus } from "./ports/router-controller.ts";
3
4
  import type { TelemetrySource } from "./ports/telemetry-source.ts";
@@ -22,22 +23,37 @@ function sameRoute(left: Route, right: Route): boolean {
22
23
  return left.provider === right.provider && left.model === right.model && left.thinking === right.thinking;
23
24
  }
24
25
 
26
+ const GLOBAL_ROUTER_SCOPE = "global";
27
+
28
+ interface RouterSessionState {
29
+ currentRoute: Route;
30
+ availableRoutes: Route[];
31
+ lastDecision: PolicyDecision | null;
32
+ previousPolicyDecision: PolicyDecision | null;
33
+ paused: boolean;
34
+ override: RouteOverride | null;
35
+ lastAccess: number;
36
+ }
37
+
38
+ function routerScope(sessionId: string | undefined): string {
39
+ if (sessionId === undefined) return GLOBAL_ROUTER_SCOPE;
40
+ if (sessionId.length === 0 || sessionId.length > ROUTER_SESSION_ID_MAX_CHARACTERS) {
41
+ throw new Error(`session_id must contain 1-${ROUTER_SESSION_ID_MAX_CHARACTERS} characters`);
42
+ }
43
+ return sessionId;
44
+ }
45
+
25
46
  export class JittorRouter implements RouterController {
26
47
  private readonly clock: () => number;
27
48
  private readonly windows = new Map<string, BudgetWindow[]>();
49
+ private readonly sessions = new Map<string, RouterSessionState>();
28
50
  private sourceStatuses: TelemetrySourceStatus[] = [];
29
- private lastDecision: PolicyDecision | null = null;
30
- private previousPolicyDecision: PolicyDecision | null = null;
31
- private paused = false;
32
- private override: RouteOverride | null = null;
33
51
  private inFlightPoll: Promise<TelemetryPollResult> | null = null;
34
- private currentRoute: Route;
35
- private availableRoutes: Route[];
52
+ private accessSequence = 0;
36
53
 
37
54
  constructor(private readonly options: JittorRouterOptions) {
38
55
  this.clock = options.clock ?? Date.now;
39
- this.currentRoute = options.currentRoute;
40
- this.availableRoutes = structuredClone(options.routes);
56
+ this.sessions.set(GLOBAL_ROUTER_SCOPE, this.newSessionState());
41
57
  }
42
58
 
43
59
  poll(): Promise<TelemetryPollResult> {
@@ -45,91 +61,102 @@ export class JittorRouter implements RouterController {
45
61
  return this.inFlightPoll;
46
62
  }
47
63
 
48
- status(): RouterStatus {
49
- this.expireOverride();
64
+ status(sessionId?: string): RouterStatus {
65
+ const state = this.sessionState(sessionId);
66
+ this.expireOverride(state);
50
67
  return {
51
- ready: this.isReady(),
52
- paused: this.paused,
68
+ ready: this.isReady(state),
69
+ paused: state.paused,
53
70
  sources: structuredClone(this.sourceStatuses),
54
- lastDecision: this.lastDecision ? structuredClone(this.lastDecision) : null,
55
- override: this.override ? structuredClone(this.override) : null,
56
- currentRoute: structuredClone(this.currentRoute),
57
- availableRoutes: structuredClone(this.availableRoutes),
71
+ lastDecision: state.lastDecision ? structuredClone(state.lastDecision) : null,
72
+ override: state.override ? structuredClone(state.override) : null,
73
+ currentRoute: structuredClone(state.currentRoute),
74
+ availableRoutes: structuredClone(state.availableRoutes),
58
75
  };
59
76
  }
60
77
 
61
- decide(): PolicyDecision {
78
+ decide(sessionId?: string): PolicyDecision {
62
79
  const now = this.clock();
63
- this.expireOverride();
64
- if (this.paused) return this.remember({ action: "halt", pressure: Number.POSITIVE_INFINITY, reason: "Jittor is paused", decidedAt: now, trace: ["manual pause"] });
65
- if (this.override) {
66
- const route = this.override.route;
67
- const action = route.provider !== this.currentRoute.provider
80
+ const state = this.sessionState(sessionId);
81
+ this.expireOverride(state);
82
+ if (state.paused) return this.remember(state, { action: "halt", pressure: Number.POSITIVE_INFINITY, reason: "Jittor is paused", decidedAt: now, trace: ["manual pause"] });
83
+ if (state.override) {
84
+ const route = state.override.route;
85
+ const action = route.provider !== state.currentRoute.provider
68
86
  ? "switch-provider"
69
- : route.model !== this.currentRoute.model
87
+ : route.model !== state.currentRoute.model
70
88
  ? "switch-model"
71
- : route.thinking !== this.currentRoute.thinking ? "lower-thinking" : "continue";
72
- return this.remember({ action, route, pressure: 0, reason: "manual route override", decidedAt: now, trace: ["manual override"] });
89
+ : route.thinking !== state.currentRoute.thinking ? "lower-thinking" : "continue";
90
+ return this.remember(state, { action, route, pressure: 0, reason: "manual route override", decidedAt: now, trace: ["manual override"] });
91
+ }
92
+ if (!this.isReady(state)) return this.remember(state, { action: "halt", pressure: Number.POSITIVE_INFINITY, reason: "required telemetry is not ready", decidedAt: now, trace: ["fail closed"] });
93
+ const activeSources = this.options.sources.filter((source) => source.provider === state.currentRoute.provider);
94
+ const activeSourceIds = new Set(activeSources.map((source) => source.id));
95
+ const requiredSourceIds = new Set(activeSources.filter((source) => source.required).map((source) => source.id));
96
+ const activeWindows = [...this.windows.entries()].filter(([sourceId]) => activeSourceIds.has(sourceId));
97
+ const requiredWindows = activeWindows.filter(([sourceId]) => requiredSourceIds.has(sourceId)).flatMap(([, windows]) => windows);
98
+ if (requiredSourceIds.size === 0 && activeWindows.every(([, windows]) => windows.length === 0)) {
99
+ return this.rememberPolicy(state, { action: "continue", pressure: 0, reason: "provider has no enforceable budget window; monitor-only", decidedAt: now, trace: ["monitor-only"] });
73
100
  }
74
- if (!this.isReady()) return this.remember({ action: "halt", pressure: Number.POSITIVE_INFINITY, reason: "required telemetry is not ready", decidedAt: now, trace: ["fail closed"] });
75
- const activeSourceIds = new Set(this.options.sources.filter((source) => source.provider === this.currentRoute.provider).map((source) => source.id));
76
- return this.rememberPolicy(evaluateRoutingPolicy({
101
+ return this.rememberPolicy(state, evaluateRoutingPolicy({
77
102
  now,
78
- windows: [...this.windows.entries()].filter(([sourceId]) => activeSourceIds.has(sourceId)).flatMap(([, windows]) => windows),
79
- currentRoute: this.currentRoute,
80
- routes: this.availableRoutes,
103
+ windows: requiredSourceIds.size > 0 && requiredWindows.length === 0 ? [] : activeWindows.flatMap(([, windows]) => windows),
104
+ currentRoute: state.currentRoute,
105
+ routes: state.availableRoutes,
81
106
  config: this.options.policy,
82
- previousDecision: this.previousPolicyDecision ?? undefined,
107
+ previousDecision: state.previousPolicyDecision ?? undefined,
83
108
  }));
84
109
  }
85
110
 
86
- pause(): RouterStatus {
87
- this.paused = true;
88
- return this.status();
111
+ pause(sessionId?: string): RouterStatus {
112
+ this.sessionState(sessionId).paused = true;
113
+ return this.status(sessionId);
89
114
  }
90
115
 
91
- resume(): RouterStatus {
92
- this.paused = false;
93
- return this.status();
116
+ resume(sessionId?: string): RouterStatus {
117
+ this.sessionState(sessionId).paused = false;
118
+ return this.status(sessionId);
94
119
  }
95
120
 
96
- setOverride(override?: RouteOverride): RouterStatus {
97
- if (!override || !this.availableRoutes.some((route) => sameRoute(route, override.route))) throw new Error("override route is not available in Pi");
121
+ setOverride(override: RouteOverride | undefined, sessionId?: string): RouterStatus {
122
+ const state = this.sessionState(sessionId);
123
+ if (!override || !state.availableRoutes.some((route) => sameRoute(route, override.route))) throw new Error("override route is not available in Pi");
98
124
  if (override.expiresAt !== null && override.expiresAt <= this.clock()) throw new Error("override expiry must be in the future");
99
- this.override = structuredClone(override);
100
- return this.status();
125
+ state.override = structuredClone(override);
126
+ return this.status(sessionId);
101
127
  }
102
128
 
103
- clearOverride(): RouterStatus {
104
- this.override = null;
105
- return this.status();
129
+ clearOverride(sessionId?: string): RouterStatus {
130
+ this.sessionState(sessionId).override = null;
131
+ return this.status(sessionId);
106
132
  }
107
133
 
108
- setCurrentRoute(route: Route): RouterStatus {
134
+ setCurrentRoute(route: Route, sessionId?: string): RouterStatus {
109
135
  if (!route.provider || !route.model || !route.thinking) throw new Error("current route is incomplete");
110
- this.currentRoute = structuredClone(route);
111
- return this.status();
136
+ this.sessionState(sessionId).currentRoute = structuredClone(route);
137
+ return this.status(sessionId);
112
138
  }
113
139
 
114
- setAvailableRoutes(routes: Route[]): RouterStatus {
140
+ setAvailableRoutes(routes: Route[], sessionId?: string): RouterStatus {
115
141
  if (!Array.isArray(routes)) throw new Error("available routes must be an array");
116
142
  const valid = routes.filter((route) => typeof route?.provider === "string" && route.provider.length > 0
117
143
  && typeof route.model === "string" && route.model.length > 0
118
144
  && typeof route.thinking === "string" && route.thinking.length > 0);
119
- this.availableRoutes = valid.filter((route, index) => valid.findIndex((candidate) => sameRoute(candidate, route)) === index).map((route) => structuredClone(route));
120
- return this.status();
145
+ this.sessionState(sessionId).availableRoutes = valid.filter((route, index) => valid.findIndex((candidate) => sameRoute(candidate, route)) === index).map((route) => structuredClone(route));
146
+ return this.status(sessionId);
121
147
  }
122
148
 
123
- applyModelRanking(candidates: Route[]): RouterStatus {
149
+ applyModelRanking(candidates: Route[], sessionId?: string): RouterStatus {
124
150
  if (!Array.isArray(candidates) || candidates.length === 0) throw new Error("model ranking must contain candidates");
151
+ const state = this.sessionState(sessionId);
125
152
  const ranked = candidates
126
153
  .filter((candidate, index) => candidates.findIndex((other) => sameRoute(other, candidate)) === index)
127
- .map((candidate) => this.availableRoutes.find((route) => sameRoute(route, candidate)))
154
+ .map((candidate) => state.availableRoutes.find((route) => sameRoute(route, candidate)))
128
155
  .filter((route): route is Route => route !== undefined);
129
- const current = ranked.find((route) => sameRoute(route, this.currentRoute));
156
+ const current = ranked.find((route) => sameRoute(route, state.currentRoute));
130
157
  if (!current) throw new Error("model ranking does not contain the current available route");
131
- this.availableRoutes = [structuredClone(current), ...ranked.filter((route) => !sameRoute(route, current)).map((route) => structuredClone(route))];
132
- return this.status();
158
+ state.availableRoutes = [structuredClone(current), ...ranked.filter((route) => !sameRoute(route, current)).map((route) => structuredClone(route))];
159
+ return this.status(sessionId);
133
160
  }
134
161
 
135
162
  private async runPoll(): Promise<TelemetryPollResult> {
@@ -148,23 +175,53 @@ export class JittorRouter implements RouterController {
148
175
  return { sources: structuredClone(statuses), observedAt: this.clock() };
149
176
  }
150
177
 
151
- private isReady(): boolean {
152
- const active = this.options.sources.filter((source) => source.provider === this.currentRoute.provider);
153
- if (active.length === 0) return false;
154
- return active.every((source) => this.sourceStatuses.some((status) => status.id === source.id && status.ok));
178
+ private newSessionState(): RouterSessionState {
179
+ return {
180
+ currentRoute: structuredClone(this.options.currentRoute),
181
+ availableRoutes: structuredClone(this.options.routes),
182
+ lastDecision: null,
183
+ previousPolicyDecision: null,
184
+ paused: false,
185
+ override: null,
186
+ lastAccess: ++this.accessSequence,
187
+ };
188
+ }
189
+
190
+ private sessionState(sessionId?: string): RouterSessionState {
191
+ const scope = routerScope(sessionId);
192
+ const existing = this.sessions.get(scope);
193
+ if (existing) {
194
+ existing.lastAccess = ++this.accessSequence;
195
+ return existing;
196
+ }
197
+ if (this.sessions.size >= ROUTER_MAX_SESSION_SCOPES) {
198
+ const oldest = [...this.sessions.entries()]
199
+ .filter(([key]) => key !== GLOBAL_ROUTER_SCOPE)
200
+ .sort((left, right) => left[1].lastAccess - right[1].lastAccess)[0];
201
+ if (oldest) this.sessions.delete(oldest[0]);
202
+ }
203
+ const created = this.newSessionState();
204
+ this.sessions.set(scope, created);
205
+ return created;
206
+ }
207
+
208
+ private isReady(state: RouterSessionState): boolean {
209
+ if (state.availableRoutes.length === 0) return false;
210
+ const required = this.options.sources.filter((source) => source.provider === state.currentRoute.provider && source.required);
211
+ return required.every((source) => this.sourceStatuses.some((status) => status.id === source.id && status.ok));
155
212
  }
156
213
 
157
- private expireOverride(): void {
158
- if (this.override?.expiresAt !== null && this.override && this.override.expiresAt <= this.clock()) this.override = null;
214
+ private expireOverride(state: RouterSessionState): void {
215
+ if (state.override?.expiresAt !== null && state.override && state.override.expiresAt <= this.clock()) state.override = null;
159
216
  }
160
217
 
161
- private remember(decision: PolicyDecision): PolicyDecision {
162
- this.lastDecision = decision;
218
+ private remember(state: RouterSessionState, decision: PolicyDecision): PolicyDecision {
219
+ state.lastDecision = decision;
163
220
  return structuredClone(decision);
164
221
  }
165
222
 
166
- private rememberPolicy(decision: PolicyDecision): PolicyDecision {
167
- this.previousPolicyDecision = decision;
168
- return this.remember(decision);
223
+ private rememberPolicy(state: RouterSessionState, decision: PolicyDecision): PolicyDecision {
224
+ state.previousPolicyDecision = decision;
225
+ return this.remember(state, decision);
169
226
  }
170
227
  }
package/src/service.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/daemon-kit/http";
2
- import { COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES, CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS, CONTEXT_ASSESSMENT_QUERY_LIMIT, MAX_USAGE_BUCKETS, PRUNE_MIN_AGE_MS, SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES, TASK_COST_QUERY_LIMIT, USAGE_MAX_DISTINCT_SCOPES } from "./constants.ts";
2
+ import { SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
3
+ import { InvalidSessionSecretError, SessionIdentity, type RegisterSessionIdentityResult } from "./session-identity-service.ts";
3
4
  import { VERSION } from "./version.ts";
4
- import { validateMetricObservation, type MetricObservation, type MetricQuery, type StoredMetricObservation } from "./domain/metric.ts";
5
- import { assessContextTelemetry, estimateCompactionDuration, type CompactionDurationEstimate, type ContextAssessment } from "./domain/context-telemetry.ts";
6
- import { buildTaskCostSummary, type TaskCostSummary } from "./domain/task-cost.ts";
5
+ import type { MetricObservation, MetricQuery, StoredMetricObservation } from "./domain/metric.ts";
6
+ import type { CompactionDurationEstimate, ContextAssessment } from "./domain/context-telemetry.ts";
7
+ import type { TaskCostSummary } from "./domain/task-cost.ts";
7
8
  import type { UsageAggregateRow } from "./domain/usage.ts";
8
9
  import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
9
10
  import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
@@ -12,9 +13,18 @@ import type { BenchmarkController } from "./ports/benchmark-controller.ts";
12
13
  import type { MetricStore } from "./ports/metric-store.ts";
13
14
  import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult } from "./ports/router-controller.ts";
14
15
  import type { PolicyDecision, Route } from "./policy.ts";
16
+ import { metricsOperations } from "./operations/metrics-operations.ts";
17
+ import { benchmarkOperations } from "./operations/benchmark-operations.ts";
18
+ import { contextOperations } from "./operations/context-operations.ts";
19
+ import { routerOperations } from "./operations/router-operations.ts";
20
+ import { modelRankingOperations } from "./operations/model-ranking-operations.ts";
21
+ import { sessionIdentityOperations } from "./operations/session-identity-operations.ts";
22
+ import { routerMutationAuthorizer } from "./operations/session-scope.ts";
23
+ import type { OperationHandlerMap } from "./operations/types.ts";
15
24
 
16
25
  export const EXPECTED_OPERATION_NAMES = [
17
26
  "metrics.record",
27
+ "metrics.record_batch",
18
28
  "metrics.query",
19
29
  "metrics.distinct_scopes",
20
30
  "metrics.usage_series",
@@ -23,6 +33,8 @@ export const EXPECTED_OPERATION_NAMES = [
23
33
  "benchmark.refresh",
24
34
  "benchmark.status",
25
35
  "benchmark.query",
36
+ "session.register",
37
+ "session.release",
26
38
  "models.rank",
27
39
  "context.assess",
28
40
  "compaction.estimate",
@@ -39,8 +51,12 @@ export const EXPECTED_OPERATION_NAMES = [
39
51
  ] as const;
40
52
 
41
53
  export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
54
+ interface RouterScopeInput { session_id?: string; session_secret?: string }
42
55
  export interface OperationInputs {
56
+ "session.register": { session_id: string };
57
+ "session.release": { session_id: string; session_secret?: string };
43
58
  "metrics.record": MetricObservation;
59
+ "metrics.record_batch": { observations: MetricObservation[] };
44
60
  "metrics.query": MetricQuery;
45
61
  "metrics.distinct_scopes": { source: string; since: number; until: number; limit?: number };
46
62
  "metrics.usage_series": { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number };
@@ -49,22 +65,25 @@ export interface OperationInputs {
49
65
  "benchmark.refresh": { force?: boolean };
50
66
  "benchmark.status": Record<string, never>;
51
67
  "benchmark.query": BenchmarkQuery;
52
- "models.rank": ModelRecommendationInput;
68
+ "models.rank": ModelRecommendationInput & RouterScopeInput;
53
69
  "context.assess": { since?: number; until?: number };
54
70
  "compaction.estimate": Record<string, never>;
55
71
  "service.checkpoint": Record<string, never>;
56
72
  "telemetry.poll": Record<string, never>;
57
- "router.status": Record<string, never>;
58
- "router.decide": Record<string, never>;
59
- "router.pause": Record<string, never>;
60
- "router.resume": Record<string, never>;
61
- "router.override": RouteOverride;
62
- "router.clear_override": Record<string, never>;
63
- "router.current_route": Route;
64
- "router.available_routes": { routes: Route[] };
73
+ "router.status": RouterScopeInput;
74
+ "router.decide": RouterScopeInput;
75
+ "router.pause": RouterScopeInput;
76
+ "router.resume": RouterScopeInput;
77
+ "router.override": RouteOverride & RouterScopeInput;
78
+ "router.clear_override": RouterScopeInput;
79
+ "router.current_route": Route & RouterScopeInput;
80
+ "router.available_routes": { routes: Route[] } & RouterScopeInput;
65
81
  }
66
82
  export interface OperationOutputs {
83
+ "session.register": RegisterSessionIdentityResult;
84
+ "session.release": { released: boolean };
67
85
  "metrics.record": StoredMetricObservation;
86
+ "metrics.record_batch": StoredMetricObservation[];
68
87
  "metrics.query": StoredMetricObservation[];
69
88
  "metrics.distinct_scopes": string[];
70
89
  "metrics.usage_series": { rows: UsageAggregateRow[]; truncated: boolean };
@@ -89,6 +108,7 @@ export interface OperationOutputs {
89
108
  }
90
109
 
91
110
  export class UnknownOperationError extends Error {}
111
+ export { InvalidSessionSecretError };
92
112
 
93
113
  class UnavailableModelRanker implements ModelRanker {
94
114
  rank(): ModelRankingResult { throw new Error("model ranking is not configured"); }
@@ -114,12 +134,30 @@ class UnavailableRouter implements RouterController {
114
134
  }
115
135
 
116
136
  export class JittorService {
137
+ private readonly router: RouterController;
138
+ private readonly operations: OperationHandlerMap;
139
+
117
140
  constructor(
118
141
  private readonly metrics: MetricStore,
119
- private readonly router: RouterController = new UnavailableRouter(),
120
- private readonly benchmarks: BenchmarkController = new UnavailableBenchmarkController(),
121
- private readonly modelRanker: ModelRanker = new UnavailableModelRanker(),
122
- ) {}
142
+ router: RouterController = new UnavailableRouter(),
143
+ benchmarks: BenchmarkController = new UnavailableBenchmarkController(),
144
+ modelRanker: ModelRanker = new UnavailableModelRanker(),
145
+ sessionIdentity?: SessionIdentity,
146
+ ) {
147
+ this.router = router;
148
+ const authorize = routerMutationAuthorizer(sessionIdentity);
149
+ // Each capability module owns a disjoint, bounded slice of EXPECTED_OPERATION_NAMES and only
150
+ // the collaborators it needs -- adding a new operation domain means adding a new module here,
151
+ // not another switch case in a single responsibility magnet.
152
+ this.operations = {
153
+ ...metricsOperations(metrics),
154
+ ...benchmarkOperations(benchmarks),
155
+ ...contextOperations(metrics),
156
+ ...routerOperations(router, authorize),
157
+ ...modelRankingOperations(modelRanker, router, authorize),
158
+ ...sessionIdentityOperations(sessionIdentity),
159
+ };
160
+ }
123
161
 
124
162
  operationNames(): OperationName[] {
125
163
  return [...EXPECTED_OPERATION_NAMES];
@@ -128,110 +166,13 @@ export class JittorService {
128
166
  async execute<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]>;
129
167
  async execute(operation: string, input: Record<string, unknown>): Promise<unknown>;
130
168
  async execute(operation: string, input: Record<string, unknown> = {}): Promise<unknown> {
131
- switch (operation) {
132
- case "metrics.record": return this.metrics.record(validateMetricObservation(input));
133
- case "metrics.query": return this.metrics.query(input as MetricQuery);
134
- case "metrics.distinct_scopes": {
135
- const source = input["source"];
136
- const since = input["since"];
137
- const until = input["until"];
138
- if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
139
- if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
140
- throw new Error("distinct scopes requires non-negative ordered integer bounds");
141
- }
142
- const requestedLimit = input["limit"];
143
- const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
144
- return this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit });
145
- }
146
- case "metrics.usage_series": {
147
- const source = input["source"];
148
- const since = input["since"];
149
- const until = input["until"];
150
- const bucketSizeMs = input["bucketSizeMs"];
151
- const bucketCount = input["bucketCount"];
152
- if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
153
- if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
154
- throw new Error("usage series requires non-negative ordered integer bounds");
155
- }
156
- if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0) throw new Error("bucketSizeMs must be a positive number");
157
- if (!Number.isInteger(bucketCount) || (bucketCount as number) <= 0 || (bucketCount as number) > MAX_USAGE_BUCKETS) {
158
- throw new Error(`bucketCount must be a positive integer up to ${MAX_USAGE_BUCKETS}`);
159
- }
160
- const requestedScopeLimit = input["scopeLimit"];
161
- const scopeLimit = Number.isFinite(requestedScopeLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedScopeLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
162
- const scopes = this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit: scopeLimit });
163
- // More distinct scopes may exist beyond this bounded list -- that is the only remaining
164
- // truncation risk once aggregation replaces a per-scope raw-row fetch (see aggregateUsage's
165
- // own doc comment for the incident this was built to stop repeating).
166
- const truncated = scopes.length >= scopeLimit;
167
- const rows = scopes.length === 0 ? [] : this.metrics.aggregateUsage({
168
- source, scopes, since: since as number, until: until as number, bucketSizeMs, bucketCount: bucketCount as number,
169
- });
170
- return { rows, truncated };
171
- }
172
- case "metrics.cost_by_task": {
173
- const since = input["since"];
174
- const until = input["until"];
175
- if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
176
- throw new Error("cost by task requires non-negative ordered integer bounds");
177
- }
178
- const rows = this.metrics.query({ source: "pi", since: since as number, until: until as number, order: "desc", limit: TASK_COST_QUERY_LIMIT });
179
- return buildTaskCostSummary(rows, { since: since as number, until: until as number, truncated: rows.length >= TASK_COST_QUERY_LIMIT });
180
- }
181
- case "metrics.prune": {
182
- const before = input["before"];
183
- if (typeof before !== "number") throw new Error("before is required");
184
- const force = input["force"] === true;
185
- const minCutoff = Date.now() - PRUNE_MIN_AGE_MS;
186
- if (!force && before > minCutoff) {
187
- throw new Error(`refusing to prune metrics newer than ${new Date(minCutoff).toISOString()} without force: true (this looked like it could delete recent or live data)`);
188
- }
189
- return { deleted: this.metrics.pruneBefore(before) };
190
- }
191
- case "benchmark.refresh": return this.benchmarks.refresh(input["force"] === true);
192
- case "benchmark.status": return this.benchmarks.status();
193
- case "benchmark.query": return this.benchmarks.query(input as unknown as BenchmarkQuery);
194
- case "models.rank": {
195
- const result = this.modelRanker.rank(input as unknown as ModelRecommendationInput);
196
- if (result.automaticSelection && this.router.applyModelRanking) this.router.applyModelRanking(result.ranked.map((item) => item.candidate));
197
- return result;
198
- }
199
- case "context.assess": {
200
- const until = input["until"] === undefined ? Date.now() : input["until"];
201
- const since = input["since"] === undefined && typeof until === "number" ? Math.max(0, until - CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS) : input["since"];
202
- if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) throw new Error("context assessment requires non-negative ordered integer bounds");
203
- const query = { since: since as number, until: until as number, order: "asc" as const, limit: CONTEXT_ASSESSMENT_QUERY_LIMIT };
204
- const injections = this.metrics.query({ ...query, source: "papyrus-context", metric: "injected-characters" });
205
- const compactions = this.metrics.query({ ...query, source: "pi-context" });
206
- return assessContextTelemetry(injections, compactions, {
207
- since: since as number,
208
- until: until as number,
209
- truncated: injections.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT || compactions.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT,
210
- });
211
- }
212
- case "compaction.estimate": {
213
- const rows = this.metrics.query({
214
- source: "pi-context", scope: "compaction", metric: "compaction-duration",
215
- order: "desc", limit: COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES,
216
- });
217
- return estimateCompactionDuration(rows);
218
- }
219
- case "service.checkpoint": this.metrics.checkpoint(); return { ok: true };
220
- case "telemetry.poll": return this.router.poll();
221
- case "router.status": return this.router.status();
222
- case "router.decide": return this.router.decide();
223
- case "router.pause": return this.router.pause();
224
- case "router.resume": return this.router.resume();
225
- case "router.override": return this.router.setOverride(input as unknown as RouteOverride);
226
- case "router.clear_override": return this.router.clearOverride();
227
- case "router.current_route": return this.router.setCurrentRoute(input as unknown as Route);
228
- case "router.available_routes": return this.router.setAvailableRoutes(Array.isArray(input["routes"]) ? input["routes"] as Route[] : []);
229
- default: throw new UnknownOperationError(`unknown operation: ${operation}`);
230
- }
169
+ const handler = this.operations[operation];
170
+ if (!handler) throw new UnknownOperationError(`unknown operation: ${operation}`);
171
+ return handler(input);
231
172
  }
232
173
 
233
174
  ready(): boolean {
234
- return this.router.status().ready;
175
+ return this.router.status(undefined).ready;
235
176
  }
236
177
 
237
178
  close(): void {
@@ -284,6 +225,7 @@ export function createApp(options: JittorAppOptions): { fetch(request: Request):
284
225
  return json({ result: await options.service.execute(body.op, input) });
285
226
  } catch (error) {
286
227
  if (error instanceof UnknownOperationError) return json({ error: error.message }, 404);
228
+ if (error instanceof InvalidSessionSecretError) return json({ error: error.message }, 403);
287
229
  return json({ error: error instanceof Error ? error.message : String(error) }, 400);
288
230
  }
289
231
  },
@@ -0,0 +1,55 @@
1
+ import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/daemon-kit/session-identity";
2
+ import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
3
+
4
+ export interface RegisterSessionIdentityResult {
5
+ sessionId: string;
6
+ secret: string;
7
+ }
8
+
9
+ /** Thrown when a session_id has a registered identity but the caller did not present a matching session_secret. Mapped to HTTP 403 in service.ts, separate from generic validation's 400. */
10
+ export class InvalidSessionSecretError extends Error {}
11
+
12
+ function assertValidSessionId(sessionId: string): void {
13
+ if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("session_id is required");
14
+ }
15
+
16
+ /**
17
+ * Wraps daemon-kit's storage-agnostic session-identity primitive against Jittor's own
18
+ * SQLite-backed store, and enforces it at the one place a caller-supplied session_id is
19
+ * behavior-affecting: mutating router.* operations (see ports/router-controller.ts).
20
+ *
21
+ * Opt-in armor, not a breaking migration: a session_id that was never registered mutates
22
+ * exactly as before (undefined sessionId included, since that maps to the router's legacy
23
+ * "global" scope). Only once a sessionId is registered does a matching session_secret become
24
+ * mandatory. Every real Pi session becomes armored automatically once its extension fires
25
+ * session_start and registers.
26
+ */
27
+ export class SessionIdentity {
28
+ constructor(private readonly store: SessionIdentityStore) {}
29
+
30
+ register(sessionId: string): RegisterSessionIdentityResult {
31
+ assertValidSessionId(sessionId);
32
+ return registerSessionIdentity(this.store, sessionId);
33
+ }
34
+
35
+ release(sessionId: string, secret: string | undefined): { released: boolean } {
36
+ assertValidSessionId(sessionId);
37
+ const wasRegistered = isSessionRegistered(this.store, sessionId);
38
+ releaseSessionIdentity(this.store, sessionId, secret);
39
+ return { released: wasRegistered && !isSessionRegistered(this.store, sessionId) };
40
+ }
41
+
42
+ isRegistered(sessionId: string): boolean {
43
+ return isSessionRegistered(this.store, sessionId);
44
+ }
45
+
46
+ verify(sessionId: string, secret: string | undefined): boolean {
47
+ return verifySessionSecret(this.store, sessionId, secret);
48
+ }
49
+
50
+ assertAuthorized(sessionId: string | undefined, secret: string | undefined): void {
51
+ if (sessionId === undefined) return;
52
+ if (!this.isRegistered(sessionId)) return;
53
+ if (!this.verify(sessionId, secret)) throw new InvalidSessionSecretError(`session "${sessionId}" is registered; a valid session_secret is required to mutate its router state`);
54
+ }
55
+ }