Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 45 additions & 35 deletions BUILD_DEVIATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,41 +57,6 @@ Format:
- `tests/plugins/memory/test_iso_link_tools.py:test_create_relationlink_raises_deferred_write_error`
asserts the message contains all three blockers.

### D-kr2-st4-no-chain-emit-mcp-tool

- **Bucket**: KR-2 ST4 (chain event emission + recent events read + finalize)
- **Why**: Spec § ST4 § 1 mandates chain events go through a Sea MCP
tool (working name `kora__append_event`) — direct INSERT into
`hivex_foundation.event_log` is forbidden because it would skip the
substrate's `_emit_chain_event` SECDEF (which sets `prev_event_hash` /
`this_event_hash` to maintain chain witness integrity). The Sea MCP
server on substrate main `28ff4f78` exposes only
`kora__propose_convention`, `kora__read_escalation_queue`,
`kora__propose_policy_change` — no append-event tool. Same pattern
as the ST3 scratchpad-write deferral.
- **Closes when**: A Sea MCP append-event tool ships (working name
`kora__append_event`; PM coordinates with substrate-team / files
the substrate dispatch — likely K-9 on CC#1's lane, queued behind
K-7 + K-8). When it lands, `events.emit_kora_event` body switches
from `raise ChainEventEmitNotAvailableError()` to
`mcp_client.invoke('kora__append_event', ...)`. Caller signature
stays unchanged — `provider._attempt_chain_event_emit` and every
lifecycle hook that uses it (`sync_turn`, `on_memory_write`,
`on_session_end`, `on_delegation`) keep working without refactor.
- **Guarded by**:
- `plugins/memory/isokron/events.py` — top-of-module `[kora.isokron.todo]`
tag; `ChainEventEmitNotAvailableError` carries the deviation ID in
every raised message.
- `IsoKronMemoryProvider._attempt_chain_event_emit` — catches
`ChainEventEmitNotAvailableError` + logs a one-line WARNING
tagged with the deviation ID and the event_type that was skipped.
Operators grep `D-kr2-st4-no-chain-emit-mcp-tool` in logs.
- `plugins/memory/isokron/README.md` § "Operator pitfalls" —
chain event deferral notice.
- `tests/plugins/memory/test_events.py` —
`test_emit_kora_event_raises_deferred_write_error` asserts the
error message + tag stay correct.

### D-kr2-st3-no-scratchpad-write-mcp-tool

- **Bucket**: KR-2 ST3 (Scratchpad reads + writes)
Expand Down Expand Up @@ -154,6 +119,51 @@ Format:

## Closed

### D-kr2-st4-no-chain-emit-mcp-tool — closed by KR-7 (2026-05-20)

- **Bucket**: KR-2 ST4 (chain event emission)
- **Resolved by**: KR-7 — `plugins/memory/isokron/events.py:emit_kora_event`
body swaps from `raise ChainEventEmitNotAvailableError()` to
`await mcp_client.invoke('kora__append_event', {...})`. Returns the
K-9 substrate tool's `event_id` (UUID string). The provider's
`_attempt_chain_event_emit` fetches the real
:class:`IsoKronMCPClient` via `IsoKronConnection.get_mcp_client()`
(wired in KR-7a) and surfaces substrate-side failures as
`IsoKronMCPInvocationError` logged at ERROR (lifecycle hooks catch
+ log so the session stays alive). `iso_node_supersede`'s
`kora.node.superseded` emit routes through the same helper instead
of duplicating the wiring.
- **Spec quote** (KR-7 § 0): *"CC#1 just shipped K-9 (`f8487059`):
the `kora__append_event` Sea MCP tool now exists. KR-7 swaps CC#3's
KR-2 ST4 deferred-emit path from the placeholder error to a real
MCP call. ~20-40 lines Python; single PR; closes one
BUILD_DEVIATIONS."*
- **Production-test posture** (IsoKron PM #27): K-9's
`kora__append_event` handler is currently a `notImplementedHandler`
stub on substrate main; substrate-team's dispatch tier (queued)
bridges Layer-A `wsk_*` auth → Layer-B `actor_kind='kora'` and
un-stubs the handler. KR-7's code shape is sound and ships green
with mock tests; production deploys wait on the dispatch tier
landing. Verify-at-first-live-emit step: confirm
`event_log.actor_id` resolves to the 0076-seeded canonical Kora
actor (`actor_kind='kora' AND workspace_id=<Flynn workspace
clerk_org_id>`) — if it resolves to a token-UUIDv5 instead (the
`cowork-claude-pm` precedent), small substrate patch needed.
- **Deprecation runway**: `ChainEventEmitNotAvailableError` class
kept exported (tagged `[kora.isokron.deprecated]`) for one release
so any pinned downstream tests still import it. Class removal
scheduled when KR-N audits show no remaining references.
- **Guarded by**:
- `tests/plugins/memory/test_events.py` — replaced the
deferred-error test with four MCP-call-path tests covering happy,
error propagation, None-client defense, and unexpected-response
shape; deprecation-runway test asserts the class is still
importable.
- `tests/plugins/memory/test_provider_end_to_end.py` —
`_FakeProviderConnection` now exposes `get_mcp_client()` returning
a `_FakeMcpClient`; E2E asserts both emits succeed with the
spec-pinned tool name + arg shape.

### D-kr3-st1-capability-check-deferred — closed by KR-6 (2026-05-20)

- **Bucket**: KR-3 ST1 (`iso_node_*` tool family)
Expand Down
43 changes: 32 additions & 11 deletions plugins/memory/isokron/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,26 +93,34 @@ This `README.md` ships with **KR-2 ST1**, which delivers the structural skeleton

## Operator pitfalls

### Deferred-surface summary (4 open BUILD_DEVIATIONS as of KR-6)
### Deferred-surface summary (3 open BUILD_DEVIATIONS as of KR-7)

All four follow the same shape: signature is forward-stable, body
All three follow the same shape: signature is forward-stable, body
swaps from `raise <DeferredError>` to `mcp_client.invoke(...)` when
the substrate-side dependency lands. **No caller refactor needed**.
Operators grep the deviation_id in logs to track defer rates.

| Deviation | What's deferred | Closes when |
|---|---|---|
| `D-kr2-st2-capability-matrix-mirror` | C2 Python mirror of `ACTOR_CAPABILITY_MATRIX` Kora column (parity test guards drift) | K-7 ships Sea MCP `kora__read_kora_capability_row` |
| `D-kr2-st2-capability-matrix-mirror` | C2 Python mirror of `ACTOR_CAPABILITY_MATRIX` Kora column (parity test guards drift) | K-7→KR-N swap (Sea MCP `kora__read_kora_capability_row` shipped 2026-05-20; PM drafts swap bucket) |
| `D-kr2-st3-no-scratchpad-write-mcp-tool` | Scratchpad writes from `sync_turn` / `on_memory_write` / `iso_node_create` / `iso_node_supersede` | K-8 ships Sea MCP `kora__write_agent_scratchpad` |
| `D-kr2-st4-no-chain-emit-mcp-tool` | Chain event emission from `on_session_end` / `on_delegation` / `iso_node_supersede` | K-9 ships Sea MCP `kora__append_event` |
| `D-kr3-st2-no-relationlink-write-mcp-tool` | `iso_link_create` writes — 3 substrate blockers in one (actor_kind CHECK + missing MCP tool + chain_event_id SECDEF) | K-10 ships the bundled substrate bucket |

**Recently closed**: `D-kr3-st1-capability-check-deferred` — KR-6
shipped the Python `actor_has_capability` helper at
`plugins/memory/isokron/capability_check.py`. Every `iso_*` tool now
gates through a real check; denied calls surface a structured
`{"ok": false, "denied": true, "capability": ..., "reason": ...}`
envelope.
**Recently closed**:
- `D-kr2-st4-no-chain-emit-mcp-tool` — KR-7 swapped `emit_kora_event`
to route through `kora__append_event` via the KR-7a-wired
`IsoKronMCPClient`. Substrate-side failures surface as
`IsoKronMCPInvocationError` logged at ERROR; lifecycle hooks catch
+ log so the session stays alive. Production-test posture per
IsoKron PM #27: substrate-team dispatch tier (queued) un-stubs the
K-9 handler; live emits will fail until then but the code shape is
correct.
- `D-kr3-st1-capability-check-deferred` — KR-6 shipped the Python
`actor_has_capability` helper at
`plugins/memory/isokron/capability_check.py`. Every `iso_*` tool
now gates through a real check; denied calls surface a structured
`{"ok": false, "denied": true, "capability": ..., "reason": ...}`
envelope.

### MCP client (KR-7a)

Expand Down Expand Up @@ -140,7 +148,20 @@ idempotency) is fully tested with mocked transports.

