@danypops/papyrus 0.33.0 → 0.33.2

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.
@@ -16,6 +16,7 @@ import {
16
16
  TASK_DRIVER_MAX_UNCHANGED_TURNS,
17
17
  PAPYRUS_CONTEXT_INJECTION_CHANNEL,
18
18
  CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
19
+ TASK_WIDGET_POLL_INTERVAL_MS,
19
20
  } from "../../src/constants.ts";
20
21
  import type { Artifact } from "../../src/domain/artifact.ts";
21
22
  import type { GateResult } from "../../src/domain/gate.ts";
@@ -102,6 +103,7 @@ export class TaskOverlay {
102
103
  private snapshot: TaskGraph = { nodes: [], rootIds: [] };
103
104
  private projectRoot: string | undefined;
104
105
  private sessionId: string | undefined;
106
+ private pollTimer: ReturnType<typeof setInterval> | undefined;
105
107
 
106
108
  setUI(ctx: ExtensionUIContext): void {
107
109
  if (ctx !== this.uiCtx) {
@@ -175,7 +177,24 @@ export class TaskOverlay {
175
177
  return renderTaskWidgetLines(theme, buildTaskWidgetProjection(this.snapshot), width);
176
178
  }
177
179
 
180
+ /**
181
+ * Fallback for a Task mutation no event announces -- the CLI run directly from a shell, or
182
+ * a second concurrent Pi session against the same daemon. Idempotent: a second call is a
183
+ * no-op rather than starting a competing timer.
184
+ */
185
+ startPolling(intervalMs: number = TASK_WIDGET_POLL_INTERVAL_MS): void {
186
+ if (this.pollTimer) return;
187
+ this.pollTimer = setInterval(() => { void this.refresh(); }, intervalMs);
188
+ }
189
+
190
+ stopPolling(): void {
191
+ if (!this.pollTimer) return;
192
+ clearInterval(this.pollTimer);
193
+ this.pollTimer = undefined;
194
+ }
195
+
178
196
  dispose(): void {
197
+ this.stopPolling();
179
198
  this.uiCtx?.setWidget(WIDGET_KEY, undefined);
180
199
  this.registered = false;
181
200
  this.tui = undefined;
@@ -549,6 +568,7 @@ export default async function (pi: ExtensionAPI) {
549
568
  overlay.setProjectRoot(ctx.cwd);
550
569
  overlay.setSessionId(ctx.sessionManager.getSessionId());
551
570
  await overlay.refresh();
571
+ overlay.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
552
572
  });
553
573
 
554
574
  pi.on("session_before_compact", () => { taskContinuation.onCompaction(); });
@@ -1,45 +1,29 @@
1
+ import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
1
2
  import { connectPapyrusClient, type PapyrusClient } from "../../src/client.ts";
2
3
  import type { OperationName } from "../../src/service.ts";
3
4
 
4
5
  type ClientConnector = () => Promise<PapyrusClient>;
5
6
 
6
7
  let connector: ClientConnector = () => connectPapyrusClient();
7
- let cached: PapyrusClient | undefined;
8
+ const client: RetryingClient<PapyrusClient> = createRetryingClient<PapyrusClient>(() => connector(), { label: "Papyrus" });
8
9
 
9
10
  export async function papyrusClient(): Promise<PapyrusClient> {
10
- if (cached) return cached;
11
- cached = await connector();
12
- return cached;
13
- }
14
-
15
- function staleConnection(error: unknown): boolean {
16
- if (error instanceof TypeError) return true;
17
- if (!(error instanceof Error)) return false;
18
- if (error.name === "AbortError" || error.name === "TimeoutError") return true;
19
- return /fetch failed|network|socket|ECONNRESET|ECONNREFUSED|connection refused/i.test(error.message);
11
+ return client.call(async (resolved) => resolved);
20
12
  }
21
13
 
22
14
  export async function callService<Input extends Record<string, unknown>, Output>(
23
15
  operation: OperationName,
24
16
  input: Input,
25
17
  ): Promise<Output> {
26
- for (let attempt = 0; attempt < 2; attempt += 1) {
27
- try {
28
- return await (await papyrusClient()).call<Input, Output>(operation, input);
29
- } catch (error) {
30
- cached = undefined;
31
- if (attempt === 1 || !staleConnection(error)) throw error;
32
- }
33
- }
34
- throw new Error("Papyrus daemon client retry exhausted");
18
+ return client.call((resolved) => resolved.call<Input, Output>(operation, input));
35
19
  }
36
20
 
37
21
  export function setPapyrusClientConnectorForTests(value: ClientConnector): void {
38
- cached = undefined;
39
22
  connector = value;
23
+ client.reset();
40
24
  }
41
25
 
42
26
  export function resetPapyrusClientForTests(): void {
43
- cached = undefined;
44
27
  connector = () => connectPapyrusClient();
28
+ client.reset();
45
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.33.0",
3
+ "version": "0.33.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -42,6 +42,6 @@
42
42
  "files": ["src", "extension", "README.md"],
43
43
  "dependencies": {
44
44
  "beautiful-mermaid": "1.1.3",
45
- "@danypops/daemon-kit": "^0.2.1"
45
+ "@danypops/daemon-kit": "^0.4.0"
46
46
  }
47
47
  }
package/src/constants.ts CHANGED
@@ -65,6 +65,12 @@ export const TASK_CONTEXT_CURRENT_LIMIT = 3;
65
65
  export const TASK_CONTEXT_REJECTED_LIMIT = 3;
66
66
  export const TASK_WIDGET_OPEN_LIMIT = 3;
67
67
  export const TASK_DETAIL_MIN_VISIBLE_LINES = 8;
68
+ /**
69
+ * Event-triggered refresh (tool_execution_end, session_compact/tree) can't see a Task
70
+ * mutation from outside this Pi session -- another concurrent session, or the CLI run
71
+ * directly from a shell. This bounded poll is the fallback for exactly that gap.
72
+ */
73
+ export const TASK_WIDGET_POLL_INTERVAL_MS = 20_000;
68
74
  export const TASK_DETAIL_MAX_VISIBLE_LINES = 24;
69
75
  export const TASK_DETAIL_RESERVED_ROWS = 8;
70
76
  export const TASK_DETAIL_HORIZONTAL_PAN_COLUMNS = 4;