Audit trail

An audit trail without a second copy of the chart

Compliance asks "who ran what, when." MedSeek answers without storing any note text: one receipt per tool execution, digests only.

What one receipt holds - and only this

{
  "seq": 12, "prevHash": "ab41...", "hash": "9f02...",
  "ts": "2026-09-08T07:31:04.221Z",
  "tool": "pubmed_evidence_search", "callId": "call-88",
  "isError": false,
  "argsHash": "c3aa...", "resultHash": "77be..."
}

Sequence number, previous receipt's hash, timestamp, tool name, call id, error flag, and SHA-256 digests of arguments and result content. No note text, no query strings, no result bodies.

Tamper-evident by construction

Each line binds to the previous line's hash. Editing or deleting any historical line breaks every hash after it. Verification is a few lines against the published dsh-medseek/audit export:

import { readFileSync } from 'node:fs'
import { verifyChain } from 'dsh-medseek/audit'

const lines = readFileSync(process.env.DSH_HOME + '/medseek-audit.jsonl', 'utf8')
  .split('\n').filter((l) => l.trim())
console.log(verifyChain(lines)) // null = intact; number = first broken line

Safe under real deployments

  • Cross-process: appends serialize through upstream's bounded writer lock (a <file>.lock sibling with a hard timeout), so two writers can never mint the same sequence or chain position.
  • Fail-closed: before appending, the tail is re-read and re-verified. A torn, unreadable, or tampered tail means the receipt is dropped with an operational warning - never a silent fresh chain inside the damaged file. Move the file aside deliberately to start anew.
  • Never blocking: receipts are written off the tool-execution path; auditing failing never breaks a clinical task.
Precise limitation, stated plainly: a dropped receipt is not visible in the file alone - numbering continues from the last successful line, so the file always looks intact. The observable signal is exactly one content-free operational warning per drop on stderr (medseek-audit:). Route stderr to your log infrastructure if drop visibility matters.

Where it lives

medseek-audit.jsonl plus its lock sibling sit in the same dsh home root dsh itself resolves (explicit override > $DSH_HOME > ~/.dsh). Disable entirely with id: medseek-audit, config: { mode: off }. Session logs are dsh's own store and are a separate matter - see the plugin's permissions-and-data documentation.

Design and tests: src/audit.ts, tests/audit.spec.ts, tests/audit-concurrency.spec.ts (three concurrent OS processes, one intact chain).