Skip to content

watch liveness check resolves relative source identities against process CWD instead of watch root #4896

Closed

Description

@Ethan-Ethan-Ethan

Version 0.9.93, macOS, daily job under launchd.

SYMPTOM: incremental code rebuild refuses to overwrite (fail-closed, saved semantic nodes missing) only when run under the scheduler; the same rebuild from a shell rooted at the watch root succeeds.

ROOT CAUSE: the alive check does Path(identity).exists() where identity is a watch-root-relative source path. Under launchd with WorkingDirectory set to the home dir, existing files (e.g. memory slash plan docs) resolve to a nonexistent absolute path, are judged deleted, their semantic nodes are evicted, and fail-closed correctly refuses the write. Manual runs succeed only because the shell CWD happens to equal the watch root.

SUGGESTED FIX: resolve against the watch root at the call site (watch_root.joinpath(relative_id).exists()), not bare Path(relative_id).exists(). Prefer this over storing absolute identities (keeps watches relocatable, avoids leaking home-dir layout into stored state).

REGRESSION TEST: parametrize CWD (project tmp dir vs home dir), assert an existing file stays alive and a truly-deleted file still trips refusal.

Activity

  1. github-actions commented on Oct 31, 2026

    @github-actions

    Thanks for opening this issue, @Ethan-Ethan-Ethan. A maintainer will take a look soon.

    If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.

    To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.

  2. Jitendarmahara commented on Oct 31, 2026

    @Jitendarmahara
    Contributor

    I tried to reproduce this on the current v8 (0.9.99) and it doesn't seem to occur there anymore.

    On current v8 the watch/update entry resolves the watch root to an absolute path before building source identities (watch_root = watch_path.resolve() in watch.py), and project_root is resolved too. Because absolute_identity() then builds each identity from an absolute root, os.path.abspath(...) is effectively a no-op and the identity no longer depends on the process CWD — so the Path(identity).exists() liveness check resolves the same whether it runs from the watch root or elsewhere.

    In a few local runs (a doc/source present on disk, reconcile run from a CWD different from the watch root) the node was preserved either way. This looks like it was addressed between 0.9.93 and 0.9.99.

    @Ethan-Ethan-Ethan — could you confirm whether you still see the eviction on 0.9.99? If it still reproduces there, a minimal setup (watch root + the scheduler CWD) would help pin down the remaining path.

  3. Ethan-Ethan-Ethan commented on Nov 1, 2026

    @Ethan-Ethan-Ethan
    ContributorAuthor

    Thanks for investigating this and for checking the path-resolution code.

    I followed up on my macOS/launchd setup on 2026-11-01.

    Verified local configuration

    • Production Graphify is still graphifyy==0.9.93 (Python 3.11).
    • Incremental job: com.ethan.graphify.update.
    • Watch root passed to the incremental rebuild: /Users/ethan/Documents (absolute path).
    • Current launchd WorkingDirectory for that job: /Users/ethan/Documents (also confirmed using the running process's CWD).
    • Stored .graphify_root for the production graph: /Users/ethan/Documents (absolute path).
    • A different semantic-once job has WorkingDirectory=/Users/ethan. My original report described a scheduler CWD of the home directory; I cannot independently verify that this was the historical CWD of the failing incremental job. I want to correct that distinction.

    v0.9.99 verification (isolated, no production upgrade)

    I installed graphifyy==0.9.99 separately using uv, with Python 3.11, and ran the same isolated fixture against both 0.9.93 and 0.9.99. The fixture uses:

    • An absolute temporary watch root with an absolute .graphify_root marker.
    • A real Markdown source under the representative relative path memory/plan/archive/*.md, plus a saved semantic node (with _origin=semantic, file_type=code, and a watch-root-relative source_file), modeled after the type of entries seen in my production graph.
    • A changed app.py to trigger _rebuild_code(..., changed_paths=[...]).
    • CWD varied between the watch root and /Users/ethan; source file varied between present and genuinely deleted.
    • Both no_cluster=True and the normal clustered rebuild path (no_cluster=False).

    Results: all four present/deleted × CWD cases passed for each version in both rebuild modes (8/8 per version). A present semantic source remained in the graph; a truly deleted source was removed. I did not reproduce an incorrect CWD-dependent eviction or a fail-closed warning in these fixtures, including on 0.9.93.

    I also checked two saved semantic source_file paths implicated by a current 0.9.93 fail-closed warning: the corresponding files are present when anchored under /Users/ethan/Documents, but not if erroneously anchored under /Users/ethan. This shows why anchoring is important, but does not establish that CWD caused the warning. The current warning is real; its cause remains unisolated.

    Conclusion: I cannot reproduce the reported CWD/liveness failure on v0.9.99 with the tested configurations. Because v0.9.93 passes the same tests, I also cannot claim that v0.9.99 fixed the original failure. I have not run v0.9.99 against the live ~279 MB graph or changed the production scheduler, so the original production-path regression remains unverified.

    I'll leave the issue open rather than mark it fixed based on a non-discriminating fixture. A failing minimal fixture, if obtained, would be the next useful evidence. Thanks again for the careful triage.

  4. Ethan-Ethan-Ethan commented on Nov 1, 2026

    @Ethan-Ethan-Ethan
    ContributorAuthor

    Follow-up / correction to my previous comment: I now have a discriminating, fully isolated reproduction of the exact fail-closed warning. My earlier basic fixtures missed an important ingredient: an AST file-self node with the same label as the saved semantic nodes.

    Minimal setup

    • macOS, Python 3.11; Graphify versions 0.9.93 and 0.9.99 tested separately.
    • Absolute watch root: $TMP/watch, with an absolute .graphify_root marker.
    • Scheduler CWDs tested independently: $TMP/watch and $HOME.
    • A previously built graphify-out/graph.json containing two distinct saved semantic nodes (_origin=semantic, file_type=code, label=validate_canonical_load.py) from different existing sources such as memory/plan/archive/a.md and memory/plan/archive/b.yaml.
    • Create/update a single AST source source-snapshots/pins/validate_canonical_load.py, then call graphify.watch._rebuild_code(root, changed_paths=[ast_file], no_cluster=False, block_on_lock=True). No force, no production data.

    Full rebuild results (both CWDs):

    • 0.9.93: rebuild returns False, graph unchanged, and emits Refusing to overwrite — saved semantic node(s) are missing from the new graph. With my synthetic fixture using the two observed saved IDs, the warning names the same two IDs as the production warning.
    • 0.9.99: rebuild returns True and writes a smaller graph, but the two original semantic node IDs are absent after the rebuild.
    • no_cluster=True: both versions preserve the semantic nodes. The difference emerges in the clustered build_from_json path, independent of CWD.

    Likely mechanism: build.py's Pass 2b (#3344) folds semantic nodes into a unique AST file-self node based on filename/label alone, even when their source_file values differ. The 0.9.93 shrink guard interprets the disappearance as missing saved semantic nodes. In 0.9.99, _lost_saved_semantic_nodes recognizes this label-only fold and no longer flags it. An isolated check also shows that distinct rationale strings on the semantic nodes are not retained on the surviving AST node (including with build_from_json(..., dedup=False)).

    The live graph's two saved source_file values really exist, are under the correct absolute watch root, and are not excluded by the currently active ignore policy. Separately, one non-ignored, newer validate_canonical_load.py file is present in the watched tree. These facts make the dedup/shrink-guard interaction a credible explanation for the production warning, although I have not run a full v0.9.99 rebuild against the live graph.

    So my original CWD root-cause assertion should be treated as unconfirmed. The reproducible issue is a cross-source semantic-fold / provenance-preservation problem, not a demonstrated CWD-dependent liveness error. I have not upgraded the production instance or modified its graph.

    Could a maintainer clarify whether the label-only fold is expected to discard distinct semantic provenance/rationale? If it is intended, a regression test should specify how that information survives; if it is not, it may need a narrower fold predicate before the 0.9.99 behavior is considered safe for existing graphs. I would not mark this production data-integrity concern as verified fixed yet.

  5. Ethan-Ethan-Ethan commented on Nov 1, 2026

    @Ethan-Ethan-Ethan
    ContributorAuthor

    Implementation follow-up: I opened PR #4927 to address the newly isolated cross-source semantic-provenance loss: #4927

    The PR is based on upstream v8 at the v0.9.100 release commit. Instead of creating a parallel storage mechanism, it narrows the existing Pass 2b label-only AST file-node fold to evidence-free mentions; distinct semantic records with source-specific rationale or attribution remain separate. The saved-semantic shrink guard mirrors the same rule.

    The isolated regression demonstrates that unpatched 0.9.100 writes a graph without the two original semantic IDs after a clustered rebuild (both CWDs), while the candidate keeps both IDs and their explanations across a cold serialize/reload. Regular evidence-free mention folding still passes.

    Validation: 386/386 scoped tests, additional 20/20 targeted tests, ruff check . PASS; broader test execution reached 6,268 passed before an unrelated provider-registry assertion that also reproduces on the unpatched upstream wheel. Full details and limitations are in the PR.

    No production upgrade, graph overwrite, --force, or launchd change was performed. This demonstrates a separate issue from the originally suspected CWD liveness bug; I would keep #4896 open until upstream reviews the provenance-preservation behavior.

  6. safishamsi commented on Nov 2, 2026

    @safishamsi
    Member

    Fixed in 0.9.101 (published to PyPI) via #4927. The confirmed root cause was a cross-source semantic fold in the clustered build path (a mention remapped into an AST file-self node by label alone, dropping independent provenance), not the original CWD-liveness theory. A mention carrying its own provenance (rationale, source_url, author, contributor) is now kept distinct, while ordinary INFERRED mention ghosts still fold so graphs do not accumulate duplicates. The related AST-id-collision facet in the reconcile path shipped alongside via #4950.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions