Skip to content

feat(panic): Panic Response Layer — behavioral destabilization detection#83

Draft
laurentftech wants to merge 22 commits into
clay-good:mainfrom
laurentftech:feat/panic-response-layer
Draft

feat(panic): Panic Response Layer — behavioral destabilization detection#83
laurentftech wants to merge 22 commits into
clay-good:mainfrom
laurentftech:feat/panic-response-layer

Conversation

@laurentftech
Copy link
Copy Markdown
Contributor

@laurentftech laurentftech commented May 19, 2026

Summary

Full implementation of the Panic Response Layer — behavioral destabilization detection orthogonal to EpistemicLease (freshness ≠ panic). The core insight: observe behavioral collapse, not activity level.

Freshness vs Panic — explicit separation

Freshness models epistemic authority decay.
Panic models behavioral destabilization.
Neither implies the other.

An agent can be stale but calm (deep linear work in stale context), or fresh but panicking (rapid confused navigation after a recent orient()). The two dimensions are computed independently; only the panic ceiling (staleDepth floors panicLevel) couples them.


Core engine (panic-response.ts)

  • PanicLevel L0–L4 with hysteresis (separate up/down thresholds — prevents thrashing)
  • Panic ceiling: staleDepth≥2 floors panicLevel≥1; staleDepth=3 floors panicLevel≥2
  • Atomic panic-state.json writes (POSIX rename(2)), fail-open reads, 30min session expiry
  • buildPanicCheckOutput(): cooldown enforcement, directive mode at interventionCountSinceStable≥3
  • revision: number in PanicState — monotonically bumped on every write, enables CAS
  • gryphWindowStart?: string — tracks Gryph query window for the panic-check hook path independently from lastOrientAt (which could be hours old). Advanced on each intervention write; 2-min fallback prevents replaying hours of history.

Signal model (epistemic-lease.ts)

Per-call behavioral signals:

Signal Weight
Trajectory burst (density ≥ 0.60) +15
Oscillation spike (osc ≥ 0.50) +10
Stale depth 3 persistence +25/call
Passive wall-clock decay −5/min
Locality recovery −3/call when stable

localityConfidence is shared behavioral state — used by both the freshness engine (burst gate) and the panic engine (stale_depth_3 gate, burst escalation gate). Computed in updateTracker() so it's always current regardless of panic mode. Changes to this metric affect both systems — modify with full blast-radius awareness:

  • stale_depth_3 and burst escalation only fire when localityConfidence < 0.5
  • Formula: (1 − min(1, density×2)) × (1 − min(1, oscillation))

Refractory period: orient() sets panicRecoverySuppressionUntil (+45s) when score reduces — suppresses upward signals to let recovery land before re-escalating. Subsequent orient() calls during an active refractory replace the deadline (not extend): the window starts fresh from the most recent recovery. Spam orient (delta=0) does not set refractory.

Provenance trace: every panic_score_delta event includes a provenance[] array with per-trigger {name, delta, evidence} entries.

commandEntropy is normalized Shannon entropy over recent shell command signatures:

H = −Σ p(cmd)·log₂(p(cmd))  normalized to [0,1]
Low entropy = retry loop. High entropy = exploratory burst.

The signal is behavioral collapse, not throughput. Raw tool frequency is never a panic signal.

Orient spam protection: −40 normal / −15 rapid (<2min) / 0 spam (≥3 rapid). interventionCountSinceStable resets on stable recovery, orient reset, or 30min session expiry.

Oscillation model fix

computeOscillationScore() now operates over the transition sequence (entries where module actually changed), not the full window. Previous formula scored A→A→A→A as 1.0 (same as confusion loop). Focused single-module work now correctly produces oscillation=0.

Gryph runtime observability (gryph-bridge.ts + gryph-watch.ts)

The MCP blind spot: panic scoring only fires on openlore MCP tool calls. Agents working exclusively via Bash/Edit/Read are invisible to the MCP-path signal model — they can enter behavioral collapse with no panic signal generated. Worse: MCP-path Gryph polling only starts after the first tool call, so sessions that never call openlore tools have zero observability.

Closed by openlore gryph-watch — a standalone observer process whose lifetime is fully decoupled from the MCP server session:

openlore gryph-watch (independent process)
  └── startGryphPolling → while loop → CAS writes to panic-state.json
      └── MCP server reads panic-state.json on each tool call

gryph-watch lifecycle:

  • Installed as a UserPromptSubmit hook via openlore setup --hooks — starts on the first user prompt, not the first tool call
  • Singleton via PID file (.openlore/gryph-watch.pid) — one per workspace directory
  • Auto-detects project directory by walking up from CWD
  • Exits on SIGTERM/SIGINT or stdin EOF (parent death detection)
  • Silent exit when mode: 'off'

Polling invariants:

  • while (!stopped) loop replaces setInterval — eliminates timer drift, stop lifecycle races, orphan timers; sequential await makes overlap structurally impossible
  • Single-flight isPolling flag kept as defense-in-depth
  • Module-level _pollerRegistry: Map<string, () => void> enforces one-poller-per-workspace at the call site
  • Async spawn (non-blocking) — isolated from MCP execution path
  • All failures degrade to null — fail-open, never throws

CAS writes prevent multi-writer score regression:

Both gryph-watch and the MCP server write to panic-state.json. Without concurrency control, a slow Gryph query that reads stale state can overwrite the MCP server's fresher write (silent score regression).

  • casWritePanicState(directory, expectedRevision, state): reads current revision, checks against expected, writes with revision+1 — all synchronous, atomic within the Node.js event loop (no await between read and write)
  • Gryph poll: CAS write → on conflict, re-reads fresh state and recomputes delta → retries once → gives up after 2 failures (tries again next interval)
  • MCP path: writePanicState() returns the new revision; synced into tracker.panicRevision so subsequent MCP writes never regress the revision
  • panicRevision added to EpistemicTracker and trackerToPanicState()

Gryph signals + decay:

Signal Condition Weight
repetitiveRetryBurst low commandEntropy + failing commands +15
largePatchWhileStale (>500 LOC write, low entropy) staleDepth≥2 (MCP-path only) +30
largePatchWhileStale (>500 LOC write, high entropy) staleDepth≥2, deliberate refactor +10
Passive wall-clock decay elapsed since state.updatedAt −5/min

Decay is applied inside applySnapshotDelta() before Gryph signal deltas so score can drop between polls when the agent is idle — not just via the MCP path.

OPENLORE_GRYPH_TIMEOUT_MS — per-query budget (default 150ms, min 50ms).
OPENLORE_GRYPH_POLL_INTERVAL_MS — poll interval override (default 15s, min floor 5s).

Centralized constants (panic-constants.ts)

All numeric thresholds, weights, cooldowns, and timing values live in panic-constants.ts as the single source of truth. Tests import and reference these constants directly — no hardcoded magic numbers that silently diverge when thresholds change.

