Activity Log

June 22, 2026 · View on GitHub

Status: M0–M4 shipped. ExecPlan crux-dual-surface-activity-log-2026-06-18. Capture + agent lane + human page + verify cross-walk, all behind CORECRUXD_FEATURE_ACTIVITY_LOG.

The activity log gives every session a rolling, chronological record of what agents do — captured once per turn-event and read two ways off one source of truth:

  • Agent lane — a cheap, token-budgeted pull (GET /v1/activity + MCP activity_recent) answering "what just happened in this session" in ~500 tokens.
  • Human lane (M3, pending) — a console Activity tab rendering the same rows with verbatim text + a ✓verify badge.

Both lanes join on turn_id and reference the same append id, so the human view is the agent view with prose rehydrated — they can never disagree.

Enabling it

Off by default. Set the flag in the daemon's environment:

CORECRUXD_FEATURE_ACTIVITY_LOG=1

With the flag unset the routes return 404 and the daemon behaves exactly as before. Optional retention override (default 24h):

CORECRUXD_FEATURE_ACTIVITY_LOG_TTL_SECS=86400

The seven categories (kind)

question · answer · reasoning · command · fact · execplan · handoff · error

(reasoning is best-effort — see OD-15 on harness reasoning capture.)

Capture — POST /v1/activity

Append a journal entry. Requires a write scope (facts:write or admin:write) for the body's tenant_id. The actor is bound to the authenticated passport (the body cannot spoof it).

POST /v1/activity
{
  "tenant_id": "acme",
  "session_id": "sess-abc",
  "turn_id": "turn-12",          // optional; the join key to receipts + the human lane
  "kind": "command",
  "text": "ran cargo test -p corecruxd",
  "refs": { "fact_ids": [], "receipt_ids": [], "event_ids": [] },  // optional cross-refs
  "meta": { "tool": "Bash", "intent": "test", "confidence": null }, // optional
  "private": false                // PII: never syncs remote, author-scoped reads
}

Response 201 is the persisted crux.activity.journal_entry.v1 entry, with a content-addressed entry_id that is also recorded in refs.receipt_ids (the append's audit reference). Each append broadcasts an activity.appended event on /v1/events/stream (ids + kind only — never the verbatim text).

Privacy: reserved-prefix tokens (__agent::, __ops::, __bootstrap__::) are stripped from text on persist and on every read. private:true entries are returned only to the authoring passport and never sync to a remote.

Agent lane — GET /v1/activity

GET /v1/activity?tenant_id=acme&session=sess-abc&token_budget=500
                 [&since=<seq>] [&kinds=command,error]
  • token_budget is mandatory (QC.2) — a missing/zero value is a 400.
  • Returns compact rows newest-first, privacy-scoped, trimmed to fit the budget (using the same estimator as tool_trace_recent).
{
  "session_id": "sess-abc",
  "token_budget": 500,
  "returned": 3,
  "truncated": false,
  "rows": [
    { "turn_id": "turn-12", "seq": 41, "ts": "...", "kind": "command",
      "tool": "Bash", "intent": "test", "confidence": null,
      "fact_refs": [], "receipt_ids": ["act_…"], "preview": "ran cargo test …" }
  ]
}

preview is truncated — never the verbatim text. To get the full text for a turn, deref it:

GET /v1/activity/turn/{turn_id}?tenant_id=acme&session=sess-abc

returns the full entries[] with verbatim text and refs.

MCP — activity_recent

Same pull from chat; applies a default token_budget of 500.

activity_recent({ "session_id": "sess-abc", "kinds": ["error","command"], "token_budget": 500 })

Human lane — /console/activity

A new page on the embedded console (linked from the console header). It is a self-contained, dependency-free page that:

  • streams live rows via EventSource('/v1/events/stream?types=activity.appended'),
  • backfills via GET /v1/activity (tenant / session / token_budget inputs),
  • colour-codes rows by kind, filters client-side by free text,
  • expands a row to its verbatim text via GET /v1/activity/turn/{turn_id} with a ✓verify badge per receipt ref.

Deep-link with ?session=<id>&tenant_id=<t>. The page is inert (its API calls return 404) unless CORECRUXD_FEATURE_ACTIVITY_LOG=1. Iterate without a rebuild by pointing CORECRUXD_CONSOLE_DEV_PATH at a dir containing activity.html.

Verification cross-walk (M4)

The agent-lane row's receipt_ids for a turn are byte-identical to the human-lane deref's refs.receipt_ids for the same turn_id — guaranteed by the parity test activity_turn_id_parity_agent_vs_human_lane. The console ✓verify badge (M3) reuses the existing receipt_verify path against those ids.

What's not here yet

  • Reasoning capture (category 3) — best-effort, pending OD-15.
  • Full CROWN signing of each append — today the append id + the activity.appended projection row satisfy the audit-trail requirement (T.4); CROWN co-signing is a tracked follow-up.
  • Hook clients — POST /v1/activity is the ingestion contract; per-workstation Claude Code hooks (prompt/stop/posttooluse → append) are an opt-in step, not shipped enabled.