@danypops/papyrus 0.44.11 → 0.44.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.44.11",
3
+ "version": "0.44.12",
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"],
package/src/constants.ts CHANGED
@@ -16,6 +16,17 @@ export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
16
16
  export const DB_OPTIMIZE_INTERVAL_MS = 24 * 60 * 60_000;
17
17
  export const GATE_COMMAND_TIMEOUT_MS = 30_000;
18
18
  export const GATE_TEST_TIMEOUT_MS = 60_000;
19
+ /**
20
+ * Ceiling a Gate's own explicit `timeoutMs` (domain/gate.ts) may request, overriding
21
+ * GATE_COMMAND_TIMEOUT_MS/GATE_TEST_TIMEOUT_MS for that one gate -- e.g. a task whose gate is a
22
+ * full monorepo test run that legitimately takes longer than either type default. Real, confirmed
23
+ * bug this exists for: a caller had no way to declare a longer-than-default gate timeout at all --
24
+ * `tasks.set_gates` silently accepted and dropped an experimental `timeoutMs` field before this.
25
+ * handlers/tasks.ts's GATE_OPERATION_LIMITS derives its own outer Vehicle transport deadline from
26
+ * this same constant, so the outer deadline can never fire strictly before a single gate honoring
27
+ * this ceiling has had a chance to.
28
+ */
29
+ export const GATE_TIMEOUT_MAX_MS = 300_000;
19
30
  export const GATE_OUTPUT_LIMIT = 200;
20
31
  export const GATE_MAX_BUFFER_BYTES = 1_048_576;
21
32
  export const GATE_FILE_MAX_BYTES = 1_048_576;
@@ -1,3 +1,5 @@
1
+ import { GATE_TIMEOUT_MAX_MS } from "../constants.ts";
2
+
1
3
  export const GATE_TYPES = ["file-exists", "command", "contains", "test"] as const;
2
4
  export type GateType = (typeof GATE_TYPES)[number];
3
5
 
@@ -5,6 +7,8 @@ export interface Gate {
5
7
  type: GateType;
6
8
  target: string;
7
9
  expect?: string;
10
+ /** Overrides GATE_COMMAND_TIMEOUT_MS/GATE_TEST_TIMEOUT_MS for this one "command"/"test" gate, bounded by GATE_TIMEOUT_MAX_MS (see its own doc comment). */
11
+ timeoutMs?: number;
8
12
  }
9
13
 
10
14
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -29,10 +33,17 @@ export function validateGates(value: unknown): Gate[] {
29
33
  if (entry.expect !== undefined && typeof entry.expect !== "string") {
30
34
  throw new Error(`gate at index ${index} expect must be a string`);
31
35
  }
36
+ if (
37
+ entry.timeoutMs !== undefined &&
38
+ (!Number.isInteger(entry.timeoutMs) || (entry.timeoutMs as number) < 1_000 || (entry.timeoutMs as number) > GATE_TIMEOUT_MAX_MS)
39
+ ) {
40
+ throw new Error(`gate at index ${index} timeoutMs must be an integer between 1000 and ${GATE_TIMEOUT_MAX_MS}`);
41
+ }
32
42
  return {
33
43
  type: entry.type as GateType,
34
44
  target: entry.target,
35
45
  ...(typeof entry.expect === "string" ? { expect: entry.expect } : {}),
46
+ ...(typeof entry.timeoutMs === "number" ? { timeoutMs: entry.timeoutMs } : {}),
36
47
  };
37
48
  });
38
49
  }
@@ -9,6 +9,7 @@ import {
9
9
  defineVehicleSchema,
10
10
  type VehicleContentBlock,
11
11
  VehicleError,
12
+ type VehicleLimits,
12
13
  type VehicleOperationContext,
13
14
  type VehicleSchemaCodec,
14
15
  } from "@danypops/vehicle-core";
@@ -244,6 +245,14 @@ export type DefineOperation = (
244
245
  required: readonly string[],
245
246
  resolve: (input: Record<string, unknown>) => Record<string, unknown>,
246
247
  execute?: (input: Record<string, unknown>, context: VehicleOperationContext<Record<string, unknown>>) => unknown,
248
+ /**
249
+ * Overrides this one operation's own Vehicle transport limits, distinct from every other
250
+ * operation this same createOperationDefiner call produces. For an operation that shells out
251
+ * to and waits on a real external command (e.g. tasks.run_gates/tasks.complete) rather than an
252
+ * instant CRUD read/write -- see handlers/tasks.ts's GATE_OPERATION_LIMITS for the motivating
253
+ * case. Omit to keep the definer's own default limits, unchanged for every other action.
254
+ */
255
+ limits?: VehicleLimits,
247
256
  ) => void;
248
257
 
249
258
  const STANDARD_OPERATION_LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
