/** * daemon/audit.ts — append-only record of what one session did to another. * * Cross-session action is now routine: a session can message another, dispatch * work to it, probe it, launch a new one, or rename it. Chains form that nobody * designed — an observation in one project reached a second session, which * relayed it to a third that happened to be fixing exactly that defect. Useful, * and entirely unplanned. * * The problem is that today the only record of any of it is each participant's * own account. A session that acts on another and does not mention it leaves no * trace; a session that ends takes its side of the story with it. "Why did work * happen in a project nobody had touched for two weeks" was answerable only * because the agent involved chose to say so. That is self-report, not audit. * * So: every daemon-mediated cross-session action is appended here as one JSON * object per line, before and independently of whatever the actor later says * about it. * * Design notes: * * - JSONL, not a database. Greppable with the tools already on the machine, * appendable by anything (other agents and MCP servers can write their own * events into the same file), and a truncated final line costs one record * rather than the file. * - Bodies are kept whole, inline when small and in a content-addressed * sidecar when not. The body IS the "why" — a summary of a message is * exactly the self-report this exists to replace — but a multi-KB line * cannot be appended atomically, so the text moves out and a hash stays in. * - Failures and REFUSALS are recorded too. "The hub declined to type this into * a shell" is as much a part of the history as a delivery. * - Causation is tracked but honestly labelled. Each actor's most recent * inbound message is remembered, and outgoing actions reference it as * `causedBy`. That reconstructs A→B→C chains correctly in the common case and * is a heuristic, not proof: an agent may act for reasons of its own. */ /** * ── Multi-writer contract ─────────────────────────────────────────────────── * * This file is designed to have more than one producer appending to it (the * hub, PAI's task bus, anything else worth recording). Three rules make that * safe, and all three are properties of the FORMAT rather than of each * writer's discipline — a convention only one side remembers is not a format. * * 1. LINES STAY SMALL. An O_APPEND write is only atomic up to a modest kernel * limit (PIPE_BUF is 512 bytes on macOS); beyond that two writers can * interleave mid-line and corrupt the file. Since bodies are the whole * point and routinely run to several KB — real lines here already reached * 3.5KB with a single writer — anything over INLINE_BODY_MAX is spilled to * a content-addressed sidecar and referenced by hash. Lines stay bounded no * matter how large the payload or how many writers arrive. * * 2. IDS ARE NAMESPACED AND SORTABLE. `--`, e.g. * `ab-m5x9k2p-7f3q2a`. The namespace prevents collisions between producers, * the timestamp makes the file sort chronologically, and the shape is * trivial to reimplement in any language — which matters, because * `causedBy` only chains across writers if their ids are mutually * recognisable. * * 3. ACTORS ARE NAMESPACED. `:` — `aibroker:hub`, * `session:Youdrill`, `pai:task-bus`, `todoist:someone@example.com`. Bare * names would collide across producers and quietly degrade the causation * heuristic, and a heuristic that degrades silently is harder to distrust * correctly than one that fails loudly. */ /** This producer's id namespace. Other writers use their own. */ export declare const AUDIT_NS = "ab"; /** Bodies longer than this are spilled to a sidecar so lines stay small. */ export declare const INLINE_BODY_MAX = 700; /** Generate an event id. See rule 2 above; other producers replicate this shape. */ export declare function newAuditId(ns?: string): string; export interface AuditEvent { /** Unique id for this event; referenced by `causedBy` on downstream events. */ id: string; ts: string; /** What happened: send | dispatch | ask | launch | rename | refuse. */ action: string; /** Who acted. Session label where known, else the raw caller id. */ actor: string; /** What was acted upon. */ target: string; /** delivered | spawned | refused | failed | … — verbatim from the operation. */ outcome: string; /** * The message body. Held inline when short; otherwise this is a preview and * the full text lives in the sidecar named by `bodyRef`. */ body?: string; /** sha256 of the full body, present only when it was spilled to a sidecar. */ bodyRef?: string; /** Length of the full body in bytes, so a truncated preview is obvious. */ bodyBytes?: number; /** Why it failed or was refused. */ reason?: string; /** The inbound event this actor was most recently handed. Heuristic. */ causedBy?: string; /** Anything action-specific. */ meta?: Record; } /** * Append one event. Never throws: auditing must not be able to break the * operation it is recording. */ export declare function audit(e: Omit & { id?: string; }): string; /** The full body of an event: inline text, or the sidecar it points at. */ export declare function resolveBody(e: AuditEvent): string | undefined; /** * Note that `target` has just been handed something, so their next outgoing * action can be attributed to it. This is what turns isolated events into a * chain. */ export declare function noteInbound(target: string, eventId: string): void; export interface AuditQuery { /** Only events where this string is the actor or the target. */ session?: string; /** Only this action type. */ action?: string; /** Follow a causation chain from this event id, in both directions. */ trace?: string; /** ISO timestamp lower bound. */ since?: string; limit?: number; } export declare function readAudit(q?: AuditQuery): AuditEvent[]; export declare function auditPath(): string; //# sourceMappingURL=audit.d.ts.map