Skip to content
Back to skills

Event Log And Reducer

ASecurity

The append-only event log + deterministic reducer pattern John uses to coordinate parallel subagent work on shared state. Each subagent emits its own event files; one reducer folds all events into canonical state. Beats file locks, scales to thousands of work units.

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added June 3, 2026
ai-agentspythonrustgoreact

Works with

  • claude code
  • cli

Security analysis

A100/100

Scanned June 3, 2026

npx -y skills add kitchen-engineer42/joharnessburg --skill event-log-and-reducer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Event Log And Reducer?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Event Log And Reducer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kitchen-engineer42-event-log-and-reducer/badge)](https://www.skillsdirectory.com/skills/kitchen-engineer42-event-log-and-reducer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: event-log-and-reducer
description: The append-only event log + deterministic reducer pattern John uses to coordinate parallel subagent work on shared state. Each subagent emits its own event files; one reducer folds all events into canonical state. Beats file locks, scales to thousands of work units.
metadata:
  triggers:
    - event log
    - reduce events
    - coordinate subagents
    - shared state
    - reducer
---

# event-log-and-reducer

When N subagents are working in parallel on shared state, the naive approach (each subagent writes to a shared catalog file with a lock) is what kc_cli learned the hard way is fragile at scale. John uses **event log + reducer** instead — same shape that React/Redux and event-sourced systems use, ported to filesystem.

## The pattern in one diagram

```
Subagent A ── events/extract/chunks/A-001.json (append-only, A's own file) ─┐
Subagent B ── events/extract/chunks/B-002.json                              │
Subagent C ── events/extract/chunks/C-003.json                              ├──► reducer ──► .john/checkpoints/extract/state.json
...                                                                         │              (canonical)
Subagent N ── events/extract/chunks/N-200.json                             ─┘
```

- Each subagent writes only to its own event file. Zero contention.
- The reducer reads all event files for a phase, in deterministic order, and produces canonical state.
- Running the reducer twice yields the same output (idempotent).
- Canonical state is the read source for the next phase.

## Where files live

In the user's project, under `<project>/.john/`:

- `<project>/.john/events/<phase-name>/<work-unit-type>/<subagent-id>-<sequence>.json` — events. One file per subagent emission.
- `<project>/.john/checkpoints/<phase-name>/state.json` — canonical reduced state. Written by reducer.

Conventions:
- Subagent-id is a short identifier (e.g., `chunk-042` for "the subagent that processed chunk 042").
- Sequence is an integer or timestamp, in case one subagent emits multiple events.
- Event files are JSON; canonical state is JSON. Markdown if the canonical state is human-facing.

## Event shape — one valid approach

The only hard requirements (everything else is taste):

1. One event = one self-contained record. No references that require another event to interpret.
2. JSON-parseable.
3. Enough metadata for the reducer to order and deduplicate (timestamp + a sender id is the usual minimum).

A **minimal** schema that satisfies the rules:

```json
{ "timestamp": "ISO 8601", "subagent_id": "string", "payload": {} }
```

A **richer** schema (used in many knowledge-extraction templates) — useful when you need to query the event log by type:

```json
{
  "event_type": "entry_extracted",
  "work_unit_id": "chunk_042",
  "timestamp": "2026-05-21T10:42:33Z",
  "subagent_id": "sub-xx7f3a",
  "payload": { "entry_ids": ["e_001", "e_002"], "notes": "..." }
}
```

Templates and phase-specific skills define their own schemas freely, as long as the three requirements above hold. Neither shape above is canonical — wide tunnel.

## How to write the reducer

The reducer is a script (Python — John ships `scripts/reduce_events.py`) that:

1. **Reads all event files** under `<project>/.john/events/<phase>/`.
2. **Sorts them deterministically** (timestamp + subagent_id is a safe primary key). At thousands-of-events scale, clock skew or identical timestamps happen — `${CLAUDE_PLUGIN_ROOT}/scripts/reduce_events.py` handles the tiebreaker. If your fold function depends on strict ordering, review the tiebreaker before trusting the result.
3. **Folds them into canonical state** using a per-phase fold function. The fold function's exact shape depends on what the phase is producing — for extraction, it concatenates entry lists and indexes by ID; for review, it tallies pass/fail; etc.
4. **Writes canonical state** to `<project>/.john/checkpoints/<phase>/state.json`.
5. **Returns idempotently**: running it twice with the same event set produces the same output, bit-for-bit.

Idempotency matters because the reducer may be invoked multiple times during a phase (e.g., after each wave of subagents) without state corruption.

## Failure handling

A subagent crashed mid-write? Its event file is partial or absent. Options for the reducer:

- **Strict**: refuse to fold if any expected event is missing. The phase isn't done; the user must re-dispatch the missing work units.
- **Lenient**: fold what's there, mark the missing work units in canonical state, and surface them in the phase's "Open decisions" or Log. Reducer's choice; document which.

John defaults to **lenient with explicit flagging** — the phase advances on best-effort, but PLAN.md gets a Log entry listing the missing units so they can be re-dispatched.

## Validation + quarantine

Subagents writing JSON events sometimes produce unparseable files — most commonly when string values contain unescaped inner quotes (a real ~10% defect rate is observable on Chinese-language content). The reducer handles this by **quarantining** unparseable events:

1. On read, `json.loads(file.read_text())` is wrapped in a try/except.
2. On parse failure: move the original file to `<project>/.john/events/<phase>/_quarantine/<original-filename>` and write a sibling `<original-filename>.parse_error.txt` with the exception message.
3. Continue reducing the remaining events.
4. At end-of-phase: log "N events quarantined" prominently in the reducer's stdout output.

This is a **lenient + surfaced** policy: the phase doesn't fail outright (one bad event shouldn't kill 199 good ones), but the user knows immediately how many events were lost and where to inspect them.

