Skip to content

feat(dashboard): show host provenance for sessions and learnings#129

Merged
yyiilluu merged 10 commits into
ReflexioAI:mainfrom
wenchanghan:feat/session-host-attribution
Jul 13, 2026
Merged

feat(dashboard): show host provenance for sessions and learnings#129
yyiilluu merged 10 commits into
ReflexioAI:mainfrom
wenchanghan:feat/session-host-attribution

Conversation

@wenchanghan

@wenchanghan wenchanghan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Show the local agent host that produced a session, then reuse that evidence as quiet generation provenance for project-specific skills and preferences.

Host answers where the recorded request ran. It does not restrict where a learning applies, replace Reflexio lineage, or imply that one host supplied every input to a consolidated learning.

Claude Code / Codex / OpenCode
              │
              ▼
     local session JSONL
              │
              ├── session host
              ├── confirmed request_id ──► project skill `Generated via …`
              └── confirmed request_id ──► preference `Generated via …`

     host is removed before publishing to Reflexio

Before / changed / after

Before Changed After
Sessions had no reliable host identity. Supported hooks select an explicit host before recording events. Local session records show Claude Code, Codex, OpenCode, or unknown.
Project skills and preferences exposed application value but not their generation request's host. Successful publish paths retain a confirmed request ID locally, and the dashboard maps it back to local sessions. Both learning types show secondary Generated via … provenance without implying host-specific applicability.
Host identity could be confused with Reflexio metadata. Host stays local; source, user_id, agent_version, and Reflexio lineage keep their existing meanings. Retrieval, consolidation, aggregation, and cross-host applicability are unchanged.

UI hierarchy

  • Sessions: applied learnings remain the primary value signal; host is secondary context.
  • Project-specific skills and preferences: application count remains primary; Generated via … is secondary provenance. Integration identity remains a separate field.
  • Shared skills: no host is shown. They are explicitly multi-source rollups without a single generation request, so the UI continues to show only their agent scope.
  • Known hosts use product colors plus text labels with WCAG-AA contrast; color is never the only signal.

Host enrichment is non-blocking. Sessions, skills, and preferences render without waiting for attribution, and an attribution failure does not make any primary content unavailable. Local attribution scans are aborted when their page unmounts and time out after five seconds. Project skills and preferences share the same request-to-host lookup and application-value UI.

Data and compatibility

  • Attribution is forward-only. Existing records without host data remain unknown; no host is guessed from IDs or other heuristics.
  • Explicit supported values and unknown are accepted. Malformed host values are ignored so a later valid record can establish the session host.
  • host is stripped from the wire projection and never sent in Reflexio interactions.
  • The successful local marker stores only a request ID confirmed by the publish path. Failed raw publishes keep the same frozen batch/request ID for retry rather than risking a duplicate SDK publish.
  • source="claude-smart" remains integration identity and is sent only when the installed client supports that keyword.
  • Project filtering continues to use user_id; shared-skill filtering continues to use agent_version.

Known limitation: learning provenance currently reads all local session summaries to build the request-to-host map. A dedicated index or cache can be added separately if real session volume makes that scan material.

Scope

This is Phase 1, and it is the only phase considered in this PR: local host attribution and dashboard presentation.

Cross-agent/global playbook lineage, changes to retrieval or consolidation, and future changes to how source is set are out of scope and will be discussed separately.

Validation

  • Full Python suite: 675 passed.
  • Final focused state suite: 24 passed.
  • Ruff, dashboard ESLint, TypeScript, and production build: passed.
  • Synthetic browser/API validation covered known and unknown hosts, malformed-host recovery (invalid → Codex), applied-learning priority, session detail, project-skill and preference list/detail provenance, shared skills remaining host-free, attribution failure isolation, dark/light themes, and desktop/narrow layouts.
  • Browser fixtures and screenshots use placeholder-only data; no private session or Reflexio data was loaded.
  • Independent spec and simplification reviews found no remaining substantive issue.

View sanitized validation screenshots

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Host attribution and request lineage now flow through event recording, publishing, session aggregation, and dashboard rendering. Skills and preferences display scopes, application status, and host provenance derived from session request mappings.

Changes

