Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(KR-2 ST4): chain events + Constitution + session context — closes KR-2#8

Merged
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR2-events-and-finalize
May 20, 2026
Merged

feat(KR-2 ST4): chain events + Constitution + session context — closes KR-2#8
rafe-walker merged 1 commit into
mainfrom
feat/kora-KR2-events-and-finalize

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Closes KR-2. Ships the final ST4 pieces:

  • Recent chain events read against hivex_foundation.event_log (the substrate's one genuine tenant_id UUID-keyed table). Resolves Kora's workspace_id TEXT to tenant_id via JOIN hivex_foundation.tenant ON t.clerk_org_id = $1. Filters event_type LIKE 'kora.%'. Truncates jsonb_pretty(payload) to 300 chars + ellipsis to match TS-side KoraRecentChainEvent. Default LIMIT 50.
  • Active Constitution revision read against kronicle.workspace_constitution_revisions. Schema gotcha verified: no superseded_at column; "active" = ORDER BY revision_number DESC LIMIT 1 (riding the idx_constitution_revisions_workspace_current index). rules_hash BYTEA hex-encoded to match the TS-side constitutionVersionHashFromRulesHash helper so K-6 (Constitution pre-screen middleware) sees the same canonical hex form. Returns None for fresh-workspace bootstrap state.
  • KoraSessionContext shape + assembler mirroring TS-side packages/sea-mcp-server/src/kora/context-assembler/types.ts:130. Six load-bearing reads + two identity fields (workspace_id, assembled_at). assemble_session_context fans out the six reads via asyncio.gather.
  • System prompt block extended with §6 "Recent kora. activity"* surfacing the last 5 events as readable bullets (size-bounded; full history queryable via read_recent_kora_events directly).
  • All remaining ABC stubs replaced with real implementations: on_session_end, on_session_switch (cache flush on reset=True), on_pre_compress (returns ""), on_delegation (scratchpad mirror + emit), save_config (no-op per env-var config), handle_tool_call (inherits ABC default for the no-tools case). No NotImplementedError stubs remain in the provider.

Rule-3 STOP-gate findings during recon

  • Spec SQL § ST4 § 2 used WHERE workspace_id = $1 against hivex_foundation.event_log. The actual schema (foundation/0003:73) keys off tenant_id UUID NOT NULL REFERENCES tenant(tenant_id) — no workspace_id column. PM's ST4 dispatch caught this correctly: this IS the substrate's one tenant_id exception. Implementation uses the TS reader's JOIN pattern verbatim (verified against packages/sea-mcp-server/src/kora/context-assembler/index.ts:287).
  • No kora__append_event MCP tool exists. Same probe as the ST3 scratchpad-write recon: only kora__propose_convention, kora__read_escalation_queue, kora__propose_policy_change registered in Sea MCP src/tools/. Per spec § ST4 § 1: "If kora__append_event is missing: defer behind ChainEventEmitNotAvailableError, file BUILD_DEVIATIONS D-kr2-st4-no-chain-emit-mcp-tool". Done — third deferred-write surface, same pattern as ST2 capability-row mirror + ST3 scratchpad write.
  • C2 capability mirror was 1 cap stale. When the ST2 parity test ran against current substrate main, it caught cap_unbless_convention (operator-only, added 2026-05-20) missing from the Python mirror. Updated the mirror + counts (24+25=49; Kora granted=22, denied=27). This is exactly the drift the C2 parity guard was built to catch — closing the C2 deviation still gated on K-7 substrate dispatch.

What landed

File Change
plugins/memory/isokron/events.py newRecentChainEvent dataclass, ChainEventEmitNotAvailableError, read_recent_kora_events, deferred emit_kora_event, canonical SQL + payload truncation.
plugins/memory/isokron/constitution.py newActiveConstitutionRevision dataclass, read_active_constitution_revision with RLS-GUC-in-transaction, BYTEA hex-encoder accepting bytes/bytearray/memoryview/str.
plugins/memory/isokron/session_context.py newKoraSessionContext dataclass mirroring TS types.ts:130; assemble_session_context async fan-out via asyncio.gather.
plugins/memory/isokron/provider.py _prefetch_all now gathers 7 reads (4 ST2 + 2 ST3 + ST4 events + ST4 constitution); system_prompt_block adds §6; new _attempt_chain_event_emit helper; all remaining ABC stubs replaced (no NotImplementedError left); new session_context() public API returning the typed shape from warm caches.
plugins/memory/isokron/capability_matrix_mirror.py added cap_unbless_convention: False per parity-test drift catch.
BUILD_DEVIATIONS.md new entry D-kr2-st4-no-chain-emit-mcp-tool under Open — closes when Sea MCP append-event tool ships.
plugins/memory/isokron/README.md "Operator pitfalls" gains 3 entries: chain emit defer + grep tag, event_log tenant_id-keying exception, Constitution table has no superseded_at. Sub-task table marked KR-2 closed.
4 new test files test_events.py (9 tests), test_constitution.py (8), test_session_context.py (3), test_provider_end_to_end.py (2 — full E2E).
2 modified tests test_isokron_provider_skeleton.py parametrize replaced with stub-free verification; test_reads.py + test_capability_matrix_parity.py updated for the 49-cap matrix size.

Key SQL (verbatim, mirrors TS readers)

-- read_recent_kora_events: event_log is tenant_id-UUID-keyed
SELECT el.event_id::text, el.event_type, el.occurred_at,
       jsonb_pretty(el.payload)::text AS payload_text
FROM hivex_foundation.event_log el
JOIN hivex_foundation.tenant t ON t.tenant_id = el.tenant_id
WHERE t.clerk_org_id = $1
  AND el.event_type LIKE 'kora.%'
ORDER BY el.occurred_at DESC
LIMIT $2
-- read_active_constitution_revision: no superseded_at column
SELECT revision_id::text, rules_hash
FROM kronicle.workspace_constitution_revisions
WHERE workspace_id = $1
ORDER BY revision_number DESC
LIMIT 1

_prefetch_all now gathers 7 reads in one asyncio.gather

Single round-trip latency for the full session-warm. Caches populated:

  • _charter_cache (RoleCharter)
  • _policy_cache (List[PolicyRegistryEntry])
  • _capability_cache (KoraCapabilityRow)
  • _own_scratchpad_cache (List[ScratchpadEntry])
  • _cross_agent_scratchpad_cache (List[ScratchpadEntry])
  • _events_cache (List[RecentChainEvent])
  • _constitution_cache (Optional[(revision_id, rules_hash_hex)])

Test plan

23 new tests, all passing:

test_events.py (9):

  • Typed shape from happy path
  • Default LIMIT 50 + custom limit binding
  • SQL JOINs tenant on clerk_org_id + filters LIKE 'kora.%'
  • Payload truncation at exactly 300 chars + ellipsis
  • Short payload pass-through unchanged
  • Empty payload handled
  • Datetime occurred_at ISO-encoded
  • Deferred emit raises ChainEventEmitNotAvailableError with tag + deviation ID

test_constitution.py (8):

  • Typed shape with hex-encoded rules_hash
  • Returns None for fresh-workspace bootstrap
  • RLS-GUC call ordering (txn.enterexecute(set_config)fetchrowtxn.exit)
  • SQL uses revision_number DESC (NOT superseded_at)
  • Hex encoder accepts bytes, bytearray, memoryview, hex str pass-through
  • Hex encoder rejects unknown types with clear error

test_session_context.py (3):

  • All 6 reads fire; typed shape with all fields populated
  • None Constitution revision pair on fresh workspace
  • Default limits (DEFAULT_SCRATCHPAD_LIMIT=100, DEFAULT_RECENT_EVENT_LIMIT=50)

test_provider_end_to_end.py (2):

  • Full lifecycle walkinitializeon_turn_start (warms 7 caches) → system_prompt_block (uses warm cache; all 6 sections present + Rule-6 verbatim + recent event_types in §6) → session_context() returns typed shape with constitution hex → sync_turn (cap_* token → attempt write → catch defer → log) → on_memory_write (mirrors to scratchpad) → on_delegation (scratchpad + emit, both deferred + logged) → on_pre_compress (returns "") → on_session_switch (no-reset keeps caches) → on_session_end (emit kora.session.ended deferred) → on_session_switch(reset=True) (flushes 7 caches) → shutdown. Zero NotImplementedErrors surface. 5+ deferred-write/emit WARNINGs logged.
  • session_context() returns None when caches are cold (caller must call on_turn_start first)

Plus existing 64 isokron tests + parametrize-cross-product still passing.

Gates

  • ty check7,337 diagnostics, same as ST3 baseline (Δ 0). Zero new diagnostics introduced.
  • pytest tests/plugins/memory/248/248 passing (23 new ST4 + 64 pre-ST4 isokron + 161 other memory providers).
  • Full suite via xdist (-n auto): 24,570 passed / 143 failed / 129 skipped. Δ vs ST3 merge baseline (24,551/143/129): +19 passed, ±0 failed. Same tests/tools/* + tests/tui_gateway/* xdist isolation noise as ST2/ST3; none touch plugins/memory/isokron/.

Rule-6 / BUILD_DEVIATIONS / Open asks

Three deferred-MCP-tool surfaces still open (all gated on substrate-team dispatches; same pattern, same closure shape):

Deviation Surface Closes when
D-kr2-st2-capability-matrix-mirror C2 Python mirror of ACTOR_CAPABILITY_MATRIX Kora column K-7 ships Sea MCP kora__read_kora_capability_row
D-kr2-st3-no-scratchpad-write-mcp-tool scratchpad writes deferred K-8 ships Sea MCP kora__write_agent_scratchpad
D-kr2-st4-no-chain-emit-mcp-tool chain event emit deferred A K-9-equivalent ships Sea MCP kora__append_event

All three follow the same shape: signature is forward-stable, body swaps from raise <DeferredError> to mcp_client.invoke('kora__*', ...). The follow-on KR-N buckets on this lane are size O(60 LOC each) once the substrate tools land. PM coordinates the K-9 substrate dispatch.

KR-2 milestone closes here. The IsoKronMemoryProvider has:

  • Full read coverage of the 6 load-bearing substrate surfaces
  • A typed KoraSessionContext shape ready for K-6 (Constitution pre-screen middleware) to consume
  • Honest deferred-write surfaces for the 3 substrate-side MCP tools we're waiting on
  • ~/.kora/config.yaml memory.provider: isokron is enable-able in production with the known caveats documented in README "Operator pitfalls"

Standing by for KR-3 (beads-pattern consumer wiring) dispatch.

🤖 Generated with Claude Code

…s KR-2

ST4 wraps KR-2. Recent chain events read, active Constitution revision
read, KoraSessionContext shape + assembler, system prompt block §6,
and removal of every remaining NotImplementedError stub. Chain event
emit is deferred behind ChainEventEmitNotAvailableError until the Sea
MCP tool ships (same pattern as the ST3 scratchpad write defer).

Read paths (new):
* events.py:read_recent_kora_events — direct asyncpg against
  hivex_foundation.event_log, the substrate's one genuine
  tenant_id-UUID-keyed table. JOINs hivex_foundation.tenant on
  clerk_org_id to resolve the workspace_id (Clerk TEXT) → tenant_id.
  Filters event_type LIKE 'kora.%'. jsonb_pretty(payload) truncated to
  300 chars + ellipsis to match the TS-side KoraRecentChainEvent shape.
  Default LIMIT 50 per spec.
* constitution.py:read_active_constitution_revision — asyncpg against
  kronicle.workspace_constitution_revisions. RLS-GUC-in-transaction
  (same pattern as policy_registry / scratchpad). No superseded_at
  column on the table — active = ORDER BY revision_number DESC
  LIMIT 1. rules_hash BYTEA hex-encoded to match the TS-side
  constitutionVersionHashFromRulesHash helper so K-6 (Constitution
  pre-screen middleware) sees the same canonical hex form. Returns
  None for fresh-workspace bootstrap state.

Session context (new):
* session_context.py:assemble_session_context — fans out the six
  load-bearing reads via asyncio.gather. Mirrors TS-side
  KoraSessionContext shape at
  packages/sea-mcp-server/src/kora/context-assembler/types.ts:130.
* KoraSessionContext dataclass: workspace_id, assembled_at,
  role_charter, capability_matrix_row, own_scratchpad,
  cross_agent_scratchpad, recent_chain_events,
  active_constitution_revision_id, active_constitution_rules_hash.
  Tuple-typed read collections — immutable end-to-end like TS side.

Deferred chain event emit:
* events.py:emit_kora_event raises ChainEventEmitNotAvailableError
  with [kora.isokron.todo] tag + D-kr2-st4-no-chain-emit-mcp-tool.
  Signature matches the future MCP-backed impl so caller code stays
  stable when the substrate tool lands. Direct INSERT into event_log
  forbidden — would skip _emit_chain_event SECDEF and break
  prev_event_hash / this_event_hash witness chain.

Provider finalize:
* on_turn_start now gathers 7 reads in one shot (the original 3 from
  ST2 + the 2 scratchpad reads from ST3 + the 2 ST4 reads). Single
  asyncio.gather per prefetch for one round-trip latency.
* IsoKronMemoryProvider.session_context() returns the typed
  KoraSessionContext from warm caches (or None when caches are cold).
* system_prompt_block extended with §6 "Recent kora.* activity"
  surfacing the last 5 events as readable bullets; cap is bounded
  to keep the prompt size sane.
* All remaining ABC stubs replaced with real implementations:
  - on_session_end emits kora.session.ended (deferred + caught)
  - on_session_switch updates session_id; reset=True flushes all 7
    caches so the next turn re-reads against fresh substrate
  - on_pre_compress returns "" (substrate is the system of record;
    nothing isokron-side to inject into compression summaries)
  - on_delegation mirrors as scratchpad reasoning_trail entry +
    emits kora.handoff.to_claude_pm (both deferred + caught)
  - save_config no-op (env-var-based config; no native YAML file)
  - handle_tool_call inherits ABC default (provider has no tools
    until KR-3 ships iso_node_* / iso_link_*)
* _attempt_chain_event_emit helper mirrors
  _attempt_scratchpad_write's pattern: submit-and-wait, catch the
  defer-error, log a one-line WARNING tagged with the deviation ID.
  Successful emits invalidate the events cache.

Capability mirror parity fix:
* The C2 mirror was 1 cap stale — Sea MCP capability-matrix.ts has
  added cap_unbless_convention (operator-only, kora: false) since
  KR-2 ST2 shipped. The parity test fired in this PR's run and
  caught the drift in both directions. Mirror + counts updated to
  match: SEA 24 + KORA_BROADER 25 = 49 total; Kora granted=22,
  denied=27. Parity test now passes against current substrate main
  (28ff4f7). This is the exact win the C2 parity guard was built
  for; closing D-kr2-st2 still gated on K-7 substrate dispatch.

Tests (23 new):
* test_events.py (9): typed shape, default limit 50, custom limit
  binding, SQL JOINs tenant on clerk_org_id + LIKE filter, payload
  truncation at 300 chars + ellipsis, short payload pass-through,
  empty payload, datetime occurred_at ISO encoding, deferred emit
  carries the tag.
* test_constitution.py (8): typed shape with hex-encoded rules_hash,
  None for fresh workspace, RLS-GUC ordering, SQL uses
  revision_number DESC (NOT superseded_at), hex encoder accepts
  bytes / bytearray / memoryview / str / rejects unknown types.
* test_session_context.py (3): fans out all 6 reads, None pair on
  no Constitution row, default limits.
* test_provider_end_to_end.py (2): full lifecycle walk
  (initialize → on_turn_start → system_prompt_block → sync_turn
  → on_memory_write → on_delegation → on_pre_compress →
  on_session_switch → on_session_end → reset → shutdown). Asserts
  no NotImplementedError surfaces anywhere; deferred-emit / deferred-
  scratchpad warnings logged but caught. Cold-cache session_context
  returns None.

ST1 skeleton test updated: parametrize replaced with
test_no_stub_methods_remain_after_st4 verifying each ABC method
returns normally; test_handle_tool_call_unsupported_tool_raises_
with_provider_name verifies the ABC's clear-error path is preserved.

Local gates:
* ty check — 7,337 diagnostics, same as ST3 baseline (zero-delta).
* pytest tests/plugins/memory/ — 248/248 passing (23 new ST4 + 64
  pre-ST4 isokron + 161 other memory providers).
* Full suite via xdist (-n auto): 24,570 passed / 143 failed / 129
  skipped. Δ vs ST3 merge baseline (24,551/143/129): +19 passed,
  ±0 failed. Failures still concentrated in tests/tools/* +
  tests/tui_gateway/* xdist isolation noise; none touch
  plugins/memory/isokron/.

Rule-6:
* BUILD_DEVIATIONS.md gains D-kr2-st4-no-chain-emit-mcp-tool under
  Open with grep snapshot of available kora__* tools (still 3) and
  exact closure condition.
* README "Operator pitfalls" gains 3 entries: chain emit deferred
  + tag to grep, event_log tenant_id-keying exception, Constitution
  revisions table has no superseded_at column.
* All [kora.isokron.todo] tags retained on deferred surfaces.

KR-2 milestone closed. Standing by for KR-3 dispatch + K-7 + K-8
+ K-9 (or equivalent) substrate dispatches to swap the three C2 /
deferred surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant