@danypops/papyrus 0.53.0 → 0.53.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.53.0",
3
+ "version": "0.53.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
  "main": "./src/index.ts",
package/src/constants.ts CHANGED
@@ -28,7 +28,16 @@ export const GATE_TEST_TIMEOUT_MS = 60_000;
28
28
  * this ceiling has had a chance to.
29
29
  */
30
30
  export const GATE_TIMEOUT_MAX_MS = 300_000;
31
- export const GATE_OUTPUT_LIMIT = 200;
31
+ /**
32
+ * How much of a command/test gate's own combined stdout+stderr is kept in the caller-facing
33
+ * `output` field -- taken from the END of the buffer (see ops.ts's callers), not the start.
34
+ * Real, confirmed bug this exists for (papyrus task d0eb81b7): at the previous value (200,
35
+ * head-sliced), a failing gate's `output` carried only the first line or two of banner/setup
36
+ * noise -- exactly the LEAST useful part of a real test run, whose actual pass/fail summary is
37
+ * always its last lines. GATE_OPERATION_LIMITS' own maxResponseBytes (262_144, handlers/tasks.ts)
38
+ * comfortably holds many gates at this size.
39
+ */
40
+ export const GATE_OUTPUT_LIMIT = 8_000;
32
41
  export const GATE_MAX_BUFFER_BYTES = 1_048_576;
33
42
  export const GATE_FILE_MAX_BYTES = 1_048_576;
34
43
 
@@ -42,6 +42,10 @@ const TASK_READ_ONLY_OPERATIONS = new Set([
42
42
  "tasks.show",
43
43
  ]);
44
44
 
45
+ /** Matches @danypops/vehicle-server's own STREAMING_IDLE_TIMEOUT_S (daemon.ts) -- see this file's
46
+ * own fetch handler for why Papyrus needs the identical fix applied directly, not inherited. */
47
+ const VEHICLE_INVOKE_IDLE_TIMEOUT_S = 3_600;
48
+
45
49
  /** Start the supervised, long-running Papyrus service. */
46
50
  export async function serveMain(): Promise<void> {
47
51
  const stateDir = daemonStateDir();
@@ -81,7 +85,19 @@ export async function serveMain(): Promise<void> {
81
85
  hostname: DAEMON_HOST,
82
86
  port: 0,
83
87
  fetch: (request, bunServer) => {
84
- if (new URL(request.url).pathname === "/push") return pushChannel.upgrade(request, bunServer) ?? undefined;
88
+ const pathname = new URL(request.url).pathname;
89
+ if (pathname === "/push") return pushChannel.upgrade(request, bunServer) ?? undefined;
90
+ // Bun.serve's own idleTimeout defaults to 10s and applies per-connection regardless of
91
+ // how long a given request is expected to take -- @danypops/vehicle-server's own daemon.ts
92
+ // (startBunListener) already fixed this for every Vehicle-backed daemon that goes through
93
+ // its shared startDaemon() substrate; Papyrus's own daemon.ts predates that substrate and
94
+ // has this separate, hand-rolled Bun.serve() call, so it needs the identical fix applied
95
+ // directly here. Real live incident (papyrus task d0eb81b7): tasks.run_gates/tasks.complete
96
+ // can legitimately take tens of seconds to actually run a caller's own gate command,
97
+ // sending zero response bytes the whole time -- just as exposed to Bun's 10s default as
98
+ // the streaming case, and neither gate.timeoutMs nor VehicleLimits.maxTimeoutMs ever gets a
99
+ // chance to apply if the raw TCP connection is already dead first.
100
+ if (pathname === "/vehicle/invoke") bunServer.timeout(request, VEHICLE_INVOKE_IDLE_TIMEOUT_S);
85
101
  return app.fetch(request);
86
102
  },
87
103
  // A no-op fallback when pushChannel never calls server.upgrade() is safe: Bun only
package/src/ops.ts CHANGED
@@ -628,17 +628,26 @@ function processGateCommand(gate: Gate): { command: string; timeout: number } {
628
628
  * previously "test" was a second, separately-maintained execSync path that never checked
629
629
  * gate.expect at all.
630
630
  */
631
+ /**
632
+ * Keeps the LAST GATE_OUTPUT_LIMIT characters, not the first -- a real command's own meaningful
633
+ * pass/fail summary is its last lines, not its first (setup/banner noise). See GATE_OUTPUT_LIMIT's
634
+ * own doc comment (constants.ts) for the real incident this fixes.
635
+ */
636
+ function gateOutputTail(text: string): string {
637
+ return text.length > GATE_OUTPUT_LIMIT ? text.slice(-GATE_OUTPUT_LIMIT) : text;
638
+ }
639
+
631
640
  function runProcessGateSync(gate: Gate, cwd?: string): GateResult {
632
641
  const { spawnSync } = require_("node:child_process");
633
642
  const { command, timeout } = processGateCommand(gate);
634
643
  const result = spawnSync(command, { shell: true, encoding: "utf-8", timeout, ...(cwd ? { cwd } : {}) });
635
- if (result.error) return { gate, passed: false, output: result.error.message.slice(0, GATE_OUTPUT_LIMIT) };
644
+ if (result.error) return { gate, passed: false, output: gateOutputTail(result.error.message) };
636
645
  const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
637
646
  const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
638
647
  return {
639
648
  gate,
640
649
  passed,
641
- output: combined.slice(0, GATE_OUTPUT_LIMIT) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`),
650
+ output: gateOutputTail(combined) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`),
642
651
  };
643
652
  }
644
653
 
@@ -695,13 +704,13 @@ function executeGateCommand(
695
704
  resolve(result);
696
705
  };
697
706
 
698
- child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT), matchable: error.message }));
707
+ child.on("error", (error) => finish({ passed: false, output: gateOutputTail(error.message), matchable: error.message }));
699
708
  child.on("close", (code) => {
700
709
  // `matchable` carries the full (GATE_MAX_BUFFER_BYTES-bounded) buffer so the caller's
701
- // gate.expect substring check sees the whole run, not just the first GATE_OUTPUT_LIMIT
702
- // characters -- a real bun test run's pass/fail summary is its last line, not its first.
710
+ // gate.expect substring check sees the whole run, regardless of GATE_OUTPUT_LIMIT --
711
+ // `output` (below) is only ever the display copy.
703
712
  const full = buffered.trim();
704
- const output = full.slice(0, GATE_OUTPUT_LIMIT);
713
+ const output = gateOutputTail(full);
705
714
  finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`), matchable: full });
706
715
  });
707
716