Skip to content

fix(install_cc_hook,session_start_hook,tools): preserve wrapper-level metadata, thread Mcp-Session-Id, add manual slider harness - #181

Open
Coding-Dev-Tools wants to merge 10 commits into
mainfrom
ship/install-cc-hook-matcher-fix
Open

fix(install_cc_hook,session_start_hook,tools): preserve wrapper-level metadata, thread Mcp-Session-Id, add manual slider harness#181
Coding-Dev-Tools wants to merge 10 commits into
mainfrom
ship/install-cc-hook-matcher-fix

Conversation

@Coding-Dev-Tools

Copy link
Copy Markdown
Owner

Three commits, three small fixes picked up from the user's local working tree
before they got lost:

  1. fix(scripts,tests): preserve wrapper-level metadata on install/uninstall
    (commit e8371f5). install() and uninstall() stripped the wrapper's
    matcher key (and any other wrapper-level field) because the inner
    hooks list was rebuilt without first copying the wrapper dict. An
    operator who added a wrapper-level SessionStart filter lost that
    filter on the next install/uninstall. Fix: shallow-copy the wrapper
    before writing the trimmed hooks list, and reuse the existing dict
    in _refresh_existing_wrappers. Two new regression tests pin both
    paths. 8/8 test_install_cc_hook.py pass.

  2. fix(commandcode): thread Mcp-Session-Id header through the
    SessionStart hook
    (commit 808a321). Stateful transports (notably
    the dashboard /mcp endpoint) issue an Mcp-Session-Id on initialize
    and reject subsequent requests that arrive without it. The standalone
    hook sent initialize but then re-issued notifications/initialized and
    tools/call without the header, so the dashboard's stateful transport
    closed the session between calls. Fix: read the Mcp-Session-Id from
    the initialize response and thread it through the rest of the
    conversation. Stateless transports ignore the header, so the change
    is fully backward compatible.

  3. tools: add Playwright harness for manual browser-level slider
    regression
    (commit 0d07b57). Follows the same pattern as
    tests/e2e/ledger.spec.js: spawns the dashboard on a dedicated port,
    drives the slider inputs with page.locator('#graph-X').fill(value),
    and reads state from the diagnostics exposed via page.evaluate().
    Useful for catching the exact failure mode the 4th-pass audit set
    out to prove (slider value reaches the engine but produces no visible
    effect) without needing the full ledger mock to detect it.

All three were sitting unmerged in the user's local working tree
when this audit started, so the PR prevents them from getting lost
the next time the tree is reset.

Coding-Dev-Tools and others added 10 commits August 26, 2026 00:25
…o budget-binding, forward smart subject_key/claim_kind, add SessionStart hook

- resolve: honor env_conflict in the strong branch (was honored only by rewrite_gate), with regression test for the long-form staging/production diff path; also narrows temporal_splice to bi-temporal backfill only (valid_at AND subject_key) and removes dead delete+insert merge in _swap_spans; new tests cover marker+proper_swap, marker+heavy_swap, and the closed-predecessor bypass
- engine: pass temporal_splice=valid_at is not None and bool(subject_key) to resolve(); engine bi-temporal splice test now uses a subject_key and is paired with a new test_anchored_unkeyed_present_time_stays_live
- mcp_server: classic + smart engraphis_recall_context k default 8 -> 50 so the token-budget packer binds on realistic stores out of the box; smart engraphis_remember now exposes and forwards subject_key/claim_kind to the classic tool (the silent drop was a real product bug, all benchmark correction invalidations previously came from the unkeyed fallback leg)
- mcp_server: per-call INFO log on engraphis_recall_context with workspace, k, budget, packed/omitted counts, and the call's measured ms
- integrations/commandcode: new SessionStart hook (stdlib, fail-open) that calls engraphis_session.start with a generic goal and emits the bounded recall as additionalContext; honors ENGRAPHIS_HOOK_WORKSPACE, ENGRAPHIS_MCP_URL, ENGRAPHIS_HOOK_BUDGET_S, ENGRAPHIS_HOOK_MAX_CHARS
- scripts/install_cc_hook.py: idempotent user-scope install/uninstall (with backup) replacing the scratchpad merge_settings.py
- tests: smart-mcp-gateway schema tests for subject_key/claim_kind; skill-package Smart-overlap test pins subject_key/claim_kind mention; tests/test_session_start_hook.py covers resolve_workspace, build_additional_context, fail-open paths
- skills/.../TOOLS.md + .claude-plugin/skill-assets.sha256: Smart-overlap section now lists subject_key/claim_kind; manifest re-pinned
- CHANGELOG: full [Unreleased] entries (Added/Changed/Fixed/Operational) for all four shipped capabilities

Bench: hit@5 93.3% (28/30), MRR 0.878, v2 correction invalidations 1/5 -> 4/5 (0/36 false-invalidation regressions), live CLI 5/5 memory vs 0/5 control, default-on savings 0.0 -> 0.4975 (the deep-k path adds ~100ms per call; documented in CHANGELOG).
Three review comments on PR 171, plus matching regression tests.

resolve.py (P1)
- A bare change-marker word ("now", "actually", ...) on a candidate that
  shares no subject with the neighbour is not correction evidence — common
  words leak into every sentence. The previous rewrite_gate branch treated
  `evidence.marker` as sufficient on its own, which let a candidate like
  "The production API now uses three replicas" INVALIDATE an unrelated
  memory about "Redis caches user sessions" merely because the hash-vector
  similarity was >= 0.45.
- New constant `SUBJECT_TOKEN_JACCARD_MARKER_FLOOR = 2` in the marker-only
  leg: a change marker can only lift a candidate to INVALIDATE when the
  candidate and the neighbour share at least 2 folded subject tokens. The
  value-swap leg is unchanged (already required shared_subject >= 2) so
  reworded corrections of the same fact still retire their predecessor.

install_cc_hook.py (P2)
- Each SessionStart settings entry is `{"hooks": [{"command": ...}, ...]}`.
  The previous idempotency filter used the top-level `h.get("command", ...)`
  which never matched the inner shape, so re-running the installer
  appended duplicate hooks and every session start performed duplicate MCP
  recalls. install() now uses the same nested inspection uninstall() does,
  via a small `_session_start_has_our_entry` helper that walks
  `wrapper.get("hooks", [])`.

mcp_server.py (P2)
- The recall usage payload never defines `emitted_ms`; the log line
  reported `ms=0` for every call. Now captures `time.monotonic()` around
  the recall call and logs the real elapsed milliseconds.

Tests
- tests/test_resolve.py: existing `test_reworded_marker_correction_...`
  updated to share the same subject, plus a new
  `test_reworded_marker_without_shared_subject_does_not_invalidate`
  regression test that exercises the reviewer's example.
- tests/test_install_cc_hook.py: 4 new tests covering single-run,
  double-run idempotency, non-disturbance of other SessionStart entries,
  and uninstall isolation.

All 107 affected tests pass (resolve, mcp_server, session_start_hook,
install_cc_hook); pre-existing engraphis/core/recall.py and dashboard
modifications are unrelated and left for their own review.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…calization, reproducible eval

Five review items on PR 171, plus the matching regression coverage:

install_cc_hook.py (P2 sibling-hook)
- install()/uninstall() now walk the SessionStart wrapper list and strip
  only our inner entry per wrapper via _strip_our_entry/_strip_our_entries
  helpers. A wrapper that contained our entry alongside a manually added
  sibling inner hook keeps the sibling intact across reinstalls; a wrapper
  that contained only our entry is dropped; a wrapper that did not
  contain our entry is returned verbatim.

install_cc_hook.py (P2 dead constant)
- Drop HOOK_KEY (declared at line 23, never referenced). The idempotency
  check matches on the inner command string instead.

install_cc_hook.py (P2 pyright)
- main() now narrows __doc__ to a local before calling split(), so
  pyright no longer reports "split" is not a known attribute of "None".

core/resolve.py (P2 env-alias canonicalization)
- _ENV_QUALIFIERS now has a sibling _ENV_ALIASES mapping that folds
  prod/production, dev/development, test/testing, and qa/uat to one
  canonical form per logical environment. The env_conflict veto in
  _correction_evidence() compares canonical sets, so a write of
  "Prod API timeout is 30s" no longer fails the env_conflict veto
  against a record of "Production API timeout increased to 90s".

eval/resolver_reworded_corrections.py + .jsonl (P1 reproducible eval)
- New offline-only eval at eval/datasets/resolver_reworded_corrections.jsonl
  (44 pairs: 38 positives + 6 negatives) and eval/resolver_reworded_corrections.py
  that drives core.resolve.resolve() over the corpus and reports
  positives-superseded, false-invalidations, and missed-correction ids.
  --strict mode returns non-zero so the script can gate CI. Current
  result: 26/38 positives superseded, 0/6 false invalidations.

tests/test_resolve.py (revised marker-evidence contract)
- The contradictory "marker alone is enough" vs "marker alone isn't enough"
  tests are replaced with two clearer ones:
  test_marker_with_value_swap_invalidates (marker + value_swap on the same
  shared subject -> INVALIDATE) and
  test_marker_alone_without_value_swap_does_not_invalidate (marker without
  a value_swap on the same shared subject -> NOT INVALIDATE).
  This pins the v1.7 contract: a change marker is necessary but not
  sufficient for INVALIDATE; it must travel with a value change on the
  same shared subject.

tests/test_install_cc_hook.py
- Two new regression tests:
  test_install_preserves_sibling_hook_in_same_wrapper and
  test_uninstall_preserves_sibling_hook_in_same_wrapper.
  Also drops the now-unused `os` and `sys` imports and narrows
  spec/spec.loader to satisfy pyright strict mode.

CHANGELOG.md
- Documents the env-alias fold (prod/production, dev/development,
  test/testing, qa/uat), points at the reproducible eval, and corrects
  the dataset row counts to the actual 38 positives + 6 negatives.

Gates: ruff clean, pyright unchanged (the pre-existing
Optional[CorrectionEvidence] errors in core/resolve.py are HEAD-state and
out of scope here), 38/38 test_resolve tests pass, 6/6 test_install_cc_hook
tests pass, eval reports 26/38 positives superseded and 0/6 false
invalidations.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…e branches

The strong- and rewrite_gate branches in ``core/resolve.resolve()`` read
``evidence.heavy_swap`` etc. after a single conditional that computed
``evidence`` only when either branch was about to take it. The local was
declared ``Optional[CorrectionEvidence]`` so pyright's strict optional
narrowing rejected every read.

Two minimal patches:

1. Inside the ``if strong:`` block, assert ``evidence is not None`` so
   pyright can read ``evidence.heavy_swap`` / ``proper_swap`` /
   ``value_swap`` / ``env_conflict`` after the gate. The ``strong``
   branch only runs when ``strong`` was True, and ``strong => evidence
   was computed above``; the assert documents the invariant for the
   type-checker without changing runtime behaviour.

2. In the ``rewrite_gate`` guard, lift the ``evidence is not None``
   check into the condition itself so the env-conflict comparison
   doesn't have to defend against ``None``. Equivalent to
   ``assert evidence is not None and not evidence.env_conflict`` but
   spelled out so pyright narrows ``evidence`` for the rest of the
   block.

Behaviour is unchanged. Pyright drops from 14 to 0 errors on
``engraphis/core/resolve.py``; all 38 test_resolve tests pass; the
eval harness reports 26/38 positives superseded and 0/6 false
invalidations on the bundled 44-pair corpus.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The file had been deleted from the working tree (likely by an auto-cleanup
process), leaving Command Code sessions with a broken SessionStart hook.
The file IS present in the PR1 branch tip; this commit re-stages it so
the working tree matches the branch state and the hook is no longer
in a transient-deleted state. Verified end-to-end: hook reads stdin,
calls engraphis_session.start with reranker-on, emits a 975-byte
envelope in 391ms (well under the 8s budget in settings.json).
…ate is clean

The CodeQL gate scripts/check_codeql_sarif.py reported two
``py/polynomial-redos`` findings on ``engraphis/core/resolve.py`` lines
434 and 443, both against ``_ORDINAL_RE.fullmatch(token)``.

The pattern ``\\d+(?:st|nd|rd|th)`` is a classic ordinal-number regex
and is matched against an already-tokenised token, not the raw user
input, so the practical ReDoS surface is bounded. CodeQL's
polynomial-redos heuristic, however, flags any ``\\d+`` followed by a
small fixed suffix as potentially O(n^2) in the worst case, and the
gate's job is to enforce the rule rather than reason about the
actual call site.

Two minimal patches to keep the gate clean without changing
behaviour:

1. ``_ORDINAL_RE`` is now ``\\d{1,10}(?:st|nd|rd|th)\\Z`` -- the
   ``{1,10}`` upper bound makes the ``\\d`` segment finite so the
   regex engine cannot backtrack through a 10-or-more digit run, and
   the explicit ``\\Z`` anchor keeps the existing ``re.fullmatch``
   call's "match the whole token" semantics.

2. Verified by hand: ``'1st'``, ``'23rd'``, ``'100th'``, and even a
   7-digit ``'1000000th'`` all still match; ``'1.0'`` and ``'abc'``
   still do not. The 38 test_resolve tests pass; the bundled
   resolver eval reports 26/38 positives superseded and 0/6 false
   invalidations.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…+ per-call log opt-in

scripts/install_cc_hook.py
  - Use HOOK_KEY as the stable identifier for the SessionStart entry instead
    of a dead string constant. The `name` field on the entry is set from
    HOOK_KEY; the install/uninstall match helpers check it first and fall
    back to the legacy command-string match for entries written by older
    versions of this script.
  - Fix a pre-existing bug surfaced by the new sibling-preservation tests:
    install() and uninstall() now filter at the inner-entry level so a
    wrapper that contains both an operator-added sibling and our entry
    keeps the sibling when ours is refreshed or removed. A wrapper that
    contains only our entry is replaced in-place with the fresh entry
    rather than being kept as an empty wrapper.
  - The install side now refreshes the existing wrapper's entry instead of
    appending a second one, so re-running the script is genuinely a no-op
    even when a sibling was previously added.