### Individual pitfalls

* **Chain event emission is currently deferred — `kora.*` events are NOT being written to `event_log`.** KR-2 ST4 ships the emit API (`events.emit_kora_event`) but the substrate-side Sea MCP tool (`kora__append_event` or equivalent) doesn't exist yet (substrate main `28ff4f78`). Until it lands, every emit raises `ChainEventEmitNotAvailableError`; `sync_turn` / `on_memory_write` / `on_delegation` / `on_session_end` catch it + log a one-line WARNING tagged `D-kr2-st4-no-chain-emit-mcp-tool`. This is a chain-audit gap — operators inspecting Kora's recent activity via the `system_prompt_block` §6 section will see only events that landed in `event_log` through other paths (e.g. SECDEF-emitted events from `compact_scratchpad`). Direct INSERT into `event_log` is forbidden — it would skip the `_emit_chain_event` SECDEF and break the `prev_event_hash` / `this_event_hash` witness chain. Tracked in `BUILD_DEVIATIONS.md`.
* **Chain event emission routes through `kora__append_event` via the
KR-7a-wired `IsoKronMCPClient`.** KR-7 closed the deferred-emit surface.
`events.emit_kora_event` now calls `mcp_client.invoke('kora__append_event', …)`
and returns the substrate-assigned `event_id`. Substrate-side
failures surface as `IsoKronMCPInvocationError`; the provider's
`_attempt_chain_event_emit` catches at the lifecycle boundary
(`on_session_end` / `on_delegation` / `iso_node_supersede`) and logs
at ERROR (`[kora.chain.emit.failed]`) so operators see drops without
the session crashing. Successful emits log INFO `[kora.chain.emit]`
with the event_id. **Production-test posture** per IsoKron PM #27:
the substrate-side K-9 handler is currently a `notImplementedHandler`
stub; the dispatch tier (queued substrate-team) un-stubs + resolves
Layer-A→Layer-B `actor_kind='kora'`. Until then live emits return
substrate errors; mock-tested code shape stays correct.

* **`event_log` is the one genuine `tenant_id UUID`-keyed substrate table.** Every other Kora table (`kora_role_charter`, `kora_policy_registry`, `kronicle.agent_scratchpad_entries`, `kronicle.workspace_constitution_revisions`) is `workspace_id TEXT`-keyed. `read_recent_kora_events` resolves the workspace_id (Clerk `org_*`) to tenant_id via `JOIN hivex_foundation.tenant ON t.clerk_org_id = $1`. If you bypass `events.read_recent_kora_events` and write your own SQL, replicate the JOIN — a `WHERE workspace_id = $1` against `event_log` will fail (no such column on that table).

Expand Down
102 changes: 67 additions & 35 deletions plugins/memory/isokron/events.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Chain event emit + recent events read (KR-2 ST4).
"""Chain event emit + recent events read (KR-2 ST4 + KR-7).

Two halves:

Expand All @@ -11,15 +11,16 @@
t.clerk_org_id = $1``. Matches the TS reader at
``packages/sea-mcp-server/src/kora/context-assembler/index.ts:287``.

- :func:`emit_kora_event` — deferred write surface. Chain events go
through the substrate's ``_emit_chain_event`` SECDEF (which sets
``prev_event_hash`` / ``this_event_hash`` to maintain chain witness
integrity); calling it from runtime Python without the SECDEF wrapper
would break the witness chain. The path is a Sea MCP tool
(working name ``kora__append_event``); as of substrate main
``28ff4f78``, no such tool is registered. Raises
:class:`ChainEventEmitNotAvailableError` until the tool ships.
BUILD_DEVIATIONS ``D-kr2-st4-no-chain-emit-mcp-tool``.
- :func:`emit_kora_event` — chain event emit via the
``kora__append_event`` Sea MCP tool. K-9 shipped the substrate tool
(`f8487059`); KR-7 (this swap) replaced the previous
``ChainEventEmitNotAvailableError`` defer with a real
``mcp_client.invoke`` call. Returns the new event_id (UUID string).
Production-test posture: substrate-team's dispatch tier (queued)
bridges Layer-A wsk_* auth → Layer-B ``actor_kind='kora'`` and
un-stubs the K-9 handler; until that lands, live calls return
substrate-side errors but the code shape is correct. KR-7a's
``IsoKronMCPClient`` handles the transport.
"""

from __future__ import annotations
Expand All @@ -37,21 +38,25 @@


