@danypops/papyrus 0.54.2 → 0.54.3

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.54.2",
3
+ "version": "0.54.3",
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",
@@ -6,7 +6,7 @@ import { diagnoseDaemon, openDaemonLifecycleLog } from "@danypops/vehicle-server
6
6
  import { acquireDaemonLock, releaseDaemonLock } from "@danypops/vehicle-server/paths";
7
7
  import { PushChannel } from "@danypops/vehicle-server/push-channel";
8
8
  import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, dbPath, WAL_CHECKPOINT_INTERVAL_MS } from "../constants.ts";
9
- import { logEvent, vehicleLogger } from "../log/log.ts";
9
+ import { logEvent, logger } from "../log/log.ts";
10
10
  import { createApp, createPapyrusService } from "../service.ts";
11
11
  import {
12
12
  clearDaemonPort,
@@ -78,7 +78,7 @@ export async function serveMain(): Promise<void> {
78
78
  pushChannel.publish("tasks", { operation });
79
79
  }
80
80
  },
81
- logger: vehicleLogger(),
81
+ logger,
82
82
  diagnose: () => diagnoseDaemon({ lifecycleLog, current: { instanceId, pid: process.pid, startedAt, provenance } }),
83
83
  });
84
84
  const server = Bun.serve({
package/src/log/log.ts CHANGED
@@ -1,25 +1,36 @@
1
- import type { Logger } from "@danypops/vehicle-server/logging";
1
+ /**
2
+ * Structured daemon logging, now backed by `@danypops/vehicle-server/logging` (pino) instead of a
3
+ * hand-rolled `console.error(JSON.stringify(...))` -- gains real level filtering (including a real
4
+ * debug level this daemon previously had no way to emit at all) and a destination injectable for
5
+ * tests, matching jittor/src/log.ts's already-migrated shape exactly. One deliberate, disclosed
6
+ * shape change from the old bespoke format: the event name is now pino's `msg` field rather than a
7
+ * separate `event` field, matching daemon-kit's shared convention across every migrated daemon.
8
+ * `component`/`level`/`timestamp` and credential-safety (callers still must pass only bounded,
9
+ * non-sensitive fields; @danypops/vehicle-server/logging's own default redact list also catches any
10
+ * credential-shaped field that slips in regardless) are unchanged.
11
+ */
12
+ import { createLogger, type Logger, type LogLevel as VehicleLogLevel } from "@danypops/vehicle-server/logging";
2
13
 
3
- export type LogLevel = "info" | "warn" | "error";
4
-
5
- /** Credential-safe structured daemon event. Callers must pass bounded, non-sensitive fields. */
6
- export function logEvent(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
7
- console.error(JSON.stringify({ timestamp: new Date().toISOString(), level, component: "papyrus-daemon", event, ...fields }));
8
- }
14
+ export type LogLevel = Extract<VehicleLogLevel, "info" | "warn" | "error">;
9
15
 
10
16
  /**
11
- * Adapts logEvent to @danypops/vehicle-server's Logger interface (debug/
12
- * info/warn/error), so createVehicleHttpApp's own failure logging lands
13
- * through this daemon's one existing structured-log sink instead of
14
- * introducing a second logging system. debug is a no-op -- this daemon's
15
- * own logEvent never had a debug level, and none of its existing output
16
- * needs one.
17
+ * Pinned to `console.error` -- rather than `createLogger`'s own default of a raw fd 2 write via
18
+ * `pino.destination(2)`, which bypasses `console.error` entirely -- so existing tooling/tests that
19
+ * intercept `console.error` keep working unchanged (matches jittor/src/log.ts's own reasoning).
20
+ * Also satisfies @danypops/vehicle-server's own `Logger` port directly wherever one is needed (e.g.
21
+ * `createVehicleHttpApp`'s failure logging in daemon.ts), replacing the old `vehicleLogger()`
22
+ * adapter now that this logger already natively implements the full debug/info/warn/error surface.
17
23
  */
18
- export function vehicleLogger(): Logger {
19
- return {
20
- debug() {},
21
- info: (msg, fields) => logEvent("info", msg, fields),
22
- warn: (msg, fields) => logEvent("warn", msg, fields),
23
- error: (msg, fields) => logEvent("error", msg, fields),
24
- };
24
+ export const logger: Logger = createLogger("papyrus-daemon", {
25
+ destination: {
26
+ write: (chunk: string) => {
27
+ console.error(chunk.replace(/\n$/, ""));
28
+ return true;
29
+ },
30
+ },
31
+ });
32
+
33
+ /** Credential-safe structured daemon event. Callers must pass bounded, non-sensitive fields. */
34
+ export function logEvent(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
35
+ logger[level](event, fields);
25
36
  }
package/src/service.ts CHANGED
@@ -594,7 +594,7 @@ export function createApp(deps: {
594
594
  * (daemon.ts) wires this to a PushChannel; tests and other embedders can ignore it.
595
595
  */
596
596
  onOperationExecuted?: (operation: string, input: OperationInput) => void;
597
- /** Defaults to a no-op (createVehicleHttpApp's own default) -- daemon.ts wires vehicleLogger() so a failed invocation is actually logged, not silently discarded. */
597
+ /** Defaults to a no-op (createVehicleHttpApp's own default) -- daemon.ts wires log/log.ts's own `logger` so a failed invocation is actually logged, not silently discarded. */
598
598
  logger?: Logger;
599
599
  /**
600
600
  * Backs GET /daemon/diagnose -- "who am I, and what happened recently" (see
@@ -56,6 +56,10 @@ export class InMemoryTaskMutationRequestStore implements TaskMutationRequestStor
56
56
  }
57
57
 
58
58
  put(record: TaskMutationRequestRecord): void {
59
+ // Checked in the same order sqlite-task-mutation-request-store.ts's own catch-and-reclassify
60
+ // does: a still-pending (taskId, operation) always becomes the more specific
61
+ // TaskMutationPendingError first, regardless of which underlying constraint actually
62
+ // collided (SQLite's partial unique pending index, or the PRIMARY KEY check below).
59
63
  if (record.state === "pending" && record.taskId) {
60
64
  const existing = this.findPending(record.taskId, record.operation, record.createdAt);
61
65
  if (existing) {
@@ -66,7 +70,18 @@ export class InMemoryTaskMutationRequestStore implements TaskMutationRequestStor
66
70
  );
67
71
  }
68
72
  }
69
- this.records.set(this.recordKey(record.scope, record.key), { ...record });
73
+ // Mirror SQLite's own PRIMARY KEY (request_scope, idempotency_key): a genuine duplicate
74
+ // (scope, key) is always rejected, never silently overwritten -- SQLite throws on any such
75
+ // collision regardless of whether the colliding row's other columns match, so this does too,
76
+ // rather than only checking receiptId. Every real write path already dedupes via
77
+ // mutationRequests.get() before ever calling put() twice for the same (scope, key)
78
+ // (task-service.ts's prepareMutation()), so this is unreachable through normal application
79
+ // flow today -- it only guards a caller that talks to the store interface directly.
80
+ const recordKey = this.recordKey(record.scope, record.key);
81
+ if (this.records.has(recordKey)) {
82
+ throw new Error(`task mutation request already exists for scope "${record.scope}" key "${record.key}"`);
83
+ }
84
+ this.records.set(recordKey, { ...record });
70
85
  }
71
86
 
72
87
  complete(scope: string, key: string, responseJson: string, updatedAt: string): void {