Skip to content

mcpx(cli): exec artifact delivery silently overwrites same-named artifacts — applyFrame and fetchArtifacts rename onto the bare sanitized name, unlike Store.Export's never-overwrite rule #1605

Closed

Description

@dezren39

Problem

The mcpx CLI's two exec artifact-delivery paths write incoming artifacts to --artifacts-dir under their sanitized name with no collision handling — a second artifact whose name sanitizes to the same file silently overwrites the first, and both artifacts' Path fields end up pointing at the one surviving file. This contradicts the artifact store's own documented rule: Store.Export resolves collisions by suffixing and never overwrites, because "a script that emits two files called shot.png has produced two files, and silently keeping one of them is data loss" (pkgs/mcpx/internal/artifacts/artifacts.go:548-555, guarded by TestExportNeverOverwrites).

Evidence (verified this run, laptop probe driving the real code)

Probe test in internal/cli (package cli) on a laptop worktree at origin/main 63ba1705; internal/cli/exec.go sha256 292a18a0… and internal/artifacts/name.go sha256 8651feab…, identical to the sandbox clone. Log: goal hidden_files probe-2026-10-28-laneA-mcpxart/out.log.

  1. Sanitize collisions are easy to hit (real artifacts.SanitizeName): "my shot.png" and "my-shot.png" both → my-shot.png; "a/b.png" and "b.png" both → b.png; identical names trivially collide.
  2. Streaming path loses the first body: real App.applyFrame fed two artifacts (id1, id2) both named shot.png with bodies FIRST-BODY / SECOND-BODY → the artifacts dir contains exactly one file, shot.png = "SECOND-BODY", and res.Artifacts[0].Path == res.Artifacts[1].Path. Root cause: FrameArtifactEnd renames the temp file straight onto filepath.Join(eo.ArtifactsDir, artifacts.SanitizeName(name)) (pkgs/mcpx/internal/cli/exec.go:313-321); os.Rename replaces the destination.
  3. Fetched path loses it too: real writeNew(dest, "FIRST-BODY") then writeNew(dest, "SECOND-BODY") → dest holds only SECOND-BODY. fetchArtifacts computes the same bare dest (exec.go:396) and writeNew finishes with os.Rename onto it (exec.go:407-420) — the temp name protects against partial files, not against clobbering a finished one. This also means re-running the same script overwrites the previous run's artifact of the same name in the dir.
  4. Control: the same probe's Store.Export of two shot.png artifacts returns shot.png and shot-1.png — the behaviour the store documents and tests.

Why it matters

mcpx run/exec --artifacts-dir is how scripts hand files back to the caller. Two screenshots, two exports with a path-y or space-y name, or simply a repeat run, silently destroy the first file while the result JSON still lists both artifacts as delivered (with the same Path) — data loss that reports success, in the one subsystem that already wrote down why that must never happen.

Acceptance criteria

  • Both CLI delivery paths (applyFrame's FrameArtifactEnd rename and fetchArtifacts/writeNew) place same-named artifacts at distinct paths, using the store's suffix convention (shot.png, shot-1.png, …) or an equivalent never-overwrite rule shared with uniquePath.
  • Each artifact's Path in the result names the file that actually holds its body; a test in internal/cli mirrors TestExportNeverOverwrites for both delivery paths (two same-named artifacts → two files, both bodies intact, suffix before the extension).
  • Re-running a script into a non-empty --artifacts-dir does not overwrite the previous run's same-named artifact (or, if that is judged intended, the overwrite is explicit in the flag help and the result — not silent).
  • Existing artifact and CLI tests still pass; TestExportNeverOverwrites semantics unchanged.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-28 00:35 CDT
    • Found via: filed this run
    • Task: Triage the mcpx CLI exec artifact-delivery overwrite: both the streaming applyFrame rename and the fetchArtifacts/writeNew path place artifacts on the bare sanitized name, so same-named artifacts silently overwrite each other, contradicting Store.Export's documented never-overwrite rule. Verified this run with a laptop probe driving the real applyFrame/writeNew/SanitizeName/Store.Export code.
    • Expectation: effort/s — done looks like both CLI paths suffixing collisions (shot.png, shot-1.png, …) with result Paths naming the file that holds each body, plus never-overwrite tests in internal/cli mirroring TestExportNeverOverwrites.
    • Links: queue packet packet-1605.json (repo-health-code-pilot, triage); probe + log in the goal's hidden_files/probe-2026-10-28-laneA-mcpxart/
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 56 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: parked
    task: 1849 (openchamber scheduled lane)
    artifact: PR #2950 @ 0230920
    ci: stress job 124486639853 remains queued; other completed checks pass.
    tests: go test ./internal/cli -run 'TestApplyFrame|TestFetchArtifacts' passed in pkgs/mcpx.\nresume: recheck CI and merge only after checks are green and review verified.

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: picked-up (merge decision)
    task: 1849 (repo-health-code-pilot, lane C resume)
    artifact: PR #2950 @ 0230920
    ci-fresh: lint SUCCESS, plugin tests SUCCESS, docs build SUCCESS, Codex CLEAN on head. Stress rerun job 124486639853 FAILED — 10m test timeout with the runnable test TestInlineDaemonCreatesNoSocketUntilAsked stuck in elicit.Open/sqlite Ping (inlinedaemon_test.go:194 via daemon/server.go, elicit/store.go). PR diff touches only internal/artifacts/name.go, internal/cli/exec.go, internal/cli/exec_artifacts_test.go — no daemon/elicit/settings code, no changed symbol in the dump. This is the second consecutive stress timeout in a different unrelated test (previous: TestEverySettingFlagReachesTheResolvedValue), i.e. stress-mode (race/shuffle/repeat) cumulative slowness on a backlogged runner.
    verification: on the laptop at head 0230920, go test ./internal/artifacts/ ./internal/cli/ -count=1 passes both packages; the timed-out test itself passes in <1s outside stress mode.
    action: merging PR #2950 on the remaining green checks + Codex-clean per the parked resume condition and the standing merge directive; completing task fixed_merged.

  10. added a commit that references this issue on Oct 31, 2026
  11. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: done
    task: 1849 (repo-health-code-pilot, lane C)
    artifact: PR #2950 MERGED d81c27f; issue #1605 closed by the merge.
    tests: laptop go test ./internal/artifacts/ ./internal/cli/ -count=1 — both packages pass (incl. the stress-timed-out TestInlineDaemonCreatesNoSocketUntilAsked, <1s outside stress mode). CI lint/plugin/docs SUCCESS; Codex clean on head. Stress job dispositioned as pre-existing stress-mode slowness (two consecutive timeouts in two different tests untouched by this diff).

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

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions