@danypops/jittor 0.6.0 → 0.7.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/src/service.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES, CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS, CONTEXT_ASSESSMENT_QUERY_LIMIT, SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
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
3
  import { VERSION } from "./version.ts";
3
4
  import { validateMetricObservation, type MetricObservation, type MetricQuery, type StoredMetricObservation } from "./domain/metric.ts";
4
5
  import { assessContextTelemetry, estimateCompactionDuration, type CompactionDurationEstimate, type ContextAssessment } from "./domain/context-telemetry.ts";
6
+ import { buildTaskCostSummary, type TaskCostSummary } from "./domain/task-cost.ts";
7
+ import type { UsageAggregateRow } from "./domain/usage.ts";
5
8
  import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
6
9
  import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
7
10
  import type { ModelRankingResult } from "./domain/model-ranking.ts";
@@ -13,6 +16,9 @@ import type { PolicyDecision, Route } from "./policy.ts";
13
16
  export const EXPECTED_OPERATION_NAMES = [
14
17
  "metrics.record",
15
18
  "metrics.query",
19
+ "metrics.distinct_scopes",
20
+ "metrics.usage_series",
21
+ "metrics.cost_by_task",
16
22
  "metrics.prune",
17
23
  "benchmark.refresh",
18
24
  "benchmark.status",
@@ -36,7 +42,10 @@ export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
36
42
  export interface OperationInputs {
37
43
  "metrics.record": MetricObservation;
38
44
  "metrics.query": MetricQuery;
39
- "metrics.prune": { before: number };
45
+ "metrics.distinct_scopes": { source: string; since: number; until: number; limit?: number };
46
+ "metrics.usage_series": { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number };
47
+ "metrics.cost_by_task": { since: number; until: number };
48
+ "metrics.prune": { before: number; force?: boolean };
40
49
  "benchmark.refresh": { force?: boolean };
41
50
  "benchmark.status": Record<string, never>;
42
51
  "benchmark.query": BenchmarkQuery;
@@ -57,6 +66,9 @@ export interface OperationInputs {
57
66
  export interface OperationOutputs {
58
67
  "metrics.record": StoredMetricObservation;
59
68
  "metrics.query": StoredMetricObservation[];
69
+ "metrics.distinct_scopes": string[];
70
+ "metrics.usage_series": { rows: UsageAggregateRow[]; truncated: boolean };
71
+ "metrics.cost_by_task": TaskCostSummary;
60
72
  "metrics.prune": { deleted: number };
61
73
  "benchmark.refresh": BenchmarkRefreshResult;
62
74
  "benchmark.status": BenchmarkRefreshResult;
@@ -119,9 +131,61 @@ export class JittorService {
119
131
  switch (operation) {
120
132
  case "metrics.record": return this.metrics.record(validateMetricObservation(input));
121
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
+ }
122
181
  case "metrics.prune": {
123
182
  const before = input["before"];
124
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
+ }
125
189
  return { deleted: this.metrics.pruneBefore(before) };
126
190
  }
127
191
  case "benchmark.refresh": return this.benchmarks.refresh(input["force"] === true);
@@ -181,19 +245,16 @@ export interface JittorAppOptions {
181
245
  maxBodyBytes?: number;
182
246
  }
183
247
 
184
- function authorized(request: Request, token: string): boolean {
185
- return request.headers.get("authorization") === `Bearer ${token}`;
186
- }
187
-
248
+ /**
249
+ * Bearer-check and the trivial health/ready/not-found responses now delegate to
250
+ * `@danypops/daemon-kit/http` (the same handful of lines every daemon's service.ts hand-rolled).
251
+ * The response-size guard below stays jittor-specific: daemon-kit's `jsonResponse` is intentionally
252
+ * unbounded (it has no operation dispatch of its own to guard), while jittor's `/api/v1/ops` can
253
+ * return arbitrarily large query results that must be capped (see SERVICE_MAX_RESPONSE_BYTES).
254
+ */
188
255
  function json(value: unknown, status = 200): Response {
189
256
  const body = JSON.stringify(value);
190
- if (new TextEncoder().encode(body).byteLength > SERVICE_MAX_RESPONSE_BYTES) {
191
- const error = JSON.stringify({ error: "response too large" });
192
- return new Response(error, {
193
- status: 413,
194
- headers: { "content-type": "application/json", "content-length": String(new TextEncoder().encode(error).byteLength) },
195
- });
196
- }
257
+ if (new TextEncoder().encode(body).byteLength > SERVICE_MAX_RESPONSE_BYTES) return errorResponse("response too large", 413);
197
258
  return new Response(body, {
198
259
  status,
199
260
  headers: { "content-type": "application/json", "content-length": String(new TextEncoder().encode(body).byteLength) },
@@ -204,19 +265,16 @@ export function createApp(options: JittorAppOptions): { fetch(request: Request):
204
265
  const maxBodyBytes = options.maxBodyBytes ?? SERVICE_MAX_BODY_BYTES;
205
266
  return {
206
267
  async fetch(request: Request): Promise<Response> {
207
- if (!authorized(request, options.token)) return json({ error: "unauthorized" }, 401);
268
+ if (!requireBearerToken(request, options.token)) return errorResponse("unauthorized", 401);
208
269
  const url = new URL(request.url);
209
- if (request.method === "GET" && url.pathname === "/health") return json({ ok: true, version: VERSION });
210
- if (request.method === "GET" && url.pathname === "/ready") {
211
- const ready = options.service.ready();
212
- return json({ ready }, ready ? 200 : 503);
213
- }
270
+ if (request.method === "GET" && url.pathname === "/health") return healthResponse(VERSION);
271
+ if (request.method === "GET" && url.pathname === "/ready") return readyResponse(options.service.ready());
214
272
  if (request.method === "GET" && url.pathname === "/api/v1/ops") return json({ operations: options.service.operationNames() });
215
- if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return json({ error: "not found" }, 404);
273
+ if (request.method !== "POST" || url.pathname !== "/api/v1/ops") return errorResponse("not found", 404);
216
274
  const contentLength = Number(request.headers.get("content-length") ?? 0);
217
- if (contentLength > maxBodyBytes) return json({ error: "payload too large" }, 413);
275
+ if (contentLength > maxBodyBytes) return errorResponse("payload too large", 413);
218
276
  const text = await request.text();
219
- if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return json({ error: "payload too large" }, 413);
277
+ if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return errorResponse("payload too large", 413);
220
278
  try {
221
279
  const body = JSON.parse(text) as { op?: unknown; input?: unknown };
222
280
  if (typeof body.op !== "string") throw new Error("op is required");
package/src/state.ts CHANGED
@@ -1,81 +1,55 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { randomBytes } from "node:crypto";
1
+ /**
2
+ * Jittor's XDG paths/token/handle layout, now delegating to `@danypops/daemon-kit/paths` --
3
+ * the shared substrate factored out after jittor's own state.ts and web-spider-daemon's were
4
+ * found byte-identical (see daemon-kit's README). Kept as a thin jittor-named wrapper (same
5
+ * exported function names/signatures as before) so every existing call site (daemon.ts,
6
+ * client.ts, cli.ts, and their tests) is untouched by this migration.
7
+ */
8
+ import {
9
+ ensureAuthToken as ensureDaemonKitAuthToken,
10
+ readDaemonHandle as readDaemonKitHandle,
11
+ removeDaemonHandle as removeDaemonKitHandle,
12
+ resolveDaemonPaths,
13
+ writeDaemonHandle as writeDaemonKitHandle,
14
+ type DaemonHandle,
15
+ type DaemonPaths,
16
+ type PathEnvironment,
17
+ } from "@danypops/daemon-kit/paths";
5
18
  import {
6
19
  DATABASE_FILENAME,
7
20
  HANDLE_FILENAME,
8
21
  JITTOR_STATE_DIRECTORY,
9
- LOOPBACK_HOST,
10
22
  SYSTEMD_UNIT_NAME,
11
23
  TOKEN_FILENAME,
12
24
  } from "./constants.ts";
13
25
 
14
- export interface JittorPaths {
15
- database: string;
16
- token: string;
17
- handle: string;
18
- systemdUnit: string;
19
- }
26
+ export type JittorPaths = DaemonPaths;
27
+ export type { DaemonHandle, PathEnvironment };
20
28
 
21
- export interface DaemonHandle {
22
- host: typeof LOOPBACK_HOST;
23
- port: number;
24
- pid: number;
25
- }
26
-
27
- export interface PathEnvironment {
28
- env?: Record<string, string | undefined>;
29
- home?: string;
30
- uid?: number;
31
- }
29
+ const JITTOR_PATH_NAMES = {
30
+ stateDirectoryName: JITTOR_STATE_DIRECTORY,
31
+ databaseFilename: DATABASE_FILENAME,
32
+ tokenFilename: TOKEN_FILENAME,
33
+ handleFilename: HANDLE_FILENAME,
34
+ systemdUnitName: SYSTEMD_UNIT_NAME,
35
+ };
32
36
 
33
37
  export function resolveJittorPaths(options: PathEnvironment = {}): JittorPaths {
34
- const env = options.env ?? process.env;
35
- const home = options.home ?? homedir();
36
- const uid = options.uid ?? process.getuid?.() ?? 0;
37
- const dataHome = env["XDG_DATA_HOME"] ?? join(home, ".local", "share");
38
- const stateHome = env["XDG_STATE_HOME"] ?? join(home, ".local", "state");
39
- const runtimeHome = env["XDG_RUNTIME_DIR"] ?? join("/run", "user", String(uid));
40
- const configHome = env["XDG_CONFIG_HOME"] ?? join(home, ".config");
41
- return {
42
- database: join(dataHome, JITTOR_STATE_DIRECTORY, DATABASE_FILENAME),
43
- token: join(stateHome, JITTOR_STATE_DIRECTORY, TOKEN_FILENAME),
44
- handle: join(runtimeHome, JITTOR_STATE_DIRECTORY, HANDLE_FILENAME),
45
- systemdUnit: join(configHome, "systemd", "user", SYSTEMD_UNIT_NAME),
46
- };
38
+ return resolveDaemonPaths(JITTOR_PATH_NAMES, options);
47
39
  }
48
40
 
49
41
  export function ensureAuthToken(paths: JittorPaths = resolveJittorPaths()): string {
50
- mkdirSync(dirname(paths.token), { recursive: true, mode: 0o700 });
51
- if (existsSync(paths.token)) {
52
- chmodSync(paths.token, 0o600);
53
- const token = readFileSync(paths.token, "utf8").trim();
54
- if (!/^[a-f0-9]{64}$/.test(token)) throw new Error("invalid Jittor authentication token");
55
- return token;
56
- }
57
- const token = randomBytes(32).toString("hex");
58
- writeFileSync(paths.token, `${token}\n`, { mode: 0o600 });
59
- return token;
42
+ return ensureDaemonKitAuthToken(paths.token, "Jittor");
60
43
  }
61
44
 
62
45
  export function writeDaemonHandle(paths: JittorPaths, handle: DaemonHandle): void {
63
- mkdirSync(dirname(paths.handle), { recursive: true, mode: 0o700 });
64
- const temporary = `${paths.handle}.${process.pid}.tmp`;
65
- writeFileSync(temporary, `${JSON.stringify(handle)}\n`, { mode: 0o600 });
66
- renameSync(temporary, paths.handle);
46
+ writeDaemonKitHandle(paths.handle, handle);
67
47
  }
68
48
 
69
49
  export function readDaemonHandle(paths: JittorPaths = resolveJittorPaths()): DaemonHandle | null {
70
- try {
71
- const value = JSON.parse(readFileSync(paths.handle, "utf8")) as Partial<DaemonHandle>;
72
- if (value.host !== LOOPBACK_HOST || !Number.isInteger(value.port) || value.port! < 1 || value.port! > 65_535 || !Number.isInteger(value.pid)) return null;
73
- return value as DaemonHandle;
74
- } catch {
75
- return null;
76
- }
50
+ return readDaemonKitHandle(paths.handle);
77
51
  }
78
52
 
79
53
  export function removeDaemonHandle(paths: JittorPaths = resolveJittorPaths()): void {
80
- rmSync(paths.handle, { force: true });
54
+ removeDaemonKitHandle(paths.handle);
81
55
  }
package/src/version.ts CHANGED
@@ -1,16 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- function packageVersion(): string {
4
- const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as unknown;
5
- if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
6
- throw new Error("Jittor package manifest must be an object");
7
- }
8
- const version = (manifest as Record<string, unknown>)["version"];
9
- if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
10
- throw new Error("Jittor package manifest has an invalid version");
11
- }
12
- return version;
13
- }
1
+ import { readPackageVersion } from "@danypops/daemon-kit/version";
14
2
 
15
3
  /** Runtime package version; package.json is the single release source of truth. */
16
- export const VERSION = packageVersion();
4
+ export const VERSION = readPackageVersion(new URL("../package.json", import.meta.url), "Jittor");