**For agents emitting events**: see the JSON-discipline section in your agent role. Prefer full-width `「」` quotes or `json.dumps()` to avoid the quarantine path entirely.

## Why this beats file locks

The file-lock approach (one shared catalog file, subagents lock-modify-unlock) hits two recurring failures:

1. **Lock held too long.** A subagent that pauses (LLM timeout, slow tool call) holds the lock, blocking everyone else. Eventually the lock-acquire timeout fires, but you've serialized parallel work.
2. **Starvation.** Some subagents repeatedly fail to acquire the lock and never make progress.

Both are structural to the lock pattern, not bugs. Event log + reducer removes both: every subagent has its own file, no contention, no waiting.

The deeper reason event-log+reducer is John's default isn't throughput — typical runs are 50-300 entries (cost-bounded). The reason is **architectural ceiling**: even at 50 entries, locks are fragile under any unusual load (a slow tool call, a retry); event-log isn't. Choosing this pattern from the start gives auditability, recoverability, and freedom to scale to thousands without re-architecting.

It also adds:
- **Replay.** Replay the reducer on a subset of events to see what canonical state would look like.
- **Audit.** Full history of every subagent's emissions, in order, on disk.
- **Recovery.** If a reducer bug corrupts canonical state, you can fix the reducer and re-derive state from events — events are immutable history.

## When NOT to use events

- **Single subagent.** If only one subagent is producing output for a phase, it can just write canonical state directly. Events buy nothing.
- **Strictly sequential pipelines.** If unit B requires unit A's canonical-state output, they're not really parallel. Run A, fold, run B.
- **State that's naturally one file** (e.g., a single `glossary.json` that grows with cross-subagent contributions). Even here, events + reducer often wins because of the audit trail, but a careful append-with-fsync pattern can suffice.

## Scaling thoughts

For thousands of work units, partition events into sub-directories per work-unit-type (`events/extract/chunks/`, `events/extract/figures/`, etc.) and make the reducer incremental (read only events newer than the last canonical state's timestamp). The shipped script supports this; templates can extend it.

## What the subagent skill says about events

Every John skill that dispatches subagents (knowledge-extraction, slide-rendering, app-feature-author, etc.) instructs the subagent to "emit events to `<project>/.john/events/<phase>/...`, do not write canonical state directly." That's the contract. The subagent doesn't need to know how the reducer works; it just emits events in the shape the phase expects.

## Workflow agents are also event producers

When a fan-out phase runs as a dynamic workflow ([[vertical-workflows]]) instead of inline dispatch, **nothing here changes.** The workflow's worker agents write the same events to `<project>/.john/events/<phase>/`; you still run this reducer afterward; the checkpoint is still truth. The workflow keeps results in *script variables* for a clean context, but those are a convenience — the durable record is the event files on disk.

Two consequences worth holding onto:

- **The event log, not the runtime, is what survives.** Workflow resume is session-bound: stop a run and it resumes within the session, but exit Claude Code and the workflow restarts fresh. The event log isn't session-bound — it survives restarts, compaction, and crashes. So a workflow-driven phase is durable for the same reason an inline one is: the events are on disk, and the reducer can re-derive state from them at any time.
- **16 concurrent writers, zero contention — by design.** A workflow runs up to 16 agents at once, all potentially writing state. The one-file-per-agent append-only design (the whole reason John chose event-log over file-locks) already eliminates the write contention that naive parallel adoption would hit. John pre-solved the concurrency hazard; running multiple workflow *batches* into the same event tree is equally safe — reduce once at the end.

## Cross-references

- [[subagent-dispatch]] — what triggers the fan-out that emits events
- [[vertical-workflows]] — workflow workers are event producers too; truth lives here, not in the run
- [[ralph-loop]] — runs reducer at phase end and advances PLAN.md
- [[workspace-discipline]] — disk-is-truth includes checkpoint state
- [[phase-design]] — phases that fan out declare their event schemas

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.