@danypops/pi-lector 0.1.10 → 0.1.12

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.
@@ -140,7 +140,7 @@ export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | und
140
140
  const lines = [theme.fg("muted", `${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}:`)];
141
141
 
142
142
  for (const diagnostic of diagnostics.slice(0, displayCount)) {
143
- const severity = theme.fg(DIAGNOSTIC_SEVERITY_COLOR[diagnostic.severity] ?? "muted", theme.bold(diagnostic.severity));
143
+ const severity = theme.fg(DIAGNOSTIC_SEVERITY_COLOR[diagnostic.severity], theme.bold(diagnostic.severity));
144
144
  const location = formatLocation(theme, diagnostic.range.path, diagnostic.range.start.line, diagnostic.range.start.character);
145
145
  const origin = diagnostic.source ? theme.fg("dim", ` (${diagnostic.source}${diagnostic.code !== undefined ? ` ${diagnostic.code}` : ""})`) : "";
146
146
  lines.push(` ${severity} ${location} -- ${diagnostic.message}${origin}`);
@@ -209,7 +209,11 @@ export function describePopulateSymbolGraphJob(job: JobSnapshot<PopulateSymbolGr
209
209
  if (job.status === "running") return `Source workspace is registered; symbol graph is still loading (job ${job.id}). Poll job_status.`;
210
210
  if (job.status === "failed") return `Job ${job.id} failed [${job.error.code}] -- ${job.error.message}`;
211
211
  const result = job.result;
212
- return `Job ${job.id} cached ${result.filesProcessed} file${result.filesProcessed === 1 ? "" : "s"}, ${result.symbolsProcessed} symbol${result.symbolsProcessed === 1 ? "" : "s"}, ${result.nodesAdded} node${result.nodesAdded === 1 ? "" : "s"}, ${result.edgesAdded} edge${result.edgesAdded === 1 ? "" : "s"}`;
212
+ const counts = `${result.filesProcessed}/${result.filesAttempted} files, ${result.symbolsProcessed} symbol${result.symbolsProcessed === 1 ? "" : "s"}, ${result.nodesAdded} node${result.nodesAdded === 1 ? "" : "s"}, ${result.edgesAdded} edge${result.edgesAdded === 1 ? "" : "s"}`;
213
+ if (result.completeness === "complete") return `Job ${job.id} cached ${counts}`;
214
+ const first = result.failures[0];
215
+ const failure = first ? ` First failure: ${first.path} [${first.code} via ${first.provenance.backend}] ${first.message}` : "";
216
+ return `Job ${job.id} partially cached ${counts}; ${result.filesFailed} failed file${result.filesFailed === 1 ? "" : "s"} (${result.failureCount} failed operations).${failure}`;
213
217
  }
214
218
 
215
219
  export function formatPopulateSymbolGraphResult(job: JobSnapshot<PopulateSymbolGraphResult> | undefined, theme: LectorTheme): string {
@@ -1,4 +1,5 @@
1
1
  import { dirname, parse } from "node:path";
2
+ import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
2
3
  import {
3
4
  connectLectorClient,
4
5
  type LectorClient,
@@ -20,60 +21,33 @@ import { nearestGitRoot } from "./nearest-workspace-root.ts";
20
21
  *
21
22
  * The daemon binds a new random port on every restart. A client resolved
22
23
  * once and cached for the rest of the session would otherwise point at a
23
- * dead port after any later restart -- lectorClient()'s returned .call()
24
+ * dead port after any later restart -- daemon-kit's createRetryingClient
24
25
  * detects that on the failing call itself (not just the first connection
25
- * attempt) and retries once against a freshly re-resolved client, matching
26
- * the pattern already proven in this house's papyrusClient()/callService().
26
+ * attempt) and retries once against a freshly re-resolved client, the same
27
+ * policy this file used to hand-roll and now shares with web-spider's
28
+ * callWebSpider(), papyrus's callService(), and pi-packed's createNatives().
27
29
  */
28
30
 
29
31
  type ClientConnector = () => Promise<LectorClient>;
30
32
 
31
33
  let connector: ClientConnector = () => connectLectorClient();
32
- let cachedClient: Promise<LectorClient> | undefined;
34
+ // Wraps `() => connector()` rather than `connector` itself, so a test's
35
+ // setLectorClientConnectorForTests still takes effect after this retrying
36
+ // client is constructed once at module load.
37
+ const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() => connector(), { label: "Lector" });
33
38
  const workspaceIdByRoot = new Map<string, WorkspaceId>();
34
39
 
35
- async function resolveClient(): Promise<LectorClient> {
36
- if (!cachedClient) {
37
- cachedClient = connector().catch((error: unknown) => {
38
- cachedClient = undefined;
39
- throw error;
40
- });
41
- }
42
- return cachedClient;
43
- }
44
-
45
- /**
46
- * True when `error` means the connection itself is bad (the daemon
47
- * restarted on a new port since this client was cached, or died outright)
48
- * -- worth invalidating the cache and retrying once. False for a genuine
49
- * domain-level rejection (e.g. UnknownWorkspace), which a retry cannot fix
50
- * and would only mask.
51
- */
52
- function isStaleConnectionError(error: unknown): boolean {
53
- if (error instanceof TypeError) return true; // fetch()'s own connection-refused/DNS-failure shape
54
- if (!(error instanceof Error)) return false;
55
- if (error.name === "AbortError" || error.name === "TimeoutError") return true;
56
- return /fetch failed|unable to connect|network|socket|ECONNRESET|ECONNREFUSED|connection refused/i.test(error.message);
57
- }
58
-
59
40
  export interface RetryingLectorClient {
60
41
  call<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]>;
61
42
  }