Host attribution and lineage

Layer / File(s) Summary
Runtime host recording
plugin/src/claude_smart/runtime.py, plugin/src/claude_smart/events/*, tests/test_events.py, tests/test_opencode_support.py
Runtime attribution distinguishes unknown hosts from compatibility hosts, and event records persist host metadata.
Publish request lineage
plugin/src/claude_smart/reflexio_adapter.py, plugin/src/claude_smart/publish.py, plugin/src/claude_smart/state.py, tests/test_adapter.py, tests/test_publish.py, tests/host_learning_harness.py
Publishing returns optional confirmed request IDs, forwards compatible metadata, and persists request IDs in watermark markers.
Session metadata dashboard
plugin/dashboard/lib/types.ts, plugin/dashboard/lib/session-reader.ts, plugin/dashboard/components/common/*, plugin/dashboard/app/dashboard/page.tsx, plugin/dashboard/app/sessions/*
Session aggregation exposes normalized hosts and request IDs, while session views render host and learning badges.
Skill and preference provenance UI
plugin/dashboard/lib/host-attribution.ts, plugin/dashboard/app/skills/*, plugin/dashboard/app/preferences/*
Skills and preferences use request attribution to display scopes, origins, host badges, unavailable-state messaging, and learning application status.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant EventHooks
  participant SessionReader
  participant SkillsPage
  Runtime->>EventHooks: attribution_host()
  EventHooks->>SessionReader: session JSONL with host and request_id
  SessionReader->>SkillsPage: SessionSummary host and request_ids
  SkillsPage->>SkillsPage: derive skill scope and host provenance
Loading
sequenceDiagram
  participant SessionPage
  participant SessionReader
  participant HostBadge
  SessionPage->>SessionReader: load session detail
  SessionReader-->>SessionPage: host and learning_interaction_count
  SessionPage->>HostBadge: render host metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the dashboard change to surface host provenance across sessions and learning-related views.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_events.py (1)

2058-2073: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test isolation: runtime.set_host() leaks global state across tests.

runtime.set_host() mutates the module-level _current_host global and sets os.environ[HOST_ENV] directly (not via monkeypatch.setenv). Neither is cleaned up after each test. While every host-stamping test currently calls set_host() explicitly, any future test in this file that relies on runtime.host() defaulting to HOST_CLAUDE_CODE will inherit the last test's host instead.

Consider adding an autouse fixture that resets runtime._current_host = None and restores HOST_ENV after each test.

♻️ Proposed fixture for test isolation
+# Add to conftest.py or the top of test_events.py
+
+@pytest.fixture(autouse=True)
+def _reset_runtime_host(monkeypatch):
+    """Reset runtime host state after each test to prevent cross-test leakage."""
+    import claude_smart.runtime as runtime
+    monkeypatch.setattr(runtime, "_current_host", None)
+    yield

Also applies to: 2076-2090, 2093-2111, 2114-2135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_events.py` around lines 2058 - 2073, Add an autouse fixture in
tests/test_events.py that snapshots the current HOST_ENV value and resets
runtime._current_host before and after each test, restoring or removing HOST_ENV
afterward as appropriate. Apply it to the host-stamping tests so runtime.host()
returns the default HOST_CLAUDE_CODE for tests that do not call
runtime.set_host().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_events.py`:
- Around line 2058-2073: Add an autouse fixture in tests/test_events.py that
snapshots the current HOST_ENV value and resets runtime._current_host before and
after each test, restoring or removing HOST_ENV afterward as appropriate. Apply
it to the host-stamping tests so runtime.host() returns the default
HOST_CLAUDE_CODE for tests that do not call runtime.set_host().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 06cf1717-22a2-4964-9ccd-d43f40bf29cb

📥 Commits

Reviewing files that changed from the base of the PR and between 540a338 and 2cc6d6b.

📒 Files selected for processing (15)
  • plugin/dashboard/app/dashboard/page.tsx
  • plugin/dashboard/app/sessions/[sessionId]/page.tsx
  • plugin/dashboard/app/sessions/page.tsx
  • plugin/dashboard/components/common/host-badge.tsx
  • plugin/dashboard/components/common/page-header.tsx
  • plugin/dashboard/lib/session-reader.ts
  • plugin/dashboard/lib/types.ts
  • plugin/src/claude_smart/events/post_tool.py
  • plugin/src/claude_smart/events/session_end.py
  • plugin/src/claude_smart/events/stop.py
  • plugin/src/claude_smart/events/user_prompt.py
  • plugin/src/claude_smart/reflexio_adapter.py
  • plugin/src/claude_smart/runtime.py
  • tests/test_adapter.py
  • tests/test_events.py

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Given Claude-smart uses Reflexio User for each "host" (need to validate), does it make sense to reuse and surface User as Host on dashboard? This wouldn't require us to introduce another Host concept and also would help with surfacing playbook's "host" with User

@wenchanghan
wenchanghan force-pushed the feat/session-host-attribution branch from 2cc6d6b to 51240a7 Compare July 12, 2026 05:16
@wenchanghan wenchanghan changed the title feat(dashboard): surface host CLI on sessions, mark reflexio rows with caller identity feat(dashboard): surface Reflexio users across sessions and skills Jul 12, 2026
@wenchanghan
wenchanghan marked this pull request as draft July 12, 2026 07:49
@wenchanghan
wenchanghan force-pushed the feat/session-host-attribution branch from 51240a7 to 64f17ce Compare July 12, 2026 09:25
@wenchanghan wenchanghan changed the title feat(dashboard): surface Reflexio users across sessions and skills feat(dashboard): show host CLI on sessions and project skills Jul 12, 2026
@wenchanghan

wenchanghan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Previous sanitized UI validation

Superseded by the updated placeholder-only screenshots after the value-first UI hierarchy change.

Return confirmed request lineage with each publish result, preserve raw-client compatibility, and load skill attribution without serial dashboard latency.
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Feedback resolution

The original User vs Host question is resolved in favor of keeping host as a claude-smart-local attribution fact. user_id is the project/worktree scope used for project skills, not the invoking CLI; current data contains multiple fragmented project/worktree/session-derived values. Re-labeling it as Host would therefore misattribute both sessions and skills. This PR keeps Reflexio's User, agent, and source meanings unchanged.

Claude review disposition:

  1. Raw failure → SDK fallback: no change. A raw timeout is ambiguous: Reflexio may have accepted the stable request ID even if the response was lost. Retrying through the SDK could publish the same batch under a different ID. Failure leaves the local watermark unchanged, so the next hook retries the same frozen batch and stable ID. A regression test now locks this behavior.
  2. Locally generated ID on the SDK path: no change. The SDK path did not send that local ID, so recording it would create false lineage. We persist only an ID confirmed by the actual publish path; older SDKs that return no ID intentionally omit lineage.
  3. Inherited CLAUDE_SMART_HOST: no behavior change. This environment variable is the explicit subprocess propagation contract, not a host guess. The attribution docstring now says unknown is used only when no explicit valid host is set.
  4. Raw source compatibility: fixed. The raw payload includes source="claude-smart" only when the installed client's publish contract supports the field, matching the public SDK path.
  5. Shared last_request_id race: fixed. Adapter.publish() now returns an immutable per-call PublishResult; the caller consumes that result directly. A concurrent shared-adapter test proves each call retains its own confirmed ID.
  6. Sequential /api/sessions load: fixed. Attribution loads in the initial Promise.all, with its own non-blocking failure result so skill loading still succeeds independently.
  7. Two runtime globals: no change. They encode different contracts for invalid input: compatibility falls back to claude-code, while attribution records unknown. After set_host(), both accessors use their cached values, so later environment mutation does not make them diverge.
  8. Shared-skill host availability at render time: fixed. hostUnavailable is now a SkillCard property set by the project/shared constructors and consumed directly by HostBadge.

The CodeRabbit host-state isolation nit is also covered: the affected event tests use isolated_runtime_host, which resets both runtime globals and removes/restores CLAUDE_SMART_HOST through monkeypatch for each test.

Validation on the final commit: 675 Python tests passed, Ruff passed, dashboard ESLint + TypeScript + production build passed, and the six host/install integration paths affected by the publish-result contract passed. The UI screenshots above use placeholder-only fixtures and no private session data.

Collapse duplicate runtime state, localize one-caller join logic, and remove repeated UI and publish capability decisions without changing attribution behavior.
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Simplification pass

Applied the lean parts of the follow-up review without changing behavior:

  • Collapsed runtime host state from two globals to one cached candidate plus one resolver. This preserves the distinct compatibility (claude-code) and attribution (unknown) fallbacks, including set_host(None).
  • Hoisted the repeated HostBadge size classes and Skills scope label.
  • Reused one source capability check across both publish paths and localized the integration source constant to the Reflexio adapter instead of exposing it from host runtime state.
  • Removed the one-caller host-attribution.ts module and kept the small request-to-host join beside its only consumer.
  • Retained hostUnavailable on SkillCard and the parallel /api/sessions load; both already implement the recommended locality and latency improvements.

I intentionally kept host stamping explicit at the four interaction producers. Adding another record-construction interface would broaden this Phase 1 change and would not reduce the caller contract enough to earn the extra seam.

Result: one fewer module, one fewer runtime state variable, a narrower runtime interface, and fewer repeated decisions. Final validation remains 675 tests passed, Ruff/ESLint/TypeScript/dashboard build passed, and all GitHub checks are green on 23d4507.

Use verified Claude, Codex, and OpenCode product colors with WCAG-AA label contrast across light and dark themes.
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Native host colors

Updated the shared host badge using colors verified from the current official desktop assets:

  • Claude Code: #D97757 / RGB 217, 119, 87
  • Codex: #0169CC / RGB 1, 105, 204
  • OpenCode: #131010 / RGB 19, 16, 16

Accessibility: the small badge labels use dark brown on Claude orange (5.65:1), white on Codex blue (5.39:1), and white on OpenCode black (18.93:1). OpenCode also gets a light boundary in dark mode so the black fill remains visually distinct.

The existing sanitized-validation comment now contains freshly uploaded 1440×1000 Sessions and Project Skills screenshots generated from placeholder-only fixtures. No private session or Reflexio data was loaded. Dashboard lint, TypeScript, production build, light/dark visual checks, and all 9 GitHub checks pass on 9a8d032.

Prioritize applied learnings, keep host badges on sessions, and present project-skill host as origin rather than applicability.
Carry applied-learning counts onto session detail, label shared applicability scope explicitly, and centralize host attribution metadata and joins.
@wenchanghan wenchanghan changed the title feat(dashboard): show host CLI on sessions and project skills feat(dashboard): attribute sessions and skill origins by host Jul 12, 2026
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Updated sanitized UI validation

Synthetic placeholder fixtures only; no private session or Reflexio data was loaded. Applied-learning value leads host context, and skill host is presented only as provenance.

Sessions

Sanitized sessions showing applied learnings before native-color host badges

Project skills

Sanitized project skills showing application value before quiet Learned via provenance

@wenchanghan
wenchanghan marked this pull request as ready for review July 12, 2026 18:48
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Code review — additional findings (advisory)

5 real findings below the automated confidence threshold (scored 75–78/100). Not false positives, but lower severity or edge-case:

  1. normalizeHost returns "unknown" (non-null) for unrecognized host strings, permanently locking the session host. foldTurns skips the host slot once host !== null, so the first record with any unrecognized host string silences all later valid-host records. Fix: return null for unrecognized strings and treat "unknown" as an explicit value written only by the Python side.

function normalizeHost(value: unknown): Host | null {
if (typeof value !== "string") return null;
return VALID_HOSTS.has(value as Host) ? (value as Host) : "unknown";
}

  1. set_host writes HOST_ENV normalized to "claude-code" even for unrecognized host strings. In subprocess hook invocations _current_host is always None, so attribution_host() reads HOST_ENV and returns "claude-code" rather than "unknown" for sessions with an unrecognized host. A second env var carrying the raw attribution value would survive the subprocess boundary.

def _resolve_host(value: str | None, fallback: str) -> str:
return value if value in VALID_HOSTS else fallback
def set_host(value: str | None) -> str:
"""Set the current host, returning the normalized value."""
global _current_host
_current_host = value if value is not None else HOST_UNKNOWN
host = _resolve_host(_current_host, HOST_CLAUDE_CODE)
os.environ[HOST_ENV] = host
return host
def host() -> str:
"""Return the current host, defaulting to Claude Code for compatibility."""
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
return _resolve_host(value, HOST_CLAUDE_CODE)
def attribution_host() -> str:
"""Return the explicitly selected record host, or unknown when unset."""
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
return _resolve_host(value, HOST_UNKNOWN)

  1. host is stamped into interaction records and flows to the Reflexio server unfiltered. source in the same PR is gated with _supports_keyword(client.publish_interaction, "source"); host has no equivalent guard. If Reflexio rejects unknown fields, all publishes fail; if it silently drops them, the stated "host stays in JSONL only" boundary is violated without warning.

"ts": int(time.time()),
"role": "User",
"content": prompt,
"user_id": project_id,
"host": runtime.attribution_host(),
},
)

  1. skills/page.tsx fetches /api/sessions with cache: "no-store" on every mount — no pagination limit — solely to build the request_id → host map. For users with many sessions this triggers a full session-directory scan on every skills page navigation.

cache: "no-store",
})
.then((r) => r.json())
.catch(() => ({
success: false,
stats: [] as PlaybookApplicationStat[],
})),
fetch("/api/sessions", { cache: "no-store" })
.then(async (response) => {
if (!response.ok) throw new Error(`sessions ${response.status}`);
const data = await response.json();
return {
sessions: (data.sessions ?? []) as SessionSummary[],
failed: false,
};
})
.catch(() => ({
sessions: [] as SessionSummary[],

  1. skills/project/[id]/page.tsx fetches both playbooks and /api/sessions inside a single Promise.all, blocking the skill detail render (trigger, rule, rationale) until the sessions API responds. Host attribution is secondary metadata with no bearing on the main content.

https://github.com/ReflexioAI/claude-smart/blob/da6366c7d13f667c9da05882c38272c536a5a8e8/plugin/dashboard/app/skills/project/[id]/page.tsx#L75-L85

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Strip dashboard-only host metadata from Reflexio interactions, ignore malformed host values when a later valid record exists, and let project-skill content render independently of attribution.
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Follow-up on the five advisory findings against the current head (086e07f):

  1. Fixed: malformed recorded hosts are ignored, so a later valid host can establish session attribution. Explicit unknown remains a valid recorded value.
  2. No code change: installed hook entry points accept only a host from VALID_HOSTS and call runtime.set_host() before dispatching the handler in the same process. An unsupported host cannot enter that path, so a second inherited environment variable would add state without fixing a reachable production case. The existing environment fallback remains compatibility behavior.
  3. Fixed: host is removed by the local-to-wire projection and covered by the existing projection test. It never reaches Reflexio interactions.
  4. Accepted limitation: the skills page still scans local session summaries. A durable fix needs an index/cache or narrower projection; that is called out in the PR description and kept outside this lean Phase 1 change.
  5. Fixed: playbook and session requests start together, but primary skill content renders without waiting for attribution. While attribution is pending, the Origin row is omitted rather than showing a premature unknown value.

Final local checks and the refreshed GitHub matrix are green; CodeRabbit completed with no new actionable thread.

@wenchanghan

wenchanghan commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Sanitized validation screenshots

Placeholder-only fixtures; no private session or Reflexio data was loaded.

Sessions — applied learning remains primary; host is secondary

Sanitized sessions list showing applied-learning badges before native-color host badges

Project-specific skills — application value before generation provenance

Sanitized project skills list showing application value before Generated via host provenance

Preferences — the same generation provenance, with integration identity kept separate

Sanitized preferences list showing application value, Generated via host provenance, and separate integration identity

Preference detail — Origin and Integration are distinct metadata

Sanitized preference detail showing distinct Origin and Integration metadata

@wenchanghan wenchanghan changed the title feat(dashboard): attribute sessions and skill origins by host feat(dashboard): show host provenance for sessions and learnings Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
plugin/dashboard/lib/host-attribution.ts (1)

25-50: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a fetch timeout to prevent indefinite hanging.

The fetch("/api/sessions") call has no timeout. If the endpoint hangs, attribution stays null forever — pages never show the "unavailable" banner or host provenance. An AbortController with a timeout ensures the error path fires and users see the appropriate fallback UI.

⏱️ Proposed fix: add AbortController timeout
 export function useRequestHostAttribution(): RequestHostAttribution | null {
   const [attribution, setAttribution] =
     useState<RequestHostAttribution | null>(null);

   useEffect(() => {
     let cancelled = false;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 5000);
+
     fetch("/api/sessions", { cache: "no-store" })
+      .then(async (response) => {
         if (!response.ok) throw new Error(`sessions ${response.status}`);
         const data = await response.json();
         return (data.sessions ?? []) as SessionSummary[];
       })
       .then((sessions) => {
         if (!cancelled) {
           setAttribution({
             hosts: hostsByRequestId(sessions),
             unavailable: false,
           });
         }
       })
       .catch(() => {
         if (!cancelled) {
           setAttribution({ hosts: new Map(), unavailable: true });
         }
       })
+      .finally(() => {
+        clearTimeout(timeout);
+      });

     return () => {
       cancelled = true;
+      controller.abort();
+      clearTimeout(timeout);
     };
   }, []);

   return attribution;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/dashboard/lib/host-attribution.ts` around lines 25 - 50, Update the
sessions-loading useEffect around fetch("/api/sessions") to create an
AbortController and abort the request after a timeout, passing its signal to
fetch so hung requests enter the existing catch fallback. Clear the timeout and
preserve the existing cancelled cleanup behavior to avoid updates after unmount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugin/dashboard/app/preferences/`[id]/page.tsx:
- Around line 344-354: Update the “Origin” Meta rendering guard in the
preferences page to require both attribution and
profile.generated_from_request_id. Preserve the existing LearningHostProvenance
props and hide the row whenever the request ID is falsy, matching the skills
detail page behavior.

In `@plugin/dashboard/app/skills/project/`[id]/page.tsx:
- Around line 351-361: Update the Origin Meta render guard in the playbook
details JSX to require both attribution and playbook.request_id, matching the
existing Request CopyMeta behavior; keep the LearningHostProvenance props and
rendering unchanged.

---

Nitpick comments:
In `@plugin/dashboard/lib/host-attribution.ts`:
- Around line 25-50: Update the sessions-loading useEffect around
fetch("/api/sessions") to create an AbortController and abort the request after
a timeout, passing its signal to fetch so hung requests enter the existing catch
fallback. Clear the timeout and preserve the existing cancelled cleanup behavior
to avoid updates after unmount.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd93b4d3-46df-4880-9940-ca48455f2823

📥 Commits

Reviewing files that changed from the base of the PR and between 086e07f and 7aa17a7.

📒 Files selected for processing (7)
  • plugin/dashboard/app/preferences/[id]/page.tsx
  • plugin/dashboard/app/preferences/page.tsx
  • plugin/dashboard/app/skills/page.tsx
  • plugin/dashboard/app/skills/project/[id]/page.tsx
  • plugin/dashboard/components/common/host-badge.tsx
  • plugin/dashboard/components/common/learning-application-badge.tsx
  • plugin/dashboard/lib/host-attribution.ts

Comment thread plugin/dashboard/app/preferences/[id]/page.tsx Outdated
Comment thread plugin/dashboard/app/skills/project/[id]/page.tsx Outdated
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Code review — iteration 2 (host SHA: 8020c62)

Validated the claim: preferences, project skills, and sessions all get host provenance; shared skills are correctly excluded (no attribution possible for aggregated-multi-source playbooks). The host exclusion from Reflexio wire turns (state.py + unpublished_slice) is verified complete — both the publish_interaction and _make_request paths consume the same pre-stripped dicts.

Three findings (one new, two carry-forward):


1. (New) preferences/page.tsx — missing generated_from_request_id guard on the list page

The fix commit (8020c62) correctly guards LearningHostProvenance on the detail page with profile.generated_from_request_id &&, but the list page renders HostBadge unconditionally when attribution is available. When p.generated_from_request_id is falsy, Map.get(falsy) returns undefined, ?? null collapses it to null, and HostBadge renders "Generated via unknown host" — a false provenance signal for preferences that have no originating request.

Fix: add the same guard as the detail page.

// before
{attribution && !attribution.unavailable && (

// after
{attribution && !attribution.unavailable && p.generated_from_request_id && (

<div className="mt-2 flex flex-wrap items-center gap-3">
{attribution && !attribution.unavailable && (
<HostBadge
host={
attribution.hosts.get(
p.generated_from_request_id,
) ?? null
}
display="provenance"


2. (Carry-forward) useRequestHostAttribution — no AbortController, N fetches per page mount

Each of the four pages that calls this hook fires an independent unbounded /api/sessions scan (O(files × lines), cache: "no-store"). The cancelled flag prevents stale React state updates after unmount but does not abort the in-flight request, so rapid page navigation leaves concurrent server-side scans running. Adding an AbortController and — when needed — lifting the fetch into a layout-level context would address both.

useEffect(() => {
let cancelled = false;
fetch("/api/sessions", { cache: "no-store" })
.then(async (response) => {
if (!response.ok) throw new Error(`sessions ${response.status}`);
const data = await response.json();
return (data.sessions ?? []) as SessionSummary[];
})
.then((sessions) => {
if (!cancelled) {
setAttribution({
hosts: hostsByRequestId(sessions),
unavailable: false,
});
}
})
.catch(() => {
if (!cancelled) {
setAttribution({ hosts: new Map(), unavailable: true });
}
});
return () => {
cancelled = true;
};
}, []);


3. (Carry-forward) set_host writes compat env var normalized to "claude-code" for unrecognized inputs

set_host("unsupported-host") stores the raw value in _current_host but writes _resolve_host("unsupported-host", HOST_CLAUDE_CODE) — i.e. "claude-code" — to HOST_ENV. Each subprocess hook invocation imports with _current_host = None, so attribution_host() reads HOST_ENV and returns "claude-code" instead of "unknown" for sessions started with an unrecognized host. A second env var carrying the raw attribution value (e.g. CLAUDE_SMART_ATTRIBUTION_HOST) would survive the subprocess boundary correctly.

def set_host(value: str | None) -> str:
"""Set the current host, returning the normalized value."""
global _current_host
_current_host = value if value is not None else HOST_UNKNOWN
host = _resolve_host(_current_host, HOST_CLAUDE_CODE)
os.environ[HOST_ENV] = host
return host
def host() -> str:
"""Return the current host, defaulting to Claude Code for compatibility."""
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
return _resolve_host(value, HOST_CLAUDE_CODE)
def attribution_host() -> str:
"""Return the explicitly selected record host, or unknown when unset."""
value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
return _resolve_host(value, HOST_UNKNOWN)


🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@wenchanghan

Copy link
Copy Markdown
Contributor Author

Latest review feedback

Addressed the actionable Claude Code and CodeRabbit findings on 4a29457:

  • Preferences without a generated_from_request_id no longer render false Generated via unknown host provenance on the list page.
  • The local /api/sessions attribution scan now uses an AbortController, aborts on unmount, and fails into the existing unavailable state after five seconds. Each mounted page still owns one fresh scan; a layout cache/context would add stale shared state for secondary local metadata, while unmount cancellation removes the reported overlapping-navigation case.
  • No second host environment variable was added. Installed hook entry points accept only VALID_HOSTS; any other argv shape is normalized to Claude Code before runtime.set_host() runs. Preserving an unsupported raw value across subprocesses would therefore add state for no reachable installed-hook path. Direct set_host("unsupported-host") calls intentionally keep compatibility behavior (claude-code) separate from same-process attribution (unknown).

The clean-context simplification pass also removed the tooltip-only learningLabel component prop. It intentionally retained explicit unknown versus missing/malformed host state because those have different fold semantics: explicit unknown is stable, while missing or malformed input may yield to a later valid host.

Validation: 675 tests passed; Ruff, dashboard ESLint, TypeScript, and production build passed. Screenshots remain current because these changes affect only missing-ID/error behavior and hidden tooltip copy.

@yyiilluu
yyiilluu merged commit 800343f into ReflexioAI:main Jul 13, 2026
9 checks passed
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.

2 participants