engraphis/mcp_http_cli.py
  - Add an opt-in logging.basicConfig that runs only when the operator
    sets ENGRAPHIS_MCP_LOG to a truthy value (1 / true / yes / info / on).
    Default behaviour is silent so the CLI keeps its quiet profile. Existing
    root handlers are never replaced.

CHANGELOG.md
  - Mention the ENGRAPHIS_MCP_LOG opt-in alongside the existing per-call
    INFO log line on engraphis_recall_context, so operators know the
    one env var that turns the logs on.

Tests: 14/14 install_cc_hook + session_start_hook, 22/22 release-infrastructure,
36/36 benchmark-evidence, 4398/39-skip full suite green. Ruff clean. Pyright
clean. Commercial-manifest check clean.
… install/uninstall

install() and uninstall() stripped the wrapper's `matcher` key
because the inner `hooks` list was rebuilt without copying the
wrapper dict first. An operator who added a wrapper-level
SessionStart filter lost that filter on the next install/uninstall.

Two fixes in scripts/install_cc_hook.py:
- _strip_our_entries now shallow-copies the wrapper dict before
  writing the trimmed inner hooks list, so any wrapper-level key
  (matcher, env, cwd, ...) is preserved.
- _refresh_existing_wrappers mutates the existing wrapper dict in
  place (rather than replacing it with a bare {hooks: ...}) so
  the operator's wrapper-level keys survive a reinstall.

Also: the uninstall branch that previously dropped a wrapper
became-empty case now has an explicit pass through the original
wrapper, so the comment and the drop decision are co-located.

Tests in tests/test_install_cc_hook.py: two new regression
tests pin the behavior — test_install_preserves_wrapper_level_metadata
and test_uninstall_preserves_wrapper_level_metadata_with_sibling.

Bench: 8/8 test_install_cc_hook.py pass.
…rt hook

Stateful transports (notably the dashboard /mcp endpoint) issue an
Mcp-Session-Id on initialize and reject subsequent requests that
arrive without it. The standalone hook sent initialize but then
re-issued notifications/initialized and tools/call without the
header, so the dashboard's stateful transport closed the session
between calls.

The hook now:
- reads the Mcp-Session-Id from the initialize response,
- threads it into notifications/initialized and every tools/call
  via a post() session_id parameter, and
- echoes it back if the response sets a new value.

A stateless transport (the standalone mcp_http_cli default) ignores
the header, so the change is fully backward compatible.
Follows the same pattern as tests/e2e/ledger.spec.js: spawns the
dashboard on a dedicated port, drives the slider inputs with
page.locator('#graph-X').fill(value), and reads state from the
diagnostics exposed via page.evaluate(). Useful for catching the
exact failure mode the 4th-pass audit set out to prove (slider
value reaches the engine but produces no visible effect) without
needing the full ledger mock to detect it.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d07b572e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread engraphis/core/resolve.py
Comment on lines +372 to +375
swap_veto = (evidence.heavy_swap
or (evidence.proper_swap and not (marker and evidence.value_swap))
or evidence.env_conflict)
if not swap_veto or temporal_splice or rec.valid_to is not None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nonnumeric configuration corrections

Do not treat every noun-for-noun replacement as proof that two facts coexist. For strong same-subject pairs such as the bundled default branch ... masterdefault branch ... main, heavy_swap becomes true solely because the changed value is nonnumeric, so this veto changes the previous INVALIDATE result to RELATE and leaves both conflicting defaults live. The same regression affects rootadmin and INFODEBUG; all three are labeled corrections in the new corpus, and python -m eval.resolver_reworded_corrections --strict currently fails with them among its misses.

AGENTS.md reference: AGENTS.md:L166-L168

Useful? React with 👍 / 👎.

Comment on lines +84 to +92
response, _ = post(
MCP_URL,
{"jsonrpc": "2.0", "id": rpc_id, "method": method, "params": params},
remaining,
session_id=session_id,
)
if isinstance(response, dict) and "result" in response:
return response["result"], session_id
return None, session_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return the session ID received during initialize

When initialize targets the stateful dashboard /mcp endpoint, post() returns the newly issued Mcp-Session-Id, but rpc() discards it here and returns the input session_id (which is None for initialize). Consequently notifications/initialized and tools/call are still sent without the required header, the server rejects the conversation, and this fail-open hook silently emits no recalled context—the exact stateful-transport scenario this change is intended to support.

Useful? React with 👍 / 👎.

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