62
43
 
44
+ // Kept async even though its own body has no await: every call site across this package does
45
+ // `await lectorClient()`, and dropping async here (just to satisfy require-await) would turn an
46
+ // internal implementation detail into a signature change rippling through every one of them.
47
+ // eslint-disable-next-line @typescript-eslint/require-await
63
48
  export async function lectorClient(): Promise<RetryingLectorClient> {
64
49
  return {
65
- async call(operation, input) {
66
- for (let attempt = 0; attempt < 2; attempt++) {
67
- const client = await resolveClient();
68
- try {
69
- return await client.call(operation, input);
70
- } catch (error) {
71
- cachedClient = undefined;
72
- if (attempt === 1 || !isStaleConnectionError(error)) throw error;
73
- }
74
- }
75
- throw new Error("Lector daemon client retry exhausted");
76
- },
50
+ call: (operation, input) => retryingClient.call((client) => client.call(operation, input)),
77
51
  };
78
52
  }
79
53
 
@@ -163,13 +137,13 @@ export async function withWorkspace<T>(resolve: () => Promise<ResolvedWorkspace>
163
137
  }
164
138
 
165
139
  export function setLectorClientConnectorForTests(value: ClientConnector): void {
166
- cachedClient = undefined;
140
+ retryingClient.reset();
167
141
  workspaceIdByRoot.clear();
168
142
  connector = value;
169
143
  }
170
144
 
171
145
  export function resetLectorClientForTests(): void {
172
- cachedClient = undefined;
146
+ retryingClient.reset();
173
147
  workspaceIdByRoot.clear();
174
148
  connector = () => connectLectorClient();
175
149
  }
@@ -44,12 +44,14 @@ export type CachePresentationState =
44
44
  | { readonly status: "not-cached"; readonly reason: string }
45
45
  | { readonly status: "caching"; readonly jobId: string }
46
46
  | { readonly status: "finished-caching"; readonly job: JobSnapshot<PopulateSymbolGraphResult> & { readonly status: "succeeded" } }
47
+ | { readonly status: "partial"; readonly result: PopulateSymbolGraphResult }
47
48
  | { readonly status: "cached" };
48
49
 
49
50
  export function describeCacheState(state: CachePresentationState): string {
50
51
  if (state.status === "not-cached") return `not cached (${state.reason})`;
51
52
  if (state.status === "caching") return `caching (job ${state.jobId})`;
52
53
  if (state.status === "finished-caching") return `finished caching (job ${state.job.id})`;
54
+ if (state.status === "partial") return `partially cached (${state.result.filesFailed} failed file${state.result.filesFailed === 1 ? "" : "s"})`;
53
55
  return "cached";
54
56
  }
55
57
 
@@ -57,6 +59,7 @@ export function cacheContextMessage(state: CachePresentationState): string {
57
59
  const prefix = `Lector workspace cache: ${describeCacheState(state)}.`;
58
60
  if (state.status === "not-cached") return `${prefix} Live code-intelligence operations remain available.`;
59
61
  if (state.status === "caching") return `${prefix} The cached graph is still building; use live code-intelligence operations until it is ready.`;
62
+ if (state.status === "partial") return `${prefix} The graph is usable, but live code-intelligence operations are required for failed files.`;
60
63
  return `${prefix} The cached graph is ready.`;
61
64
  }
62
65
 
@@ -80,6 +83,15 @@ export async function monitorWorkspaceCache(operations: WorkspaceCacheOperations
80
83
  options.onState({ status: "cached" });
81
84
  return;
82
85
  }
86
+ if (initial.status === "partial") {
87
+ options.onState({ status: "partial", result: initial.generation.result });
88
+ return;
89
+ }
90
+
91
+ function reportCompleted(job: JobSnapshot<PopulateSymbolGraphResult> & { readonly status: "succeeded" }): void {
92
+ options.onState({ status: "finished-caching", job });
93
+ options.onState(job.result.completeness === "partial" ? { status: "partial", result: job.result } : { status: "cached" });
94
+ }
83
95
 
84
96
  let jobId: string;
85
97
  if (initial.status === "caching") {
@@ -89,8 +101,7 @@ export async function monitorWorkspaceCache(operations: WorkspaceCacheOperations
89
101
  const submitted = await operations.submit(options.directory, options.maxFiles, options.maxSymbolsPerFile);
90
102
  if (submitted.status === "failed") throw new Error(`${submitted.error.code}: ${submitted.error.message}`);
91
103
  if (submitted.status === "succeeded") {
92
- options.onState({ status: "finished-caching", job: submitted });
93
- options.onState({ status: "cached" });
104
+ reportCompleted(submitted);
94
105
  return;
95
106
  }
96
107
  jobId = submitted.id;
@@ -103,8 +114,7 @@ export async function monitorWorkspaceCache(operations: WorkspaceCacheOperations
103
114
  const job = await operations.jobStatus(jobId);
104
115
  if (job.status === "failed") throw new Error(`${job.error.code}: ${job.error.message}`);
105
116
  if (job.status === "succeeded") {
106
- options.onState({ status: "finished-caching", job });
107
- options.onState({ status: "cached" });
117
+ reportCompleted(job);
108
118
  return;
109
119
  }
110
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,8 @@
18
18
  "typebox": "*"
19
19
  },
20
20
  "dependencies": {
21
- "@danypops/lector": "^0.1.9"
21
+ "@danypops/daemon-kit": "^0.4.0",
22
+ "@danypops/lector": "^0.1.10"
22
23
  },
23
24
  "devDependencies": {
24
25
  "@earendil-works/pi-ai": "^0.81.1",