Dual-channel intervention

  • MCP channel: panic signal injected into every tool response when panicLevel≥1 (advisory/experimental_blocking modes only)
  • PreToolUse hook: openlore panic-check fires before every tool call — closes the tunneling blind spot (agent stops calling openlore → MCP signals can't reach it)
  • Gryph background channel: openlore gryph-watch observes continuously, session-lifetime independent
  • Anti-wallpaper: advisory→directive mode at interventionCountSinceStable≥3
  • L4 enforcement model: experimental_blocking emits {"decision":"block","advisory":true} at L4 and exits 0 — advisory:true is always present; runtime decides enforcement. OpenLore never mandates.

Cooldown preservation

mcp.ts reads disk state before each writePanicState() call to preserve lastHookInterventionAt and gryphWindowStart set by the panic-check process (separate OS process). Previously these fields were silently wiped on every MCP tool call, breaking cooldown enforcement.

Intervention feedback loop

mcp.ts detects orient() calls (via tracker.lastOrientResetAt change) and emits panic_intervention_outcome when orient followed a hook intervention within 5 minutes. Enables retrospective validation of whether interventions actually produced behavioral change from telemetry.

Opt-in panic response (PanicResponseMode)

Panic is disabled by default. Users opt in via .openlore/config.json:

{ "panicResponse": { "mode": "observe" } }
Mode Panic scoring State file Response injection Hook output gryph-watch
off (default) silent exit 0 silent exit 0
observe silent exit 0
advisory L2+ warning text advisory JSON
experimental_blocking L2+ warning text {"decision":"block","advisory":true} at L4

mode:'off' disables: panic scoring, panic state persistence, panic interventions, panic telemetry, panic hook output, Gryph observation. Behavioral metrics required by the freshness engine (density, oscillation, localityConfidence) continue to be computed in-memory as part of EpistemicLease.

Policy is never stored in EpistemicTracker or panic-state.json — config is the single source of truth. panic-check always exits 0 (fail-open). setup --hooks and setup --panic <mode> are independent commands.

Architecture: policy / state separation

  • updatePanic() extracted from updateTracker() and exported — mcp.ts calls it conditionally based on panic mode
  • localityConfidence computed in updateTracker() — shared behavioral state reused by freshness and panic; changes affect both systems
  • tracker.density and tracker.panicRevision stored after each call so callers can read/sync them post-updateTracker()
  • EpistemicTracker contains behavioral/freshness state only — no policy fields
  • updateTracker() always runs; only updatePanic() + writePanicState() are gated on mode !== 'off'
  • Gryph observation runs as a separate process (gryph-watch), not inside the MCP server

Status line integration

openlore panic-level — read-only command outputting P:L{n} (empty at L0) for status line displays. No side effects, no writes.

{ "statusLine": { "type": "command", "command": "openlore panic-level --directory $(pwd) 2>/dev/null" } }

Trajectory tracking during stale

Trajectory density and oscillation continue accumulating while stale so post-stale burst and behavioral patterns remain observable. Stale state does not freeze the behavioral model.

Runtime safety invariants

- panic-check MUST fail open (always exits 0, on all code paths including errors)
- Gryph absence MUST have zero behavioral impact
- gryph-watch MUST be singleton per workspace (PID file guard)
- gryph-watch poll loop MUST NOT overlap (while loop + isPolling flag)
- CAS writes MUST be atomic within Node.js event loop (no await between read and write)
- Telemetry failure MUST NOT affect tool execution
- panic-state.json corruption MUST resolve to stable state
- Hook execution failure MUST NOT block MCP flow
- panic-check and telemetry CLI commands never call updateTracker — no recursive loop
- mode:'off' disables panic subsystem; freshness metrics continue in-memory
- refractory deadline is replaced (not extended) by subsequent orient() calls
- experimental_blocking payload always carries advisory:true — runtime decides enforcement
- revision monotonically increases across all writers (MCP + gryph-watch)
- lastHookInterventionAt and gryphWindowStart preserved across MCP writes (read-merge-write)

Telemetry + observability

  • panic.jsonl domain: panic_level_change, panic_orient_reset, hook_intervention, panic_signal_injected, panic_score_delta (with provenance), gryph_poll, panic_intervention_outcome
  • panic_score_delta events carry source: 'gryph' when emitted from gryph-watch; provenance entries include evidence: { source: 'gryph' }
  • telemetry.ts rotation: 50MB threshold, keep 5 rotated files
  • openlore telemetry panic section: episodes, avg recovery latency, failed recovery rate, hook intercepts, MCP injections, orient spam, Gryph enrichment rate, trigger frequency

Setup integration

  • openlore setup --hooks claude|kilo|codex — installs both PreToolUse panic-check hook and UserPromptSubmit gryph-watch hook
  • openlore setup --panic <mode> — sets panicResponse.mode in .openlore/config.json; explicit downgrade always allowed
  • Both flags work non-interactively (no TTY required when --hooks or --panic used without --tools)

Test plan

  • panic-response.test.ts: 29 tests — hysteresis, state I/O, fail-open, buildPanicCheckOutput, getPanicSignalText
  • epistemic-lease.test.ts: 86 tests — score accumulation, ceiling, orient spam, signal detection, refractory, localityConfidence formula, burst escalation gate, trackerToPanicState
  • telemetry.test.ts: 24 tests — computePanicStats, computeRecovery, computeObstinacy with synthetic JSONL events
  • gryph-bridge.test.ts: 19 tests — single-flight, stop cleanup, null stability, no-signal no-write, retry burst score update, large patch stale gating, large patch NOT stale (no delta), provenance source:gryph, exception fail-open, null tracker still writes file, score accumulation across polls (decay-aware)
  • TypeScript build clean (0 errors)
  • 2821/2821 tests pass

🤖 Generated with Claude Code

laurentftech and others added 22 commits May 19, 2026 22:04
Behavioral destabilization detection orthogonal to EpistemicLease.
Panic = oscillation + trajectory bursts + stale-depth persistence.
EpistemicLease = freshness/authority decay. Neither implies the other.

Architecture:
- panic-response.ts: PanicState I/O (atomic writes), hysteresis engine
  (separate up/down thresholds L0-L4), buildPanicCheckOutput, signal text
- epistemic-lease.ts: EpistemicTracker extended with panicScore, panicLevel,
  localityConfidence, recentOrientCount, interventionCountSinceStable;
  updatePanic() called on every tool call; orient spam protection in
  resetTracker() (-40 normal / -15 rapid / 0 at 3+ rapid orients);
  trackerToPanicState() export
- mcp.ts: writes panic-state.json after every updateTracker; appends panic
  signal as separate content item (never corrupts JSON result body);
  increments interventionCountSinceStable on injection
- panic-check.ts: new CLI command for PreToolUse hook consumers; reads
  panic-state.json, applies cooldown, always exits 0
- index.ts: registers panic-check command

Telemetry (panic.jsonl, gated by OPENLORE_TELEMETRY=1):
- panic_level_change: every L0-L4 transition with trigger (score/ceiling)
- panic_orient_reset: orient kind (normal/rapid/spam), delta, from/to level
- panic_signal_injected: tool, agent, directive_mode flag
- hook_intervention: channel, severity, directive_mode
- panic_level + panic_score added to every mcp tool_call event

Tests: 101 passing (26 new in panic-response.test.ts, 26 new in
epistemic-lease.test.ts covering hysteresis, state I/O, fail-open,
cooldowns, directive mode, orient spam protection, trackerToPanicState)
connect() + openTable() now called once per dbPath, not every search.
BM25 full table scan eliminated on cache hit — cached rows reused.
Both caches invalidated by build() when index is rebuilt.
- gryph-bridge: optional CLI bridge for safedep/gryph observability;
  queryGryphSignals() + applyGryphDelta() with fail-open absence semantics
- epistemic-lease: replace proxy signals with spec-correct panic weights:
  trajectory burst (density≥0.60→+15), oscillation spike (osc≥0.50→+10),
  staleDepth≥3→+25 per call; add passive decay (−5/min wall-clock) and
  locality recovery (−3/call when stable); add lastPanicUpdateAt and
  panicTriggers fields; expose triggers via trackerToPanicState()
- panic-check: add --format claude|kilo|codex; query Gryph and apply delta
  before building hook output; emit gryph_enriched in telemetry
- telemetry: add 50MB rotation (keep 5 rotated files); add panic section
  to telemetry command (episodes, avg recovery latency, hook intercepts,
  orient spam, Gryph enrichment rate, trigger frequency)
- setup: add --hooks claude|kilo|codex to install PreToolUse panic-check
  hook into .claude/settings.json independently of --tools
- gryph-bridge: rename largePatchWhileActive → largePatchWhileStale; add
  OPENLORE_GRYPH_TIMEOUT_MS env var (default 150ms, min 50ms)
- mcp.ts: add invariant comment excluding panic-check/telemetry from panic
  computation loop (CLI procs read state, never call updateTracker)
- spec: add Runtime Safety Invariants section (fail-open guarantees for all
  subsystems); clarify L4 advisory-only enforcement model; add Shannon entropy
  formula for commandEntropy; add locality recovery philosophical framing;
  document trajectory tracking continues during stale; document depth-3
  saturation intent; add OPENLORE_GRYPH_TIMEOUT_MS to Gryph config table;
  add interventionCountSinceStable reset conditions to formal invariants
Refractory period:
- Add panicRecoverySuppressionUntil to EpistemicTracker and PanicState
- orient() sets 45s suppression window on any score-reducing call
- Upward signals (trajectory_burst, oscillation_spike, stale_depth_3) skip
  during refractory; decay and locality recovery still apply
- Written to state file so hook can apply guard without MCP round-trip

Provenance trace:
- updatePanic() now emits panic_score_delta on every score change with full
  per-trigger attribution: name, delta, evidence (measured values)
- Separates trigger labels from evidence so threshold tuning is possible
- in_refractory flag on events where suppression was active
- panic_level_change events include full provenance array
- tool name propagated from updateTracker to updatePanic for attribution

Spec:
- Document refractory period rationale and semantics
- Add panic provenance trace format with trigger/evidence separation
- Known Limitations section: oscillation fragility, productive chaos,
  goal coherence absence, hook non-mandatory invariant
- V2 non-goals: convergence signals, productive refactor mode detection,
  goal coherence / task scope tracking
…tion

localityConfidence now uses both density and oscillation (not just density):
  localityConfidence = (1 - min(1, density×2)) × (1 - min(1, oscillation))

Locality gates two signals:
- stale_depth_3 (+25/call): only fires when localityConfidence < 0.5
- burst escalation (depth → 3): only fires when localityConfidence < 0.5

A stale agent doing focused local work (high confidence) is not in the same
risk category as a stale agent drifting cross-module. Suppressing these signals
prevents the panic layer from treating coherent deep work as a crisis.

Spec: add Behavioral Space section documenting the five independent dimensions
and the interpretation matrix (8 situations). Add Locality Confidence Modulation
subsection with formula, gating table, and rationale for appropriate vs maximum
tool utilization.
Export computePanicStats, computeRecovery, computeObstinacy, and their
types for direct testing.

24 tests covering:
- computePanicStats: episode counting, avg_recovery_ms (completed only),
  failed_recovery_rate, hook_intercepts, mcp_injections, orient spam/rapid,
  gryph_enriched_intercepts, trigger frequency + sort order,
  out-of-order chronological events, non-level-change event isolation
- computeRecovery: stale→orient latency avg, multi-pair avg, null when no
  subsequent orient, recovery half-life, recurrence rate, edge cases
- computeObstinacy: tool call counting per episode, depth tracking,
  open episode handling, multiple episode separation
… tests

Also fix computeOscillationScore to operate over transition sequence —
A→A→A→A was scoring 1.0 (same as A→B→A→B confusion loop) because the
old formula checked bigrams across the full window. Focused single-module
work should produce oscillation=0.
Add PanicResponseMode ('off'|'telemetry'|'advisory'|'experimental_blocking')
to OpenLoreConfig. Default: 'off'. Existing users unaffected.

Architecture:
- Policy lives in mcp.ts only — never stored in EpistemicTracker or panic-state.json
- updatePanic() extracted from updateTracker() and exported; mcp.ts calls it
  conditionally based on policy mode
- localityConfidence moved into updateTracker() — it's navigation state, not panic
- tracker.density stored after each updateTracker() so mcp.ts can pass it to updatePanic()

Mode semantics:
- off: zero panic overhead (no scoring, no file writes, no Gryph)
- telemetry: scoring + state file, no agent impact
- advisory: + injection into responses at L2+, hook exits 0 always
- experimental_blocking: + hook emits {"decision":"block"} at L4, still exits 0

panic-check always exits 0 — fail-open invariant. Runtime decides enforcement.
setup --panic <mode> sets config; --hooks remains independent.
Separates instrumentation from policy. 'off' already disabled
persistence and intervention; 'privacy' cuts deeper — skips
updateTracker() entirely so no behavioral profiling occurs in memory.

Mode ladder (bottom to top):
  privacy             → no instrumentation, no persistence, no intervention
  off (default)       → in-memory tracking only
  telemetry           → tracking + persistence
  advisory            → tracking + persistence + response injection
  experimental_blocking → + block signal to hook runtime
'off' now means zero instrumentation (updateTracker skipped).
Removed the intermediate 'off' state that ran tracking without
consumers — it was computationally dead. Inner updatePanic gate
drops; anything past 'off' runs the full pipeline.

Final ladder: off | telemetry | advisory | experimental_blocking
Restores updateTracker() to always run — freshness/epistemic tracking
is not panic instrumentation. 'off' disables the panic subsystem only;
the epistemic engine remains active regardless of panic mode.

Renames 'telemetry' → 'observe': the mode observes the panic engine
without intervening, not just moves telemetry data.

Final ladder: off | observe | advisory | experimental_blocking
…emantics

localityConfidence is shared behavioral state (freshness + panic),
not purely freshness state. Refractory deadline is replaced on each
orient() recovery, not extended — document both explicitly in-source.
…lock advisory

- localityConfidence interface gets explicit WARNING: affects both
  freshness and panic — blast-radius comment locks the shared contract
- experimental_blocking payload adds advisory:true — protocol now
  matches the documented "runtime decides" semantics; no implicit authority
- OpenLoreConfig docstring enumerates exactly what 'off' disables vs.
  what continues (freshness metrics) — eliminates the apparent contradiction
Single source of truth for all numeric thresholds, weights, cooldowns,
and timing values. Both panic-response.ts and epistemic-lease.ts now
import from it. Tests reference constants directly — no more behavioral
snapshot drift when a threshold changes.

New exports: PANIC_UP/DOWN_THRESHOLD, PANIC_TRAJECTORY_DENSITY/DELTA,
PANIC_OSCILLATION_THRESHOLD/DELTA, PANIC_STALE_D3_LOCALITY_GATE/DELTA,
PANIC_LOCALITY_RECOVERY, PANIC_DECAY_PER_MIN, PANIC_REFRACTORY_MS,
PANIC_SESSION_EXPIRY_MS, HOOK_COOLDOWN_MS, SEVERITY_MAP.
…MCP blind spot

Promotes Gryph from optional enrichment to first-class behavioral source.
Background poll loop (default 15s) updates panic state independently of
MCP tool calls: agents working purely via Bash/Edit/Read are now observable.

Architecture:
  RuntimeBehaviorProvider (interface) + GryphBehaviorProvider (impl)
  startGryphPolling: async, single-flight, syncs in-memory tracker to
  prevent MCP path from overwriting Gryph-elevated scores on next call.

New constants: GRYPH_POLL_INTERVAL_MS, GRYPH_POLL_INTERVAL_MIN_MS,
GRYPH_RETRY_BURST_DELTA, GRYPH_LARGE_PATCH_*_DELTA, GRYPH_*_THRESHOLD.
New env: OPENLORE_GRYPH_POLL_INTERVAL_MS (min 5000ms, default 15000ms).

Provenance carries source:'gryph' on all Gryph-originated deltas.
Backward-compat: queryGryphSignals/applyGryphDelta preserved for hook path.
Fail-open invariant: all Gryph failures resolve to null, zero impact.

19 new tests: single-flight, timeout, null stability, score accumulation,
stale gating, provenance attribution, tracker sync, exception safety.
Three architectural fixes from post-Gryph review:

Gryph polling decoupled from MCP session (option B):
- New `openlore gryph-watch` command: standalone observer process, lifetime
  independent of MCP server. Singleton via PID file, auto-detects project dir,
  exits on SIGTERM/SIGINT/stdin-close (parent death).
- Installed via `openlore setup --hooks claude` as a UserPromptSubmit hook —
  starts once per session from the first user prompt, not the first tool call.
- Closes the blind spot permanently: Gryph signals flow even when no openlore
  MCP tools are called during the entire session.
- Removed Gryph polling from mcp.ts (was MCP-session-scoped, too late to start).

CAS writes prevent multi-writer score regression:
- `revision: number` added to PanicState — monotonically bumped on every write.
- `casWritePanicState()`: synchronous read-check-write (atomic within Node.js
  event loop, no await between ops). Returns false on revision mismatch.
- Gryph poll uses CAS with one retry: re-reads fresh state and recomputes delta
  if MCP wrote between poll read and poll write.
- MCP path uses `writePanicState()` (returns new revision) and syncs
  `tracker.panicRevision` — prevents MCP from writing stale revision on next call.
- `panicRevision` added to EpistemicTracker and `trackerToPanicState()`.

setInterval replaced with while loop:
- `while (!stopped) { await sleep(intervalMs); if (!stopped) await poll(); }`
- Eliminates timer drift, stop lifecycle races, and orphan timer edge cases.
- Sequential await guarantees no overlap (isPolling guard kept as defense-in-depth).

Workspace registry in startGryphPolling:
- Module-level `_pollerRegistry: Map<string, () => void>` keyed by directory.
- `startGryphPolling` stops any existing poller for the same directory before
  starting a new one — enforces one-per-workspace at the call site.
Hook environments (nohup, UserPromptSubmit) often strip PATH so `which gryph`
fails even when the binary exists at ~/.local/bin/gryph. Falls back to checking
~/.local/bin, ~/go/bin, /usr/local/bin, /opt/homebrew/bin before giving up.
Stores resolved path in _gryphBin so spawns use the absolute path, not 'gryph'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…chema

Gryph events use PascalCase (Command, ExitCode, ResultStatus, LinesAdded) but
code mapped lowercase fields — commands array was always empty, entropy always 1,
repetitiveRetryBurst never fired.

Two fixes:
1. Interface and mappings now use PascalCase with lowercase fallbacks
2. Burst detection adds OR path: failingCommandRate > 0.30 triggers regardless
   of entropy — catches mixed-window scenarios where diagnostic commands dilute
   the entropy signal below the 0.30 threshold

Verified end-to-end: panic_score_delta events firing with correct provenance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efix

panic_mode_active was written to panic-response.jsonl which telemetry
never reads — silent data loss. Mode is always derivable from config.

kilo format was prepending [PANIC:SEVERITY] before a message that
already contained its own tag, producing double-prefix output.
…on feedback loop, panic-level statusline

1. Decay in Gryph path — applySnapshotDelta now applies passive
   wall-clock decay (PANIC_DECAY_PER_MIN) before adding Gryph signals.
   Score could previously only go up via Gryph while MCP was idle.

2. gryphWindowStart — new PanicState field separates the Gryph query
   window from lastOrientAt (which could be hours old). panic-check
   hook now queries only the ~2min window since last intervention,
   eliminating replaying hours of history. Advanced on each warn write.

3. Cooldown preservation — mcp.ts now reads disk state before writing
   to preserve lastHookInterventionAt and gryphWindowStart set by the
   panic-check process. Fixes broken cooldown: these fields were
   silently wiped on every MCP tool call.

4. Intervention feedback loop — mcp.ts detects orient() calls and
   emits panic_intervention_outcome event when orient followed a hook
   intervention within 5 minutes. Closes the measurement gap on
   whether interventions actually produce behavioral change.

5. panic-level command — new read-only `openlore panic-level` outputs
   "P:L{n}" (empty at L0) for status line integration. No side effects.
   Configure: openlore setup installs hooks separately.
laurentftech pushed a commit to laurentftech/OpenLore that referenced this pull request Jun 3, 2026
…mode perf)

