@danypops/jittor 0.12.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/jittor",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Just-in-Time Token Optimizing Router for Pi -- supervised daemon, router policy, and CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -16,7 +16,8 @@
16
16
  "service:install": "bun src/cli.ts service install"
17
17
  },
18
18
  "dependencies": {
19
- "@danypops/daemon-kit": "^0.3.1",
19
+ "@danypops/vehicle-server": "^0.3.1",
20
+ "@danypops/vehicle-client": "^0.1.1",
20
21
  "google-auth-library": "^10.9.0"
21
22
  },
22
23
  "devDependencies": {
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { join } from "node:path";
5
+ import { createNodeServiceInstallDeps, generateSystemdUnit, installUserService, type ServiceSpec } from "@danypops/vehicle-server/service";
5
6
  import { SYSTEMD_UNIT_NAME } from "../constants.ts";
6
7
  import { resolveJittorPaths } from "../state.ts";
7
8
  import { callAndPrint, type CliDependencies } from "./support.ts";
@@ -16,23 +17,30 @@ export interface SystemdUnitOptions {
16
17
  openRouterBenchmarks?: boolean;
17
18
  }
18
19
 
19
- export function renderSystemdUnit(options: SystemdUnitOptions): string {
20
- return `[Unit]
21
- Description=Jittor token optimizing router
22
- After=default.target network-online.target
23
- Wants=network-online.target
24
-
25
- [Service]
26
- Type=simple
27
- ExecStart=${options.bunBin} ${options.cliPath} serve
28
- ${options.codexAuthFile ? `Environment="JITTOR_CODEX_AUTH_FILE=${options.codexAuthFile.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"\n` : ""}${options.openRouterBenchmarks ? "Environment=JITTOR_OPENROUTER_BENCHMARKS=1\n" : ""}Restart=always
29
- RestartSec=2
30
- NoNewPrivileges=true
31
- PrivateTmp=true
20
+ function jittorServiceSpec(options: SystemdUnitOptions): ServiceSpec {
21
+ const env: Record<string, string> = {};
22
+ if (options.codexAuthFile) env["JITTOR_CODEX_AUTH_FILE"] = options.codexAuthFile;
23
+ if (options.openRouterBenchmarks) env["JITTOR_OPENROUTER_BENCHMARKS"] = "1";
24
+ return {
25
+ name: "jittor",
26
+ displayName: "Jittor token optimizing router",
27
+ binPath: options.bunBin,
28
+ args: [options.cliPath, "serve"],
29
+ env,
30
+ descriptorPath: resolveJittorPaths().serviceDescriptor,
31
+ // Jittor's own client (connectJittorClient) never auto-spawns -- systemd's own
32
+ // supervision is this daemon's only recovery path, same as Lector's.
33
+ restartOnFailure: true,
34
+ restartSec: 2,
35
+ noNewPrivileges: true,
36
+ privateTmp: true,
37
+ waitForNetwork: true,
38
+ };
39
+ }
32
40
 
33
- [Install]
34
- WantedBy=default.target
35
- `;
41
+ /** Pure text generator, delegating to vehicle-server's shared generateSystemdUnit -- kept as its own named export since jittor's own tests (and any external caller) call it directly with the same options shape as before. */
42
+ export function renderSystemdUnit(options: SystemdUnitOptions): string {
43
+ return generateSystemdUnit(jittorServiceSpec(options));
36
44
  }
37
45
 
38
46
  export function systemctl(...args: string[]): void {
@@ -41,17 +49,18 @@ export function systemctl(...args: string[]): void {
41
49
 
42
50
  /** cliPath is the caller's own entrypoint file -- resolved from the real CLI script's `import.meta.url`, never this module's own, so the installed unit's ExecStart always points at the actual runnable CLI. */
43
51
  export function installService(cliPath: string): void {
44
- const unitPath = resolveJittorPaths().systemdUnit;
45
- mkdirSync(dirname(unitPath), { recursive: true });
46
52
  const codexAuthFile = join(process.env["CODEX_HOME"] ?? join(homedir(), ".codex"), "auth.json");
47
- writeFileSync(unitPath, renderSystemdUnit({
53
+ const spec = jittorServiceSpec({
48
54
  bunBin: process.execPath,
49
55
  cliPath,
50
56
  ...(existsSync(codexAuthFile) ? { codexAuthFile } : {}),
51
57
  openRouterBenchmarks: process.env["JITTOR_OPENROUTER_BENCHMARKS"] === "1",
52
- }));
53
- systemctl("daemon-reload");
54
- systemctl("enable", SYSTEMD_UNIT_NAME);
58
+ });
59
+ const result = installUserService(spec, createNodeServiceInstallDeps());
60
+ if (!result.installed) throw new Error(`failed to install the Jittor service: ${result.reason}`);
61
+ // installUserService's Linux path is `enable --now` (starts if not already running) --
62
+ // an explicit restart on top ensures a re-install after a Jittor upgrade actually picks
63
+ // up the freshly-generated unit's new ExecStart path, not just re-enables the old one.
55
64
  systemctl("restart", SYSTEMD_UNIT_NAME);
56
65
  }
57
66
 
@@ -8,7 +8,7 @@ export interface CliDependencies {
8
8
  stderr(line: string): void;
9
9
  systemctl(...args: string[]): void;
10
10
  installService(): void;
11
- serve(): void;
11
+ serve(): Promise<void>;
12
12
  }
13
13
 
14
14
  export function humanField(value: string): string {
package/src/cli.ts CHANGED
@@ -52,7 +52,7 @@ function usage(stderr: (line: string) => void): number {
52
52
  export async function runCli(args: string[], deps: CliDependencies = DEFAULT_DEPENDENCIES): Promise<number> {
53
53
  const [command, action, ...rest] = args;
54
54
  const fail = () => usage(deps.stderr);
55
- if (command === "serve") { deps.serve(); return 0; }
55
+ if (command === "serve") { await deps.serve(); return 0; }
56
56
  if (command === "session") return runSessionCommand(action, rest, deps, fail);
57
57
  if (command === "metrics") return runMetricsCommand(action, rest, deps, fail);
58
58
  if (command === "telemetry") return runTelemetryCommand(action, rest, deps, fail);
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AuthenticatedRpcClient, type FetchTransport } from "@danypops/daemon-kit/rpc-client";
1
+ import { AuthenticatedRpcClient, type FetchTransport } from "@danypops/vehicle-client/rpc-client";
2
2
  import type { OperationInputs, OperationName, OperationOutputs } from "./service.ts";
3
3
  import { ensureAuthToken, readDaemonHandle, resolveJittorPaths, type JittorPaths } from "./state.ts";
4
4
 
@@ -6,7 +6,7 @@ export type { FetchTransport };
6
6
 
7
7
  /**
8
8
  * Jittor's typed authenticated RPC client, now a thin named subclass of
9
- * `@danypops/daemon-kit/rpc-client`'s `AuthenticatedRpcClient` -- the shared substrate factored
9
+ * `@danypops/vehicle-client/rpc-client`'s `AuthenticatedRpcClient` -- the shared substrate factored
10
10
  * out after jittor's own client.ts and web-spider-daemon's were found byte-identical (see
11
11
  * daemon-kit's README). Keeps the old 3-positional-argument constructor so every existing call
12
12
  * site is untouched by this migration.
package/src/constants.ts CHANGED
@@ -115,3 +115,21 @@ export const GOOGLE_VERTEX_BUDGET_MAX_MESSAGES_PER_PULL = 20;
115
115
  */
116
116
  export const GOOGLE_VERTEX_BUDGET_CONFIDENCE = 0.6;
117
117
  export const GOOGLE_ADC_TOKEN_REFRESH_SKEW_MS = 60_000;
118
+ /**
119
+ * Context Hub: the shared, versioned, multi-producer channel any extension can target to
120
+ * contribute one segment of the context-window breakdown (successor to the single-producer
121
+ * papyrus.context-injection.v1 shape) -- Papyrus's rules/tasks segment is the first producer.
122
+ */
123
+ export const CONTEXT_HUB_CONTRIBUTION_CHANNEL = "jittor.context-contribution.v1";
124
+ export const CONTEXT_HUB_CONTRIBUTION_SCHEMA = "jittor.context-contribution/v1";
125
+ export const CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS = 80;
126
+ export const CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS = 160;
127
+ export const CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS = 80;
128
+ export const CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS = 200;
129
+ export const CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT = 500;
130
+ export const CONTEXT_HUB_MAX_ITEM_DEPTH = 6;
131
+ export const CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS = 5 * MILLISECONDS_PER_MINUTE;
132
+ export const CONTEXT_HUB_CONTRIBUTION_DEDUP_LIMIT = 1_000;
133
+ /** Matches Papyrus's own CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN; kept independent since Jittor does not depend on the Papyrus package. */
134
+ export const CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN = 4;
135
+ export const CONTEXT_HUB_CONFIDENCE_TIERS = ["exact-tool", "exact-cooperative", "correlated", "audited"] as const;
package/src/daemon.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { startDaemon as startDaemonKit, type RunningDaemon } from "@danypops/daemon-kit/daemon";
1
+ import { startDaemon as startDaemonKit, type RunningDaemon } from "@danypops/vehicle-server/daemon";
2
2
  import { MAINTENANCE_INTERVAL_MS, TELEMETRY_POLL_INTERVAL_MS } from "./constants.ts";
3
3
  import { DEFAULT_POLICY, UNCONFIGURED_ROUTE } from "./config.ts";
4
4
  import { SQLiteMetricStore } from "./adapters/sqlite-metric-store.ts";
@@ -23,7 +23,7 @@ import type { GoogleVertexMetricSource } from "./providers/google-vertex-contrac
23
23
  import { ensureAuthToken, resolveJittorPaths, type JittorPaths } from "./state.ts";
24
24
  import { logEvent, logger } from "./log.ts";
25
25
 
26
- export type { RunningDaemon } from "@danypops/daemon-kit/daemon";
26
+ export type { RunningDaemon } from "@danypops/vehicle-server/daemon";
27
27
 
28
28
  export function reportMaintenanceFailure(event: string, error: unknown): void {
29
29
  logEvent("error", event, { message: error instanceof Error ? error.message : String(error) });
@@ -65,7 +65,7 @@ export function telemetrySourcesFromEnvironment(env: Record<string, string | und
65
65
  }
66
66
 
67
67
  /**
68
- * Composition root, now built on `@danypops/daemon-kit/daemon`'s `startDaemon` for binding,
68
+ * Composition root, now built on `@danypops/vehicle-server/daemon`'s `startDaemon` for binding,
69
69
  * atomic handle write, maintenance-timer driving, and clean shutdown -- the skeleton that used to
70
70
  * be hand-rolled here (and, byte-identically, in web-spider-daemon's and papyrus's daemon.ts; see
71
71
  * daemon-kit's README). Each maintenance task still catches and classifies its own failure via
@@ -74,10 +74,10 @@ export function telemetrySourcesFromEnvironment(env: Record<string, string | und
74
74
  * daemon-kit's own generic "maintenance task failed: <name>" catch, which exists as a safety net
75
75
  * for tasks that don't self-classify, not to replace a consumer's own richer classification.
76
76
  */
77
- export function startDaemon(
77
+ export async function startDaemon(
78
78
  paths: JittorPaths = resolveJittorPaths(),
79
79
  env: Record<string, string | undefined> = process.env,
80
- ): RunningDaemon {
80
+ ): Promise<RunningDaemon> {
81
81
  const token = ensureAuthToken(paths);
82
82
  const db = openJittorDb(paths.database);
83
83
  const metrics = new SQLiteMetricStore(db);
@@ -96,7 +96,7 @@ export function startDaemon(
96
96
  });
97
97
  const service = new JittorService(metrics, router, benchmarks, modelRanker, sessionIdentity);
98
98
 
99
- const daemon = startDaemonKit({
99
+ const daemon = await startDaemonKit({
100
100
  daemonLabel: "Jittor",
101
101
  handlePath: paths.handle,
102
102
  logger,
@@ -115,8 +115,8 @@ export function startDaemon(
115
115
  return daemon;
116
116
  }
117
117
 
118
- export function serveMain(): void {
119
- const daemon = startDaemon();
118
+ export async function serveMain(): Promise<void> {
119
+ const daemon = await startDaemon();
120
120
  console.error(`[jittor] listening on ${daemon.host}:${daemon.port}`);
121
121
  const stop = async (): Promise<void> => {
122
122
  await daemon.stop();
package/src/db.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import { openSqliteWithPragmas } from "@danypops/daemon-kit/storage";
2
+ import { openSqliteWithPragmas } from "@danypops/vehicle-server/storage";
3
3
  import { SQLITE_BUSY_TIMEOUT_MS } from "./constants.ts";
4
4
 
5
5
  const INITIAL_SCHEMA = `
@@ -31,7 +31,7 @@ CREATE INDEX session_identities_last_seen_idx
31
31
  `;
32
32
 
33
33
  /**
34
- * Delegates bootstrap (pragmas, migration engine) to `@danypops/daemon-kit/storage`, which
34
+ * Delegates bootstrap (pragmas, migration engine) to `@danypops/vehicle-server/storage`, which
35
35
  * generalizes the byte-identical pragma/PRAGMA-user_version skeleton jittor's own db.ts used to
36
36
  * hand-roll (see daemon-kit's README). Jittor's only remaining responsibility is its own schema.
37
37
  */
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Context Hub: Jittor's cross-extension context-window attribution. Two independent pieces:
3
+ *
4
+ * - The tool-schema ledger (computeToolSchemaLedger/toolLedgerSegment): exact, zero-cooperation
5
+ * attribution of tool-schema cost (name + description + parameters + guidelines) per owning
6
+ * extension, using Pi's own `sourceInfo` on every registered tool -- no other package needs to
7
+ * change for this to work.
8
+ * - The shared contribution channel (validateContextContribution/contextContributionMetric): any
9
+ * extension can opt in and contribute one segment of the breakdown (e.g. Papyrus's rules/tasks
10
+ * segment) over `CONTEXT_HUB_CONTRIBUTION_CHANNEL`, the same shared-bus pattern already proven
11
+ * by papyrus.context-injection.v1, generalized to a human-readable producer name and a nested
12
+ * segment/item shape instead of one flat character count.
13
+ */
14
+ import {
15
+ CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
16
+ CONTEXT_HUB_CONFIDENCE_TIERS,
17
+ CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS,
18
+ CONTEXT_HUB_CONTRIBUTION_SCHEMA,
19
+ CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS,
20
+ CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT,
21
+ CONTEXT_HUB_MAX_ITEM_DEPTH,
22
+ CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS,
23
+ CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS,
24
+ CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS,
25
+ } from "../constants.ts";
26
+ import type { MetricObservation } from "./metric.ts";
27
+
28
+ export type ContextConfidenceTier = (typeof CONTEXT_HUB_CONFIDENCE_TIERS)[number];
29
+
30
+ export interface ContextSegmentItem {
31
+ label: string;
32
+ estimatedTokens: number;
33
+ children?: ContextSegmentItem[];
34
+ }
35
+
36
+ export interface ContextSegment {
37
+ key: string;
38
+ label: string;
39
+ estimatedTokens: number;
40
+ confidence: ContextConfidenceTier;
41
+ items?: ContextSegmentItem[];
42
+ }
43
+
44
+ export interface ContextContribution {
45
+ schema: typeof CONTEXT_HUB_CONTRIBUTION_SCHEMA;
46
+ observedAt: number;
47
+ sequence: number;
48
+ producerName: string;
49
+ segment: ContextSegment;
50
+ }
51
+
52
+ function nonEmptyString(value: unknown, name: string, maxLength: number): string {
53
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) throw new Error(`${name} must be a non-empty string of at most ${maxLength} characters`);
54
+ return value;
55
+ }
56
+
57
+ function boundedInteger(value: unknown, name: string): number {
58
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${name} must be a bounded non-negative integer`);
59
+ return value;
60
+ }
61
+
62
+ function countItems(items: ContextSegmentItem[]): number {
63
+ return items.reduce((sum, item) => sum + 1 + (item.children ? countItems(item.children) : 0), 0);
64
+ }
65
+
66
+ function validateSegmentItem(value: unknown, depth: number): ContextSegmentItem {
67
+ if (depth > CONTEXT_HUB_MAX_ITEM_DEPTH) throw new Error(`context segment item nesting exceeds ${CONTEXT_HUB_MAX_ITEM_DEPTH} levels`);
68
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment item must be an object");
69
+ const input = value as Record<string, unknown>;
70
+ for (const key of Object.keys(input)) {
71
+ if (key !== "label" && key !== "estimatedTokens" && key !== "children") throw new Error(`context segment item contains unexpected field: ${key}`);
72
+ }
73
+ const item: ContextSegmentItem = {
74
+ label: nonEmptyString(input["label"], "item.label", CONTEXT_HUB_ITEM_LABEL_MAX_CHARACTERS),
75
+ estimatedTokens: boundedInteger(input["estimatedTokens"], "item.estimatedTokens"),
76
+ };
77
+ if (input["children"] !== undefined) {
78
+ if (!Array.isArray(input["children"])) throw new Error("item.children must be an array");
79
+ item.children = input["children"].map((child) => validateSegmentItem(child, depth + 1));
80
+ }
81
+ return item;
82
+ }
83
+
84
+ /** Validates a bare segment (no contribution envelope) -- shared by validateContextContribution and Jittor's own directly-computed segments (tool ledger, base prompt, ...). */
85
+ export function validateContextSegment(value: unknown): ContextSegment {
86
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context segment must be an object");
87
+ const input = value as Record<string, unknown>;
88
+ for (const key of Object.keys(input)) {
89
+ if (key !== "key" && key !== "label" && key !== "estimatedTokens" && key !== "confidence" && key !== "items") {
90
+ throw new Error(`context segment contains unexpected field: ${key}`);
91
+ }
92
+ }
93
+ const confidence = input["confidence"];
94
+ if (typeof confidence !== "string" || !CONTEXT_HUB_CONFIDENCE_TIERS.includes(confidence as ContextConfidenceTier)) {
95
+ throw new Error(`confidence must be one of ${CONTEXT_HUB_CONFIDENCE_TIERS.join(", ")}`);
96
+ }
97
+ const segment: ContextSegment = {
98
+ key: nonEmptyString(input["key"], "segment.key", CONTEXT_HUB_SEGMENT_KEY_MAX_CHARACTERS),
99
+ label: nonEmptyString(input["label"], "segment.label", CONTEXT_HUB_SEGMENT_LABEL_MAX_CHARACTERS),
100
+ estimatedTokens: boundedInteger(input["estimatedTokens"], "segment.estimatedTokens"),
101
+ confidence: confidence as ContextConfidenceTier,
102
+ };
103
+ if (input["items"] !== undefined) {
104
+ if (!Array.isArray(input["items"])) throw new Error("segment.items must be an array");
105
+ const items = input["items"].map((item) => validateSegmentItem(item, 1));
106
+ if (countItems(items) > CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT) throw new Error(`segment.items exceeds ${CONTEXT_HUB_MAX_ITEMS_PER_SEGMENT} total items`);
107
+ segment.items = items;
108
+ }
109
+ return segment;
110
+ }
111
+
112
+ const CONTRIBUTION_FIELDS = new Set(["schema", "observedAt", "sequence", "producerName", "segment"]);
113
+
114
+ /** Validates one contribution posted on CONTEXT_HUB_CONTRIBUTION_CHANNEL. Fails closed on schema drift or an oversized/malformed payload, the same posture as validatePapyrusContextInjection. */
115
+ export function validateContextContribution(value: unknown, now = Date.now()): ContextContribution {
116
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("context contribution must be an object");
117
+ const input = value as Record<string, unknown>;
118
+ for (const key of Object.keys(input)) if (!CONTRIBUTION_FIELDS.has(key)) throw new Error(`context contribution contains unexpected field: ${key}`);
119
+ if (input["schema"] !== CONTEXT_HUB_CONTRIBUTION_SCHEMA) throw new Error("context contribution schema is not supported");
120
+ const observedAt = boundedInteger(input["observedAt"], "observedAt");
121
+ if (Math.abs(now - observedAt) > CONTEXT_HUB_CONTRIBUTION_MAX_AGE_MS) throw new Error("context contribution is stale");
122
+ return {
123
+ schema: CONTEXT_HUB_CONTRIBUTION_SCHEMA,
124
+ observedAt,
125
+ sequence: boundedInteger(input["sequence"], "sequence"),
126
+ producerName: nonEmptyString(input["producerName"], "producerName", CONTEXT_HUB_PRODUCER_NAME_MAX_CHARACTERS),
127
+ segment: validateContextSegment(input["segment"]),
128
+ };
129
+ }
130
+
131
+ /** Bounded, content-free metric projection for storage/history -- mirrors papyrusContextMetric's shape. */
132
+ export function contextContributionMetric(contribution: ContextContribution): MetricObservation {
133
+ return {
134
+ source: "context-hub",
135
+ scope: contribution.producerName,
136
+ metric: "segment-tokens",
137
+ value: contribution.segment.estimatedTokens,
138
+ unit: "count",
139
+ observedAt: contribution.observedAt,
140
+ attributes: {
141
+ sequence: contribution.sequence,
142
+ segmentKey: contribution.segment.key,
143
+ segmentLabel: contribution.segment.label,
144
+ confidence: contribution.segment.confidence,
145
+ itemCount: contribution.segment.items ? countItems(contribution.segment.items) : 0,
146
+ },
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Exact, zero-cooperation attribution of tool-schema cost: every registered tool's `sourceInfo`
152
+ * (populated by Pi's own resource loader from the extension that registered it, never guessed)
153
+ * identifies its owning extension. Tool schemas are fixed overhead paid on every single turn
154
+ * regardless of whether the tool is ever called, so this is the walking-skeleton signal --
155
+ * buildable without any other package changing.
156
+ */
157
+ export interface ToolLedgerEntry {
158
+ name: string;
159
+ description?: string;
160
+ parameters?: unknown;
161
+ promptGuidelines?: string[];
162
+ promptSnippet?: string;
163
+ sourceInfo?: { source?: string; path?: string };
164
+ }
165
+
166
+ export interface ToolLedgerToolUsage {
167
+ name: string;
168
+ characters: number;
169
+ estimatedTokens: number;
170
+ }
171
+
172
+ export interface ToolLedgerSourceUsage {
173
+ source: string;
174
+ toolCount: number;
175
+ characters: number;
176
+ estimatedTokens: number;
177
+ tools: ToolLedgerToolUsage[];
178
+ }
179
+
180
+ function toolCharacters(tool: ToolLedgerEntry): number {
181
+ const parameterCharacters = tool.parameters === undefined ? 0 : JSON.stringify(tool.parameters).length;
182
+ const guidelineCharacters = (tool.promptGuidelines ?? []).reduce((sum, guideline) => sum + guideline.length, 0);
183
+ return tool.name.length + (tool.description?.length ?? 0) + parameterCharacters + guidelineCharacters + (tool.promptSnippet?.length ?? 0);
184
+ }
185
+
186
+ /** Groups every registered tool's serialized schema size by its owning extension (`sourceInfo.source`), sorted heaviest-first at both the source and tool level. A tool with no sourceInfo (e.g. an SDK-supplied custom tool) is grouped under "unknown" rather than dropped. */
187
+ export function computeToolSchemaLedger(tools: readonly ToolLedgerEntry[]): ToolLedgerSourceUsage[] {
188
+ const bySource = new Map<string, ToolLedgerToolUsage[]>();
189
+ for (const tool of tools) {
190
+ const source = tool.sourceInfo?.source && tool.sourceInfo.source.length > 0 ? tool.sourceInfo.source : "unknown";
191
+ const characters = toolCharacters(tool);
192
+ const usage: ToolLedgerToolUsage = { name: tool.name, characters, estimatedTokens: Math.ceil(characters / CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN) };
193
+ const existing = bySource.get(source);
194
+ if (existing) existing.push(usage);
195
+ else bySource.set(source, [usage]);
196
+ }
197
+ return [...bySource.entries()]
198
+ .map(([source, toolUsages]) => {
199
+ const sorted = [...toolUsages].sort((left, right) => right.characters - left.characters);
200
+ return {
201
+ source,
202
+ toolCount: sorted.length,
203
+ characters: sorted.reduce((sum, tool) => sum + tool.characters, 0),
204
+ estimatedTokens: sorted.reduce((sum, tool) => sum + tool.estimatedTokens, 0),
205
+ tools: sorted,
206
+ };
207
+ })
208
+ .sort((left, right) => right.characters - left.characters);
209
+ }
210
+
211
+ /** Projects the tool-schema ledger into one ContextSegment (`toolDefinitions`), ready to merge alongside any contributed segment. */
212
+ export function toolLedgerSegment(tools: readonly ToolLedgerEntry[]): ContextSegment {
213
+ const ledger = computeToolSchemaLedger(tools);
214
+ const items: ContextSegmentItem[] = ledger.map((sourceUsage) => ({
215
+ label: `${sourceUsage.source} (${sourceUsage.toolCount} tool${sourceUsage.toolCount === 1 ? "" : "s"})`,
216
+ estimatedTokens: sourceUsage.estimatedTokens,
217
+ children: sourceUsage.tools.map((tool) => ({ label: tool.name, estimatedTokens: tool.estimatedTokens })),
218
+ }));
219
+ return {
220
+ key: "toolDefinitions",
221
+ label: "Tool definitions",
222
+ estimatedTokens: items.reduce((sum, item) => sum + item.estimatedTokens, 0),
223
+ confidence: "exact-tool",
224
+ items,
225
+ };
226
+ }
package/src/index.ts CHANGED
@@ -32,6 +32,20 @@ export {
32
32
  applyTaskFocusEvent,
33
33
  validateTaskFocusEvent,
34
34
  } from "./domain/task-focus.ts";
35
+ export {
36
+ type ContextConfidenceTier,
37
+ type ContextContribution,
38
+ type ContextSegment,
39
+ type ContextSegmentItem,
40
+ type ToolLedgerEntry,
41
+ type ToolLedgerSourceUsage,
42
+ type ToolLedgerToolUsage,
43
+ computeToolSchemaLedger,
44
+ contextContributionMetric,
45
+ toolLedgerSegment,
46
+ validateContextContribution,
47
+ validateContextSegment,
48
+ } from "./domain/context-hub.ts";
35
49
  export {
36
50
  METRIC_UNITS,
37
51
  type MetricObservation,
package/src/log.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Structured daemon logging, now backed by `@danypops/daemon-kit/logging` (pino) instead of a
2
+ * Structured daemon logging, now backed by `@danypops/vehicle-server/logging` (pino) instead of a
3
3
  * hand-rolled `console.error(JSON.stringify(...))` -- daemon-kit's own module doc explains why:
4
4
  * level ordering/filtering/child-scoping is exactly the kind of thing worth one shared,
5
5
  * dependency-backed implementation instead of four independent hand-rolled ones. One deliberate,
@@ -8,9 +8,9 @@
8
8
  * four daemons. `component`/`level`/`timestamp` and credential-safety (callers still must pass
9
9
  * only bounded, non-sensitive fields) are unchanged.
10
10
  */
11
- import { createLogger, type LogLevel as DaemonKitLogLevel, type Logger } from "@danypops/daemon-kit/logging";
11
+ import { createLogger, type LogLevel as VehicleLogLevel, type Logger } from "@danypops/vehicle-server/logging";
12
12
 
13
- export type LogLevel = Extract<DaemonKitLogLevel, "info" | "warn" | "error">;
13
+ export type LogLevel = Extract<VehicleLogLevel, "info" | "warn" | "error">;
14
14
 
15
15
  /**
16
16
  * Also passed directly as `StartDaemonOptions.logger` so daemon-kit's own maintenance-task
@@ -1,5 +1,5 @@
1
- import type { SessionIdentityRecord, SessionIdentityStore as DaemonKitSessionIdentityStore } from "@danypops/daemon-kit/session-identity";
1
+ import type { SessionIdentityRecord, SessionIdentityStore as VehicleSessionIdentityStore } from "@danypops/vehicle-server/session-identity";
2
2
 
3
3
  /** Jittor's persistence port for daemon-kit's storage-agnostic session-identity primitive. */
4
- export type SessionIdentityStore = DaemonKitSessionIdentityStore;
4
+ export type SessionIdentityStore = VehicleSessionIdentityStore;
5
5
  export type { SessionIdentityRecord };
package/src/service.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/daemon-kit/http";
1
+ import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/vehicle-server/rpc-http";
2
2
  import { SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
3
3
  import { InvalidSessionSecretError, SessionIdentity, type RegisterSessionIdentityResult } from "./session-identity-service.ts";
4
4
  import { VERSION } from "./version.ts";
@@ -188,7 +188,7 @@ export interface JittorAppOptions {
188
188
 
189
189
  /**
190
190
  * Bearer-check and the trivial health/ready/not-found responses now delegate to
191
- * `@danypops/daemon-kit/http` (the same handful of lines every daemon's service.ts hand-rolled).
191
+ * `@danypops/vehicle-server/rpc-http` (the same handful of lines every daemon's service.ts hand-rolled).
192
192
  * The response-size guard below stays jittor-specific: daemon-kit's `jsonResponse` is intentionally
193
193
  * unbounded (it has no operation dispatch of its own to guard), while jittor's `/api/v1/ops` can
194
194
  * return arbitrarily large query results that must be capped (see SERVICE_MAX_RESPONSE_BYTES).
@@ -1,4 +1,4 @@
1
- import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/daemon-kit/session-identity";
1
+ import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/vehicle-server/session-identity";
2
2
  import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
3
3
 
4
4
  export interface RegisterSessionIdentityResult {
package/src/state.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  /**
2
- * Jittor's XDG paths/token/handle layout, now delegating to `@danypops/daemon-kit/paths` --
2
+ * Jittor's XDG paths/token/handle layout, now delegating to `@danypops/vehicle-server/paths` --
3
3
  * the shared substrate factored out after jittor's own state.ts and web-spider-daemon's were
4
4
  * found byte-identical (see daemon-kit's README). Kept as a thin jittor-named wrapper (same
5
5
  * exported function names/signatures as before) so every existing call site (daemon.ts,
6
6
  * client.ts, cli.ts, and their tests) is untouched by this migration.
7
7
  */
8
8
  import {
9
- ensureAuthToken as ensureDaemonKitAuthToken,
10
- readDaemonHandle as readDaemonKitHandle,
11
- removeDaemonHandle as removeDaemonKitHandle,
9
+ ensureAuthToken as ensureVehicleAuthToken,
10
+ readDaemonHandle as readVehicleHandle,
11
+ removeDaemonHandle as removeVehicleHandle,
12
12
  resolveDaemonPaths,
13
- writeDaemonHandle as writeDaemonKitHandle,
13
+ writeDaemonHandle as writeVehicleHandle,
14
14
  type DaemonHandle,
15
15
  type DaemonPaths,
16
16
  type PathEnvironment,
17
- } from "@danypops/daemon-kit/paths";
17
+ } from "@danypops/vehicle-server/paths";
18
18
  import {
19
19
  DATABASE_FILENAME,
20
20
  HANDLE_FILENAME,
@@ -39,17 +39,17 @@ export function resolveJittorPaths(options: PathEnvironment = {}): JittorPaths {
39
39
  }
40
40
 
41
41
  export function ensureAuthToken(paths: JittorPaths = resolveJittorPaths()): string {
42
- return ensureDaemonKitAuthToken(paths.token, "Jittor");
42
+ return ensureVehicleAuthToken(paths.token, "Jittor");
43
43
  }
44
44
 
45
45
  export function writeDaemonHandle(paths: JittorPaths, handle: DaemonHandle): void {
46
- writeDaemonKitHandle(paths.handle, handle);
46
+ writeVehicleHandle(paths.handle, handle);
47
47
  }
48
48
 
49
49
  export function readDaemonHandle(paths: JittorPaths = resolveJittorPaths()): DaemonHandle | null {
50
- return readDaemonKitHandle(paths.handle);
50
+ return readVehicleHandle(paths.handle);
51
51
  }
52
52
 
53
53
  export function removeDaemonHandle(paths: JittorPaths = resolveJittorPaths()): void {
54
- removeDaemonKitHandle(paths.handle);
54
+ removeVehicleHandle(paths.handle);
55
55
  }
package/src/version.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { readPackageVersion } from "@danypops/daemon-kit/version";
1
+ import { readPackageVersion } from "@danypops/vehicle-server/version";
2
2
 
3
3
  /** Runtime package version; package.json is the single release source of truth. */
4
4
  export const VERSION = readPackageVersion(new URL("../package.json", import.meta.url), "Jittor");