@@ -262,7 +271,7 @@ export function createOperationDefiner(
262
271
  permissions: readonly [string, string],
263
272
  defaultCall: (name: string, input: Record<string, unknown>) => unknown,
264
273
  ): DefineOperation {
265
- return (action, description, effect, properties, required, resolve, execute) => {
274
+ return (action, description, effect, properties, required, resolve, execute, limits) => {
266
275
  const operation = defineVehicleOperation({
267
276
  name: `${domain}.${action}`,
268
277
  version: 1,
@@ -272,7 +281,7 @@ export function createOperationDefiner(
272
281
  permissions: [...permissions],
273
282
  effect,
274
283
  idempotency: { mode: effect === "read" ? "safe" : "unsafe" },
275
- limits: STANDARD_OPERATION_LIMITS,
284
+ limits: limits ?? STANDARD_OPERATION_LIMITS,
276
285
  });
277
286
  registry.register(
278
287
  owner,
@@ -19,8 +19,10 @@
19
19
  *
20
20
  * remove/remove_subtree/restore are not duplicated here -- see ./artifact-trash-vehicle.ts.
21
21
  */
22
+ import type { VehicleLimits } from "@danypops/vehicle-core";
22
23
  import type { VehicleRegistry } from "@danypops/vehicle-server";
23
24
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
25
+ import { GATE_TIMEOUT_MAX_MS } from "../constants.ts";
24
26
  import type { TaskViewMode } from "../domain/task-scope.ts";
25
27
  import { tasksOperations } from "../modules/tasks.ts";
26
28
  import type { SessionIdentity } from "../session-identity/session-identity-service.ts";
@@ -40,7 +42,31 @@ import {
40
42
  } from "./shared.ts";
41
43
 
42
44
  const OWNER = "tasks";
43
- const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
45
+
46
+ /**
47
+ * tasks.run_gates/tasks.complete's own Vehicle transport limits, distinct from every other
48
+ * tasks.* CRUD action's shared STANDARD_OPERATION_LIMITS (createOperationDefiner's own default,
49
+ * 5s). Those two operations shell out to and wait on a real, caller-configured external command
50
+ * (domain/gate.ts's Gate.timeoutMs) -- the shared 5s CRUD default was an accidental inheritance,
51
+ * not a deliberate choice, and aborted the RPC call for any gate command that took longer than a
52
+ * few seconds, even a genuinely successful one (confirmed live, twice, with hard numbers: a real
53
+ * ~13s gate failed deterministically every time; a ~28s gate flapped around a separate, unrelated
54
+ * 30s inner per-gate-type default).
55
+ *
56
+ * defaultTimeoutMs is derived from GATE_TIMEOUT_MAX_MS (the longest a single gate's own explicit
57
+ * timeoutMs may request) plus a buffer for process-spawn/RPC/serialization overhead, so the outer
58
+ * transport deadline can never fire strictly before a single gate honoring that ceiling has had a
59
+ * chance to. A task with SEVERAL gates each near that ceiling can still exceed this default in
60
+ * aggregate (gates run sequentially -- see ops.ts's runGatesAsync); there is no aggregate
61
+ * task-level gate-time budget yet. maxTimeoutMs gives a caller who knows its gates are
62
+ * collectively slower room to explicitly request a longer deadline.
63
+ */
64
+ const GATE_OPERATION_LIMITS: VehicleLimits = {
65
+ defaultTimeoutMs: GATE_TIMEOUT_MAX_MS + 60_000,
66
+ maxTimeoutMs: GATE_TIMEOUT_MAX_MS * 4,
67
+ maxRequestBytes: 65_536,
68
+ maxResponseBytes: 262_144,
69
+ };
44
70
 
45
71
  const objectProp = { type: "object" } as unknown as { type: string };
46
72
  const arrayProp = { type: "array" } as unknown as { type: string };
@@ -456,6 +482,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
456
482
  const labels = labelsById(artifacts, dependencyIds);
457
483
  return { ...result, content: [{ type: "text" as const, text: completionContentText(labels, result) }] };
458
484
  },
485
+ GATE_OPERATION_LIMITS,
459
486
  );
460
487
 
461
488
  define(
@@ -484,6 +511,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
484
511
  "No gates configured.";
485
512
  return { gates, content: [{ type: "text" as const, text }] };
486
513
  },
514
+ GATE_OPERATION_LIMITS,
487
515
  );
488
516
 
489
517
  define(
@@ -496,7 +524,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
496
524
  );
497
525
  define(
498
526
  "set_gates",
499
- "Replaces a Task's gate commands in full.",
527
+ "Replaces a Task's gate commands in full. Each gate is {type, target, expect?, timeoutMs?} -- timeoutMs overrides the default per-type command timeout (30s)/test timeout (60s) for a legitimately slower gate, up to a bounded ceiling.",
500
528
  "local-write",
501
529
  { id: stringProp, name: stringProp, gates: arrayProp, project_root: stringProp },
502
530
  ["gates"],
package/src/ops.ts CHANGED
@@ -601,8 +601,9 @@ function readBoundedGateFile(path: string): string {
601
601
  /** Shared by the sync and async process-gate runners so "test" is never a second, independently
602
602
  * maintained copy of "command"'s own command-template/timeout selection. */
603
603
  function processGateCommand(gate: Gate): { command: string; timeout: number } {
604
- if (gate.type === "test") return { command: `npx vitest run ${gate.target} --reporter=dot`, timeout: GATE_TEST_TIMEOUT_MS };
605
- return { command: gate.target, timeout: GATE_COMMAND_TIMEOUT_MS };
604
+ if (gate.type === "test")
605
+ return { command: `npx vitest run ${gate.target} --reporter=dot`, timeout: gate.timeoutMs ?? GATE_TEST_TIMEOUT_MS };
606
+ return { command: gate.target, timeout: gate.timeoutMs ?? GATE_COMMAND_TIMEOUT_MS };
606
607
  }
607
608
 
608
609
  /**