Field regression: multiple dogfooding sessions report batched/delayed
tool-result delivery once the openlore MCP server is registered. Root
cause traced to the watch-mode re-index pipeline:

  - --watch-auto defaults to true (mcp.ts:1610), so plain `openlore mcp`
    silently arms a watcher on the first tool call.
  - Every save runs an O(repo), not O(change), pipeline: full 2.1MB
    llm-context.json rewrite (which then forces a cold re-parse on the
    next tool call via the mtime-keyed read cache), plus a full
    vector-index read+overwrite ("incremental" build still toArray()s
    and createTable(overwrite)s the whole corpus).
  - No cross-file coalescing, so a branch switch/formatter fires the
    pipeline N times back-to-back; one stderr line per change floods the
    client's drain.

Spec 13.1 keeps --watch-auto on by default but makes freshness genuinely
O(change): batch coalescing, write-behind + read-cache handoff, real
incremental LanceDB row ops, signature-freshness decoupled from
embedding-freshness, VCS-flood detection + backpressure, stderr
discipline, doc reconciliation, and a watch-mode benchmark + tests.

Slotted ahead of spec 14 in spec 13's Progress list (urgent regression
fix; pollutes any benchmark run through the MCP server). Not addressed
by PR clay-good#83 (Panic Response Layer) — independent pipeline.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant