Skip to content

feat(sync): per-file staleness banner — MCP + CLI (closes #66 phase-1)#166

Merged
Wolfvin merged 1 commit into
mainfrom
feat/issue-66-staleness-banner-phase1
Jul 3, 2026
Merged

feat(sync): per-file staleness banner — MCP + CLI (closes #66 phase-1)#166
Wolfvin merged 1 commit into
mainfrom
feat/issue-66-staleness-banner-phase1

Conversation

@Wolfvin

@Wolfvin Wolfvin commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Closes #66 (phase 1)

Ringkasan

Phase 1 dari issue #66 — per-file staleness banner. Detects when indexed files have been edited since the last scan dan surfaces a warning to the agent before the tool's actual output. Banner prepended ke semua read-tool MCP responses (suppressed on scan/init — those are the fix path).

Perubahan

New files

File Purpose
scripts/sync/__init__.py Re-exports public API
scripts/sync/pending.py StaleFileDetector (thread-safe, in-memory Dict[str, float] cache, 5s TTL per workspace) + detect_stale_files() + format_staleness_banner()
scripts/commands/staleness.py codelens staleness CLI command — manual check + full list when MCP banner truncates to 10
tests/test_staleness.py 41 tests (detection, cache, thread safety, banner formatting, CLI, MCP integration)
docs/sync/staleness-banner.md Arsitektur + design decisions

Modified

  • scripts/mcp_server.pyMCPServer._staleness_detector (lazy) + _attach_staleness_banner() prepends banner to read-tool responses (suppressed on scan/init) + _invalidate_staleness_cache() after successful scan
  • README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py — sync'd via sync_command_count.py --apply (command count 70 → 71)

Detection algorithm

  1. Load stored mtimes dari .codelens/mtimes.json (source of truth — written by incremental.save_mtimes() on every scan)
  2. For each indexed file: os.stat() compare (st_size, st_mtime_ns)
  3. If mtime differs, re-compute SHA-256 (only when needed) untuk confirm content actually changed — skips false positives dari touch / git checkout of identical content
  4. Sort by edit_age ascending (most recent edit first)
  5. Return tuple of StaleFile records

Cache

5s TTL per workspace, thread-safe via threading.Lock. Invalidated after successful scan so next read tool re-probes against fresh index. Exists karena walking 10k+ files on every tool call would add ~50ms latency.

Banner shape

⚠️ Some files referenced below were edited since the last index sync.
The index may be stale for these files — re-run `codelens scan` to refresh.
Stale files (showing 3 of 3, most recent first):
  • path/to/file.py (edited 2.3s ago, content differs)
  • other.js (edited 1m 12s ago, content differs)
  • third.ts (edited 5m 0s ago, size/mtime differ)

Plain text (not markdown) — renders correctly di terminal + MCP content blocks. ⚠️ marker untuk agent pattern-matching. "content differs" vs "size/mtime differ" distinguishes hash-confirmed staleness dari mtime-only staleness. Truncates to 10 files dengan "and N more" — full list via codelens staleness.

MCP integration

_attach_staleness_banner() dipanggil di 3 response paths:

  1. Cached response — banner attached (staleness is workspace property, not call property)
  2. Fresh success — banner attached, unless scan/init
  3. Error response — banner attached, unless scan/init (staleness context lebih useful dari error itself)

Banner prepended to first content block's text AND attached as structured response['_staleness'] field. Both paths ensure warning surfaces regardless of how agents consume tool output.

After successful scan, _invalidate_staleness_cache(workspace) called so next read tool re-probes against fresh index.

Definition of Done (Phase 1, dari issue)

  • In-memory Dict[str, float] (path → edit_timestamp), thread-safe via threading.Lock
  • Walk indexed file list dengan os.stat(path) untuk compare (st_size, st_mtime_ns)
  • Re-compute content-hash only when size/mtime changed
  • Prepend ⚠️ Some files referenced below were edited since the last index sync… banner ke MCP responses
  • Surface non-referenced pending files as small footer ("and N more" line + codelens staleness for full list)
  • New file: scripts/sync/pending.py

Phase 2 (connect-time catch-up), Phase 3 (native file watcher), Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue spec.

Verifikasi

  • python3 -m pytest tests/test_staleness.py -v41 passed
  • python3 -m pytest tests/test_staleness.py tests/test_command_count.py tests/test_doctor.py tests/test_cli.py tests/test_codelens.py tests/test_mcp_hooks.py220 passed, 0 failed
  • python3 scripts/sync_command_count.py --check → clean
  • codelens staleness smoke-tested end-to-end (text + json output, --no-confirm-hash flag)
  • MCP integration verified: banner prepended on stale workspace, suppressed on fresh, cache invalidated after scan, init failure isolated

Design decisions

  1. scripts/sync/ subpackage — staleness dan worktree mismatch (Phase 4, PR feat(sync): worktree index mismatch detection — doctor check + MCP banner (closes #66 phase-4) #154) are independent concerns sharing only the "index vs working tree" theme. Package keeps them discoverable without forcing them into one file.
  2. Lazy detector construction in MCPServer — keeps import out of server startup path. If sync subpackage fails to import, server still starts; only staleness detection degraded.
  3. Prepend (not append) banner — agents read top-to-bottom. Banner at bottom = agent acted on stale data before reaching it.
  4. Both structured _staleness field AND prepended text — different agents consume tool output differently. Both paths ensure warning surfaces.
  5. content_hash_changed tri-state (True/False/None) — True = hash confirmed stale, False = touch (skip), None = no stored hash available ("size/mtime differ").
  6. mtimes.json as source of truth (not SQLite files table) — mtimes.json written by every scan including legacy JSON registry workspaces. SQLite files table only populated when persistent registry active.
  7. 5s cache TTL — balances freshness (user edit → banner within seconds) against cost (re-stat 10k files on every call = ~50ms).

Findings (di luar scope)

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Wolfvin Wolfvin force-pushed the feat/issue-66-staleness-banner-phase1 branch from 0b41bc0 to ef4271e Compare July 3, 2026 03:30
Wolfvin added a commit that referenced this pull request Jul 3, 2026
Issue #173: PR #166 was written when main had 70 commands. After rebasing
on latest main (which merged orient #165, affected #142, and others),
the runtime COMMAND_REGISTRY now reports 77 commands.

Ran sync_command_count.py --apply to regenerate all count references:
- README.md: 5 substitutions
- SKILL-QUICK.md: 5 substitutions
- SKILL.md: 2 substitutions
- pyproject.toml: 1 substitution
- scripts/graph_model.py: 1 substitution
- skill.json: 1 substitution

Also resolved rebase conflicts in:
- scripts/mcp_server.py: merged Phase 1 (staleness) + Phase 4 (worktree)
  banner methods — both coexist on the MCP server
- scripts/sync/__init__.py: merged worktree (Phase 4) + pending (Phase 1)
  exports into unified __all__

Pre-existing finding (NOT caused by this PR): the 'affected' command
(PR #142, already on main) uses -f for --filter, which conflicts with
the global --format/-f flag added by codelens.py. This breaks ALL CLI
invocations that trigger argparse registration (e.g. 'codelens affected
--help'). Flagged for BOS — separate issue needed.
@Wolfvin Wolfvin force-pushed the feat/issue-66-staleness-banner-phase1 branch 4 times, most recently from d865095 to df47ffb Compare July 3, 2026 04:28
@Wolfvin Wolfvin merged commit bc603ea into main Jul 3, 2026
0 of 7 checks passed
@Wolfvin Wolfvin deleted the feat/issue-66-staleness-banner-phase1 branch July 3, 2026 04:28
Issue #66 Phase 1 — Per-file staleness banner. Detects when indexed
files have been edited since the last scan and surfaces a warning to
the agent before the tool's actual output.

New files:
- scripts/sync/__init__.py        — re-exports public API
- scripts/sync/pending.py         — StaleFileDetector (thread-safe,
                                    in-memory Dict[str, float] cache,
                                    5s TTL per workspace) +
                                    detect_stale_files() +
                                    format_staleness_banner()
- scripts/commands/staleness.py   — 'codelens staleness' CLI command
                                    (manual check + full list when MCP
                                    banner truncates to 10)
- tests/test_staleness.py         — 41 tests (detection, cache, thread
                                    safety, banner formatting, CLI,
                                    MCP integration)
- docs/sync/staleness-banner.md   — architecture + design decisions

Modified:
- scripts/mcp_server.py           — MCPServer._staleness_detector (lazy)
                                    + _attach_staleness_banner() prepends
                                    banner to read-tool responses
                                    (suppressed on scan/init) +
                                    _invalidate_staleness_cache() after
                                    successful scan
- README/SKILL/SKILL-QUICK/pyproject/skill.json/graph_model.py —
                                    sync'd via sync_command_count.py
                                    --apply (command count 70 -> 71)

Detection algorithm:
1. Load stored mtimes from .codelens/mtimes.json
2. For each indexed file: os.stat() compare (st_size, st_mtime_ns)
3. If mtime differs, re-compute SHA-256 (only when needed) to confirm
   content actually changed — skips false positives from  /
   M	README.md
M	SKILL-QUICK.md
M	SKILL.md
A	docs/sync/staleness-banner.md
M	pyproject.toml
A	scripts/commands/staleness.py
M	scripts/graph_model.py
M	scripts/mcp_server.py
A	scripts/sync/__init__.py
A	scripts/sync/pending.py
M	skill.json
A	tests/test_staleness.py of identical content
4. Sort by edit_age ascending (most recent edit first)
5. Return tuple of StaleFile records

Why mtimes.json (not SQLite files table) as source of truth?
  mtimes.json is written by every scan, including workspaces that use
  the legacy JSON registry (pre-v8.2). The SQLite files table is only
  populated when the persistent registry is active.

Cache: 5s TTL per workspace, thread-safe via threading.Lock. Invalidated
after successful scan so next read tool re-probes against fresh index.

Banner: plain text (not markdown), prepended to first content block's
text + structured response['_staleness'] field. Both paths ensure the
warning surfaces regardless of how agents consume tool output.

Verified:
- tests/test_staleness.py: 41 passed
- tests/test_staleness.py + test_command_count.py + test_doctor.py +
  test_cli.py + test_codelens.py + test_mcp_hooks.py: 220 passed
- sync_command_count.py --check: clean
- 'codelens staleness' smoke-tested end-to-end (text + json output)

Phase 2 (connect-time catch-up), Phase 3 (native file watcher),
Phase 5 (anonymous telemetry) deferred ke follow-up issues per issue
spec. StaleFileDetector cache + StaleFile data structure designed to
accommodate Phase 2 without API change.

Note: PR #154 (issue #66 Phase 4 — worktree mismatch) also creates
scripts/sync/__init__.py and modifies mcp_server.py. If that PR merges
first, this PR will need a small rebase to resolve the __init__.py
overlap (both add the same package marker) and the mcp_server.py
overlap (both add methods to MCPServer — different method names, no
conflict). BOS will resolve at merge time.
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

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.

[FEATURE] Telemetry, file watcher & worktree detection — observability + reliability

1 participant