class ChainEventEmitNotAvailableError(RuntimeError):
"""Raised by :func:`emit_kora_event` until the Sea MCP tool ships.

Same pattern as :class:`scratchpad.ScratchpadWriteNotAvailableError`
— runtime callers MUST NOT bypass with direct INSERT or
``_emit_chain_event`` calls (would break chain witness integrity).
"""[DEPRECATED in KR-7] Raised by the pre-K-9 deferred-emit path.

Kept exported for one release so any downstream code or pinned
tests that still reference the class still resolve. After KR-7
(which swapped the defer for a real ``mcp_client.invoke`` call)
this class is no longer raised by ``emit_kora_event``; substrate-
side failures now surface as
:class:`IsoKronMCPInvocationError` from ``mcp_client``.

BUILD_DEVIATIONS ``D-kr2-st4-no-chain-emit-mcp-tool`` is Closed in
KR-7. Remove this class when KR-N audits show no remaining
references.
"""

DEFAULT_MESSAGE = (
"[kora.isokron.todo] chain event emit deferred — Sea MCP server "
"does not yet expose kora__append_event (or equivalent). Tracked "
"in BUILD_DEVIATIONS.md as D-kr2-st4-no-chain-emit-mcp-tool. "
"Direct INSERT into hivex_foundation.event_log bypasses the "
"prev_event_hash / this_event_hash chain — do NOT do that; the "
"MCP tool wraps the substrate's _emit_chain_event SECDEF which "
"preserves chain witness integrity."
"[kora.isokron.deprecated] ChainEventEmitNotAvailableError is "
"obsolete after KR-7 — chain event emits now route through "
"kora__append_event via IsoKronMCPClient. Substrate-side "
"failures surface as IsoKronMCPInvocationError."
)

def __init__(self, message: Optional[str] = None):
Expand Down Expand Up @@ -171,22 +176,49 @@ async def emit_kora_event(
workspace_id: str,
event_type: str,
payload: Any,
mcp_client: Any = None,
mcp_client: Any,
) -> str:
"""Emit a ``kora.*`` chain event via the Sea MCP tool surface.
"""Emit a ``kora.*`` chain event via the ``kora__append_event`` MCP tool.

Raises ``ChainEventEmitNotAvailableError`` until
``kora__append_event`` (or equivalent) lands in the Sea MCP
server. Caller signature matches the future MCP-backed
implementation; when the tool ships the body switches to an
``mcp_client.invoke('kora__append_event', ...)`` call without
any caller-side refactor.
Returns the new ``event_id`` (UUID string) on success.
``mcp_client`` must be a started :class:`IsoKronMCPClient`
(typically obtained via ``IsoKronConnection.get_mcp_client()``).

``event_type`` must start with ``kora.`` and appear in the
``event_log_event_type_check`` constraint set (foundation/0136 +
foundation/0138 ship the canonical vocabulary). Validation is
enforced substrate-side by the MCP tool — runtime callers pass
the literal through.
foundation/0138 ship the canonical vocabulary). The Sea MCP tool
validates with a Zod regex ``^kora\\.[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$``
— runtime callers pass the literal through; bad values surface as
``IsoKronMCPInvocationError`` from the MCP boundary.

Raises:
IsoKronMCPInvocationError — substrate-side error (CHECK violation,
actor_kind resolution failure, chain lock failure, etc.).
IsoKronMCPNotStartedError — ``mcp_client`` is not started.
ValueError — ``mcp_client`` is ``None`` (defensive: should have
been resolved before calling).
"""
del workspace_id, event_type, payload, mcp_client
raise ChainEventEmitNotAvailableError()
if mcp_client is None:
raise ValueError(
"emit_kora_event: mcp_client is required (resolve via "
"IsoKronConnection.get_mcp_client() before calling)"
)
result = await mcp_client.invoke(
"kora__append_event",
{
"workspace_id": workspace_id,
"event_type": event_type,
"payload": payload,
},
)
# K-9 contract: tool returns {'event_id': '<uuid>'}.
event_id = result.get("event_id") if isinstance(result, dict) else None
if not isinstance(event_id, str):
# Defensive: surface a clear error if the substrate response
# shape drifts (the parity is informal — Zod-strict on the
# input side, but the output is just a dict).
raise RuntimeError(
f"kora__append_event returned unexpected shape: {result!r}; "
f"expected {{'event_id': '<uuid>'}}"
)
return event_id
Loading