Four demo-safety fixes on top of #43, none touching demo-pod data:
- De-dup at source: the suppression path returned before activeConflicts.add(),
so the same unresolved dismissed collision wrote a SuppressionDoc every frame
(~194 spam rows observed on prod). Now mark the conflict handled first, so it
records ONCE per recurrence and re-arms via the onScreenFrame resolution sweep.
- Await the write: recordSuppression is the visible learning proof, so await it
(like recordCollision/recordIntervention) instead of void ...catch().
- Display de-dup: collapse suppressed beats to one per file (most recent) with a
stable per-file id, so any pre-fix duplicate rows never render as spam.
- Live-graph gate: suppression beats now satisfy the "has activity" check in
materializePodGraph, so a clean pod with preserved suppressions (but no
collision/file nodes) no longer falls back to the demo graph and hides the proof.
- Ops: /api/memory/stats counts `suppressions`; docs/mongodb.md documents the
collection and flags it "preserve in DB cleanup" (visible learning evidence).
Verified end-to-end on a throwaway pod (not demo-pod): suppression-only pod
materializes; 2 dupe rows render as 1 beat. backend+frontend typecheck + eslint pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Codex review fix. The earlier approach synthesized a 'suppressed' beat from every
dismissed OUTCOME, which is wrong: a dismissal is not a later suppressed repeat,
and the old dismissal timestamp sank below the 12-row activity cap (invisible).
Now the negative-feedback loop is recorded WHEN IT HAPPENS. When shouldIntervene()
returns false specifically because a signature was dismissed before
(agent/podman.ts), the agent writes a durable SuppressionDoc to a new
`suppressions` collection, timestamped at the repeat. graph/live.ts materializes
those into kind:'suppressed' activity (recent -> surfaces at the top). The
suppressed collision is never written to `collisions` (agent returns before
recordCollision), so this is its own record.
Verified end-to-end: a transient demo-pod suppression renders at the TOP of the
activity stream; cleaned up after. shared build + backend/frontend typecheck +
eslint pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the P1 brittleness in the Step 3 verifier so learned_from edges
(graph/live.ts:413) can't be silently zeroed on stage.
- Capture overlap AT detection time as Collision.gitOverlap (podman.ts), while
engineer_states are still fresh, instead of re-deriving from possibly-stale
state when the user clicks. deriveWasRealCollision() now prefers this stored
evidence and only falls back to a live re-derivation for older collisions.
- Canonicalize engineer names (trim + lowercase) on both the capture and the
fallback path, so "Karti" vs "karti" no longer misses the git state.
- conflictKey now reuses the shared comparableBasename() helper (dedupe).
shared/src/collision.ts gains optional `gitOverlap?: boolean` (additive).
shared rebuilt; backend+frontend typecheck + eslint pass. PLAN.md rung 3 updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend becomes authoritative for the verifier signal. recordOutcome now
overrides the client-supplied wasRealCollision with deriveWasRealCollision():
a flagged collision counts as REAL only if BOTH named engineers currently have
the collided file in their git changedFiles (getGitStates, 120s freshness TTL).
Conservative false when the collision is orphaned/missing or git state is
stale. Restores the (accepted x wasReal) 2x2 the spec assumes instead of the
hardcoded 107/107 true.
frontend/useInterventions.ts stops sending a hardcoded `true` (now a
backend-overridden placeholder). Spec: continual-learning/spec.md:98-108,
policy.md:35-42. backend+frontend typecheck + eslint pass. PLAN.md P0.5 rung 3
added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 1 — policy.ts shouldIntervene: suppress on a prior dismissal alone.
The former `&& !priorOutcome.wasRealCollision` term was dead code (outcomes
record wasRealCollision hardcoded true), so the 85 real dismissals in Atlas
were never used. Now a prior accepted===false suppresses the next identical
nudge. Spec: continual-learning/policy.md:41, spec.md:163.
Step 2 — podman.ts handle: only escalate severity to 'critical' (the spoken
alert trigger) when the recalled prior was an accepted *real* collision,
instead of blanket-escalating every recall. Surfaces learned routing in
preferredAction; stops dismissed/false priors over-escalating to voice.
Spec: continual-learning/policy.md:62-63, plan.md:66.
Documentation-first: adds PLAN.md section 8 "P0.5 - RSI negative-feedback
activation" with both rungs + follow-ups. No schema change. backend typecheck
passes. Independent of the MongoDB-cleanup handoff (Codex).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fresh-session handoff covering the dynamic force-directed graph, honest de-noised
metrics, the click-to-explain Flow pane, the divergence vs main's parallel impl,
the best-of-both reconciliation (PR #38), build/verify quirks, and gotchas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lands the dynamic Team-memory redesign on top of main's continual-learning
pipeline. main already computes loop/activity in the API but its GraphView never
rendered them and kept a static-column graph; this swaps in the dynamic graph and
surfaces the rails, reusing main's richer PodLearningLoop / PodGraphActivity types.
Frontend (new frontend/src/components/graph/*, composed into GraphView.tsx):
- forceSim.ts: dependency-free force layout (charge, link springs, centroid
recenter, 2-pass collision, bounds clamp, alpha anneal) — no new deps/lockfile churn.
- GraphCanvas.tsx: SVG render from the sim — draggable + pinnable nodes, curved
edges, weight-sized shapes, fade-in, animated learned_from dash, risk-path
lighting / rest dimmed, label collision-avoidance.
- MetricsRail / LearningLoop / ActivityStream / SelectedNodePanel / encoding.ts:
the mock's rails + stream + detail panel in light shadcn. LearningLoop consumes
main's PodLearningLoop (steps + activeStep); ActivityStream consumes
PodGraphActivity (title + detail). SelectedNodePanel adds a Flow section that
narrates the path through a clicked node (flowNarrative); default copy is
mode-aware. Edge legend rounded out (editing/touches).
- GraphView polls every 5s and diffs (positions preserved), + ws /api/events nudge.
Stale selection (node gone across a poll) dropped so the canvas can't dim entirely.
Backend (surgical — main's materializer + buildLoop kept):
- live.ts: headline metric cards derived from the FINAL de-noised graph (Open risk
paths = distinct collision files; Learned owners = distinct owner engineers)
instead of raw collision-signature / accepted-outcome counts that inflate with
test churn (50 -> 4 risk files, 16 -> 1 owner on live). buildLoop untouched.
- demo.ts: metrics realigned to the demo graph (3 owners / 1 risk path / 100%).
Verified: lint + -r typecheck + -r build pass; Playwright confirmed the dynamic
graph (ticks on load, draggable), the loop rail (5 steps, ADAPT active) and
activity stream rendering main's shapes, honest metrics (3/1/100%), and the flow
narrative per node kind — zero page errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deployed graph was an unreadable mess: full file paths centered on nodes
bled across columns, and collision labels showed garbage vision symbols
("infra/README.md### Running the git watcher").
- node labels are now the last two path segments (full path in summary)
- collision labels are just the file (drop the junk #symbol)
- cap file nodes to 9 (collision files prioritized)
demo-pod: 22 -> 20 clean nodes; no label bleed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Materializer (live.ts) — cut demo-pod from 110 -> 22 nodes:
- cap to 8 recent collisions; collapse repeats by memorySignature
- collapse interventions to one (most recent) per collision
- filter junk 'files' (URLs, env vars, browser/app names, scratch/test) to
real source paths only
- prune test-artifact engineers (a/b/verify/codex-check/-testrepo) + any node
orphaned by that
- collisions referenced by accepted outcomes bypass the cap so the learned_from
money path never drops; risk-paths metric counts distinct signatures
UI: per-pod 'Team memory' action on each PodCard's menu (replaces the header
button that always opened pods[0]).
Verified against live demo-pod data (joins connect); typechecks clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified materializePodGraph against real demo-pod data (joins all connect:
21/21 interventions->collisions, learned_from produced). Tuning:
- engineer nodes are case-insensitive (merges Shakthi/shakthi)
- git (engineer_states) now only CONFIRMS editing on files vision/collisions
already surfaced, instead of adding the whole repo diff (killed a 29-file
node explosion from a watcher running against the full podman repo)
Cut demo-pod from 110 -> 77 nodes; git file-edges 39 -> 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- backend/src/graph/live.ts: materializePodGraph builds the graph from the real
collections (pods, engineer_states, observations, collisions, interventions,
outcomes) instead of the demo seed. Parses git-status paths, fuses git+vision,
draws collides/warns/learned_from, computes live metrics + a column layout.
- store.ts: loadPodGraph now prefers live → seeded team_model.graph → demo.
- GraphView.tsx: light/shadcn theme (from the team-memory-theme work), composed
from the ruixen primitives; node-shape encoding unchanged.
- docs/graph.md: live data→graph mapping + fallback order.
Typechecks clean. Not yet runtime-verified end-to-end (Atlas creds rotated again).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve App.tsx header conflict: keep main's "Privacy-limited" badge AND the
"Team memory" graph button (both in the header action group). server.ts and
backend/package.json auto-merged cleanly — main's closeMemory/shutdown and the
graph:seed script are both preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts:
- backend/src/server.ts: keep main's getPresence + closeRoom (room auto-cleanup)
and add the graph imports/routes (additive).
- frontend/src/App.tsx: take main's shadcn command-center UI and re-integrate
GraphView — import, graphPodId state, an early-return render branch, and a
shadcn "Team memory" button next to Refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- reachFrom now returns the direct outbound edges PLUS the recursive
$graphLookup reaches, de-duped by edge id, with maxDepth to bound cycles.
It was dropping edges straight off the start node. [review P2]
- GraphView SVG nodes are keyboard-accessible: role/tabIndex/aria-label +
Enter/Space handler alongside onClick. [review P3]
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the team_model graph layer (the "it learned" view) and its visualization,
per docs/graph.md. Demo-backed first; built on origin/main conventions.
- shared: PodGraph / node / edge / metric types + GraphNodeDoc / GraphEdgeDoc
- backend/src/graph: demo graph data; Mongo store (loadPodGraph w/ demo
fallback, seedGraph, reachFrom via $graphLookup); graph:seed script
- backend/src/server.ts: additive GET /api/pods/:id/graph + /graph/reach/:node
- frontend: dark-Bauhaus GraphView + fetch helper, reachable from a
"Team memory" toggle in App.tsx
- docs/graph.md: spec (data model, $graphLookup, API, demo-first plan)
Demo-backed: the route serves a grounded demo graph when team_model has no
graph yet; graph:seed writes team_model + graph_nodes/graph_edges so the
$graphLookup traversal is real. Swap loadPodGraph to live team_model later.
Known follow-ups before merge: branch is based on 65a0791; origin/main is now
at 1294b84, so this needs a rebase + App.tsx conflict resolution (teammate
rewrote App.tsx with live room view / one-click join).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>