Skip to content

fix: repair orphaned pending skill versions stuck after publish - #3401

Open
trippyogi wants to merge 19 commits into
openclaw:mainfrom
trippyogi:fix/3349-publish-version-projection
Open

fix: repair orphaned pending skill versions stuck after publish#3401
trippyogi wants to merge 19 commits into
openclaw:mainfrom
trippyogi:fix/3349-publish-version-projection

Conversation

@trippyogi

Copy link
Copy Markdown

Closes #3349

Related: #3353 (scanner retry cap only — explicitly not this defect)

What Problem This Solves

Fixes an issue where clawhub skill publish could report success and reserve a version while that version never appeared in latest, the version index, or tag pointers. Publishers then hit a dead end: metadata reads 404, files remain fetchable by explicit version, and republish is rejected as a duplicate.

Why This Change Was Made

Staged skill publish inserts a pending skillVersion first and only projects it onto the skill document during async finalization. If finalization never completes, the version stays orphaned forever.

This change:

  • Caps consecutive transient finalization failures so attempts surface as failed instead of looping forever
  • Adds an admin-gated repair path (maintenance:repairOrphanedPendingSkillVersion + sweep) that re-runs finalization from the version's captured insert args, refuses while a live attempt still owns the version, and force-closes the original attempt afterward
  • Surfaces pending / blocked / stuck-failed publication state to authenticated owners (including org publisher scope) instead of a bare 404, without leaking existence to other callers

Non-goals: does not change the normal happy-path publish projection; does not auto-run repair in production without an operator.

User Impact

  • Owners of stuck pending versions get a clear diagnostic (423 in-flight, 409 after terminal finalize failure) instead of a silent 404
  • Operators can repair orphaned pending versions without deleting the skill or forcing a version bump
  • Publishers are less likely to be permanently blocked by a half-written publish

Evidence

Focused tests (local):

  • bunx vitest run convex/httpApiV1.handlers.test.ts -t "3349" — 5 passed
  • bunx vitest run convex/publishAttempts.test.ts -t "findActiveSkillPublishAttempt|3349|orphaned|finalizationFailure|scanner retries" — 11 passed

Bugbot (branch vs main): fixed findings before draft —

  1. Org publisher owners missed pending diagnostic → publisher-scope auth
  2. Terminal-failed finalize still said "re-check shortly" → 409 + repair hint
  3. checkFailureCount treated as abandoned → only finalizationFailureCount abandons grace (scanner retries stay live)

Note: #3353 merged earlier and bounds prepublication scanner retries; it does not close #3349 (called out in that PR body as a separate post-finalization consistency defect).

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@trippyogi is attempting to deploy a commit to the OpenClaw Foundation Team on Vercel.

A member of the Team first needs to authorize it.

trippyogi added a commit to trippyogi/clawhub that referenced this pull request Aug 4, 2026
…air flow

- releaseFinalizationClaimPatch (publishAttempts.ts) now caps
  consecutive finalization failures only for kind==="skill". Package
  attempts have no repair path yet, so they retry unbounded instead of
  becoming a permanent orphan.

- isActiveAttemptLive (publishAttempts.ts) no longer abandons a
  non-terminal attempt just because finalizationFailureCount > 0. It
  now checks staleness against updatedAt (falling back to createdAt)
  using ACTIVE_ATTEMPT_RETRYABLE_STALE_MS for attempts with retry
  activity, or ACTIVE_ATTEMPT_UNCLAIMED_GRACE_MS for zero-failure
  attempts, so a below-cap dispatcher retry (or a long pending_checks
  phase that just transitioned cleanly) is not raced by manual repair.
  Added findActiveSkillPublishAttemptByIdInternal so the repair
  pre-check can look up a known publishAttemptId directly instead of
  only scanning by slug/version.

- repairOrphanedPendingSkillVersionHandler (maintenance.ts) now
  publishes the pending version and closes the orphaned attempt
  atomically via a new publishPendingVersionAndCloseAttemptInternal
  mutation (skills.ts), closing before follow-ups are scheduled. This
  removes the publish-before-close race where a crash between publish
  and close left the version published but the attempt reclaimable.
  Also handles the already-published-but-still-open-attempt retry
  case as an attempt-cleanup path.

- skillsGetRouterV1Handler exact-version path (skillsV1.ts) falls back
  to an owner-visible pending-publication diagnostic via
  getSkillBySlugInternal + describeOwnerVisibleSkillVersionState when
  the public getBySlug lookup misses entirely, so a brand-new hidden
  skill (pending its first publish) no longer 404s for its owner.

Tests added/updated in publishAttempts.test.ts, maintenance.test.ts,
and httpApiV1.handlers.test.ts for all four findings.
@trippyogi
trippyogi marked this pull request as ready for review August 4, 2026 21:57
@trippyogi
trippyogi requested review from a team and Patrick-Erichsen as code owners August 4, 2026 21:57
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Aug 5, 2026
@clawsweeper

clawsweeper Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed August 20, 2026, 12:44 AM ET / 04:44 UTC.

ClawSweeper review

What this changes

The PR bounds failed skill finalization retries, adds admin recovery for orphaned pending skill versions, and gives authorized owners actionable publication-state diagnostics.

Merge readiness

Blocked until stronger real behavior proof is added - 3 items remain

Keep this PR open: the current recovery design addresses the prior maintainer blockers, but it still needs inspectable real after-fix proof from the current head before merge.

Priority: P1
Reviewed head: 57bfb9bdec1e0ec6dbb2e7884c291ad6e1c935d7

Review scores

Measure Result What it means
Overall readiness 🦪 silver shellfish (2/6) The implementation and regression coverage look solid, but current-head real behavior proof remains a merge gate.
Proof confidence 🦪 silver shellfish (2/6) Needs stronger real behavior proof before merge: The discussion describes earlier real runs, but no inspectable after-fix artifact or redacted transcript proves the current PR head's behavior; add current-head evidence and redact private deployment details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: The discussion describes earlier real runs, but no inspectable after-fix artifact or redacted transcript proves the current PR head's behavior; add current-head evidence and redact private deployment details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 6 items Security eligibility gate: Recovery requires both recorded TruffleHog and ClawScan statuses to be clean before any eligible attempt status can be repaired.
Atomic recovery and audit: The recovery mutation rechecks the attempt, publishes and closes it transactionally, schedules required security follow-ups, clears staged data, and writes an audit record in the same mutation.
Owner diagnostic alignment: The owner-visible 409 repair message is conditional on the same repair-eligibility predicate, avoiding a claim that incomplete checks are repairable.
Findings None None.
Security None None.

Live Verification

Command: bunx vitest run convex/publishAttempts.runtime.test.ts -t "refuses CLEAN-but-ClawScan-pending repair"

Result: PASS (completed)

bunx vitest run convex/publishAttempts.runtime.test.ts -t "refuses CLEAN-but-ClawScan-pending repair"
runner@runnervm76f27:/tmp/clawsweeper-live-proof-3401-GVnvpE/target$ bunx vitest run convex/publishAttempts.runtime.test.ts -t "refuses CLEAN-but-ClawScan-pendi
ng repair"

 RUN  v4.1.10 /tmp/clawsweeper-live-proof-3401-GVnvpE/target

bunx vitest run convex/publishAttempts.runtime.test.ts -t "refuses CLEAN-but-ClawScan-pending repair"
 ✓ convex/publishAttempts.runtime.test.ts (4 tests | 3 skipped) 654ms
     ✓ refuses CLEAN-but-ClawScan-pending repair, then publishes after both checks are clean (#3466)  653ms

 Test Files  1 passed (1)
      Tests  1 passed | 3 skipped (4)
   Start at  04:45:22
   Duration  2.26s (transform 1.04s, setup 33ms, import 1.31s, tests 654ms, environment 63ms)

stderr | file:/tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/convex-test/dist/index.js:1187:41
Error when running scheduled function vt:scanWithVirusTotal Error [EnvironmentTeardownError]: Cannot load '/convex/lib/packageArtifacts.ts' imported from /tmp/c
lawsweeper-live-proof-3401-GVnvpE/target/convex/vt.ts after the environment was torn down. This is not a bug in Vitest. The last recorded callstack:
- /convex/lib/packageArtifacts.ts
- /tmp/clawsweeper-live-proof-3401-GVnvpE/target/convex/vt.ts
- /tmp/clawsweeper-live-proof-3401-GVnvpE/target/convex/publishAttempts.runtime.test.ts
    at reviveInvokeError (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:546:14)
    at Object.invoke (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:562:33)
    at processTicksAndRejections (node:internal/process/task_queues:104:5)
    at VitestModuleRunner.getModuleInformation (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-run
ner.js:1199:7)
    at request (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:1216:83)
    at /tmp/clawsweeper-live-proof-3401-GVnvpE/target/convex/vt.ts:5:1
    at VitestModuleEvaluator._runInlinedModule (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/dist/module-evaluator.js:206:4)
    at VitestModuleRunner.directRequest (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:
1259:59)
    at VitestModuleRunner.cachedRequest (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:
1166:73)
    at file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/convex-test/dist/index.js:1366:16 {
  runnerError: Error: RunnerError
      at reviveInvokeError (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:547:64)
      at Object.invoke (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:562:33)
      at processTicksAndRejections (node:internal/process/task_queues:104:5)
      at VitestModuleRunner.getModuleInformation (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-r
unner.js:1199:7)
      at request (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.js:1216:83)
      at /tmp/clawsweeper-live-proof-3401-GVnvpE/target/convex/vt.ts:5:1
      at VitestModuleEvaluator._runInlinedModule (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/dist/module-evaluator.js:206:4)
      at VitestModuleRunner.directRequest (file:///tmp/clawsweeper-live-proof-3401-GVnvpE/target/node_modules/vitest/node_modules/vite/dist/node/module-runner.j
s:1259:59)
      at VitestModuleRunner.cachedRequest (file:///tmp/clawsw
… output truncated …

Assertions:

  • PASS expect_output: refuses CLEAN-but-ClawScan-pending repair, then publishes after both checks are clean

How this fits together

ClawHub stages a skill version before asynchronously projecting it into the public skill index, latest pointer, tags, and security follow-ups. This change governs how failed finalization is surfaced and safely recovered without exposing pending versions to other callers.

flowchart LR
  A[Skill publish request] --> B[Pending skill version]
  B --> C[Recorded security checks]
  C --> D[Async finalization]
  D --> E[Public index and latest tags]
  D --> F[Owner-only diagnostics]
  D --> G[Admin orphan recovery]
Loading

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: The discussion describes earlier real runs, but no inspectable after-fix artifact or redacted transcript proves the current PR head's behavior; add current-head evidence and redact private deployment details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • Resolve merge risk (P1) - The PR changes a security-gated publication recovery boundary and terminal retry behavior; the current head needs an inspectable redacted deployment or runtime trace before maintainers can safely rely on it.
  • Complete next step (P2) - Needs contributor-supplied, inspectable current-head real behavior proof before a human merge decision; this is not an automation repair task.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch size 12 files, +3,993 / -252 lines The recovery spans publication, authorization diagnostics, persistence, and tests, so focused runtime proof matters before merge.
Production versus test growth production +1,344 / -252, tests +2,649 The substantial regression coverage supports the design, while the production recovery boundary warrants direct behavior evidence.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #3349
Summary: This PR is the candidate fix for the canonical stranded skill-version publication defect.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Add current-head recovery proof (recommended)
    Attach a redacted current-head Convex deployment or runtime transcript showing blocked incomplete checks followed by an audited successful repair after both checks are clean.

Technical review

Best possible solution:

Land the bounded recovery path only after a current-head run proves that incomplete checks remain blocked and that a clean stranded version is atomically projected, audited, and made publicly readable.

Do we have a high-confidence way to reproduce the issue?

Yes for source reproduction: the current Convex-test fixture creates the pending-version and incomplete-check state, then verifies rejection before the clean-check repair path. This review did not execute the test.

Is this the best way to solve the issue?

Yes: reusing the existing finalizer from the stored pending payload, while requiring recorded clean checks and atomically closing the attempt, is the narrowest maintainable recovery path.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d6a8c68501d4.

Labels

Label changes:

  • remove merge-risk: 🚨 availability: Current PR review merge-risk labels are merge-risk: 🚨 compatibility, merge-risk: 🚨 security-boundary.

Label justifications:

  • P1: A successful-looking publish can strand a real publisher's version and block republishing.
  • merge-risk: 🚨 compatibility: Five finalization failures now transition an existing publish workflow into an operator-recovery path.
  • merge-risk: 🚨 security-boundary: The new administrator recovery path can make a previously pending version public and must preserve recorded scan gates.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦪 silver shellfish and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The discussion describes earlier real runs, but no inspectable after-fix artifact or redacted transcript proves the current PR head's behavior; add current-head evidence and redact private deployment details. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

What I checked:

  • Security eligibility gate: Recovery requires both recorded TruffleHog and ClawScan statuses to be clean before any eligible attempt status can be repaired. (convex/publishAttempts.ts:579, 57bfb9bdec1e)
  • Atomic recovery and audit: The recovery mutation rechecks the attempt, publishes and closes it transactionally, schedules required security follow-ups, clears staged data, and writes an audit record in the same mutation. (convex/skills.ts:13805, 57bfb9bdec1e)
  • Owner diagnostic alignment: The owner-visible 409 repair message is conditional on the same repair-eligibility predicate, avoiding a claim that incomplete checks are repairable. (convex/httpApiV1/skillsV1.ts:1722, 57bfb9bdec1e)
  • Runtime regression coverage: The Convex-test regression first rejects clean-but-incomplete checks, then asserts publication, latest/tag projection, attempt closure, staged-data clearing, and audit output after both checks are clean. (convex/publishAttempts.runtime.test.ts:195, 57bfb9bdec1e)
  • Current-head provenance: The checked-out PR head is 57bfb9b; its final commit removes the throwaway proof harness, so the earlier claimed proof artifact is not present at the current head. (57bfb9bdec1e)
  • Maintainer validation: A collaborator's review records current-main red/green validation and identifies the three prior blockers; the current source contains the corresponding check gate, transactional audit, and owner-diagnostic fixes.

Likely related people:

  • Patrick-Erichsen: Authored the related merged scanner-retry repair, contributed multiple recovery hardening commits here, and supplied the current-main validation review. (role: recent area contributor and reviewer; confidence: high; commits: 50c4ffc1a027, ed9c74f976ed, f32279946396; files: convex/publishAttempts.ts, convex/skills.ts, convex/maintenance.ts)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Attach a redacted current-head runtime or deployment trace showing incomplete checks blocked, then successful repair after both checks are clean.
  • Update the PR body with that evidence; if a fresh review does not trigger, ask a maintainer to comment @clawsweeper re-review.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (24 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-05T22:18:16.004Z sha 91cd641 :: needs real behavior proof before merge. :: [P2] Skip follow-ups after a late finalizer claim | [P2] Clear the pending snapshot on every successful repair | [P2] Pass liveness time into active-attempt queries | [P2] Pass the sweep cutoff into the candidate query | [P3] Record the orphan-repair invariant in a spec
  • reviewed 2026-08-05T23:37:15.417Z sha 91cd641 :: needs real behavior proof before merge. :: [P2] Do not enqueue follow-ups after losing finalizer ownership | [P2] Clear staged publish data after repairing a failed attempt | [P2] Pass a fixed cutoff into the sweep query | [P3] Document the orphaned-publish recovery invariant
  • reviewed 2026-08-05T23:45:11.647Z sha 91cd641 :: needs real behavior proof before merge. :: [P2] Skip repair follow-ups after a finalizer claim wins | [P2] Clear staged data for every repaired version | [P2] Pass a fixed sweep cutoff into the query | [P3] Record the recovery invariant in specs
  • reviewed 2026-08-06T02:10:43.969Z sha 91cd641 :: needs real behavior proof before merge. :: [P2] Avoid duplicate follow-ups after a finalizer wins the race | [P2] Clear staged publication data on terminal-attempt repair | [P2] Use one cutoff for the entire repair sweep | [P3] Record the orphaned-publish recovery invariant in specs
  • reviewed 2026-08-06T03:57:37.371Z sha ad0b5f7 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-06T13:35:48.066Z sha b149533 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-06T23:37:29.747Z sha 778ecf6 :: needs real behavior proof before merge. :: none
  • reviewed 2026-08-20T04:28:47.871Z sha 8c312d8 :: needs real behavior proof before merge. :: none

@Patrick-Erichsen Patrick-Erichsen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This adds owner diagnostics and an operator repair for skill versions stranded in staged publication. The repair is not safe yet because a terminally failed publish attempt is not actually closed/cleared by the helper path.

LOC: +2087/-26 (9 files)

Finding: when the linked attempt is already failed, the close helper returns without clearing the persisted pendingPublication snapshot. The version can become public while its record remains internally staged. Add the cap-exhausted regression case and real Convex repair proof.

Best-fix verdict: too broad in its current form. Reusing the finalization contract is sound, but the atomic repair must explicitly handle terminal attempts before this operator path is safe.

Alternatives considered: a second cleanup mutation would restore the crash window; the better shape is one transaction that publishes and clears/closes every supported attempt state.

Code read: convex/maintenance.ts, convex/skills.ts, convex/lib/skillPublish.ts, publish-attempt helpers, HTTP owner diagnostics, and adjacent tests.

Remaining uncertainty: no after-fix staged publish/repair has been exercised on a real Convex deployment, and the branch is currently conflicted with main.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. merge-risk: 🚨 automation 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. labels Aug 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale due to inactivity.
Please update it or it will be closed.

@github-actions github-actions Bot added the stale label Aug 19, 2026
@Patrick-Erichsen

Copy link
Copy Markdown
Collaborator

Independent #3466 validation confirms this PR owns the same stranded staged-publication lifecycle as #3349, so I am not opening a duplicate fix PR.

Validated PR head 778ecf691de071db67c686c3ce32c3153031820d on a local merge with current origin/main d6a8c68501d4244664ce443e686c24292f607471.

Runtime red/green proof used a real convex-test database fixture with:

  • a first-version skill hidden as pending.publication while its moderation verdict is clean
  • a retained pending skillVersion with stored SKILL.md and publication args
  • the exact version-scoped publishAttempt
  • TruffleHog clean while ClawScan is still pending

Current main fails because no recovery action exists. This branch correctly refuses recovery while the original ClawScan check is incomplete, then publishes and projects latest/tags only after both recorded attempt checks are clean.

Three blockers remain before this is safe to land:

  1. P1 — finalized attempts bypass the required check evidence. isAttemptEligibleForOrphanRepair returns true for status === "finalized" before requiring both recorded checks to be clean. A retained pending version associated with an inconsistent finalized row can therefore cross the recovery boundary without affirmative TruffleHog + ClawScan evidence. Require clean recorded checks for every repair-eligible status. See convex/publishAttempts.ts around lines 580–588.
  2. P1 — recovery is not durably audited. The public action authenticates an admin but discards the actor identity; neither the internal action nor the atomic publish/attempt-close mutation writes auditLogs. Persist actor, version, attempt, prior state, and result transactionally with the recovery. See convex/maintenance.ts around lines 3977–3989.
  3. P2 — owner status can claim an unrepairable finalization failure. Any failed attempt gets the 409 “finalization failed; operator can repair” message, including failed/incomplete pre-publication checks that recovery correctly rejects. Base the owner message on recorded check completion plus finalization failure state. See convex/httpApiV1/skillsV1.ts around lines 1717–1728.

Validation on the current-main merge:

The PR is also currently conflicting with main. Recommended next step: rebase/merge current main into #3401, add the runtime regression, fix the three blockers above, rerun the same gates, and attach inspectable real Convex deployment proof. No production repair should run before that lands.

@github-actions github-actions Bot removed the stale label Aug 20, 2026
trippyogi and others added 18 commits August 19, 2026 20:47
…claw#3349)

A staged skill publish inserts the skillVersion as pending up front and
only projects it onto the skill's latest/tags/version-index once the
async publishAttempts finalize step succeeds. If that worker never runs
again (crashed action, exhausted retries), the version was left
orphaned forever: files exist, the duplicate-version guard blocks a
retry, but every metadata read 404s.

- Cap consecutive transient finalization failures
  (finalizationFailureCount, publishAttempts.ts) so a stuck attempt
  surfaces as failed instead of looping forever.
- Add an admin-gated repair path (maintenance.ts) that re-runs
  finalization for one orphaned pending version, or sweeps the whole
  table in age-bounded batches, from the exact insert args captured on
  the version at publish time. Refuses to act while a live attempt
  still owns the version, and force-closes the original attempt row
  afterward so the normal dispatcher can't re-claim it and re-run
  followups.
- Surface pending/blocked publication state to the authenticated owner
  on version reads instead of a bare 404, without leaking it to other
  callers (skillsV1.ts).
- Authorize owner-visible pending/blocked reads via publisher scope
- Return 409 when linked finalize attempt already failed
- Do not treat checkFailureCount as abandoned (scanner retry gaps)
…air flow

- releaseFinalizationClaimPatch (publishAttempts.ts) now caps
  consecutive finalization failures only for kind==="skill". Package
  attempts have no repair path yet, so they retry unbounded instead of
  becoming a permanent orphan.

- isActiveAttemptLive (publishAttempts.ts) no longer abandons a
  non-terminal attempt just because finalizationFailureCount > 0. It
  now checks staleness against updatedAt (falling back to createdAt)
  using ACTIVE_ATTEMPT_RETRYABLE_STALE_MS for attempts with retry
  activity, or ACTIVE_ATTEMPT_UNCLAIMED_GRACE_MS for zero-failure
  attempts, so a below-cap dispatcher retry (or a long pending_checks
  phase that just transitioned cleanly) is not raced by manual repair.
  Added findActiveSkillPublishAttemptByIdInternal so the repair
  pre-check can look up a known publishAttemptId directly instead of
  only scanning by slug/version.

- repairOrphanedPendingSkillVersionHandler (maintenance.ts) now
  publishes the pending version and closes the orphaned attempt
  atomically via a new publishPendingVersionAndCloseAttemptInternal
  mutation (skills.ts), closing before follow-ups are scheduled. This
  removes the publish-before-close race where a crash between publish
  and close left the version published but the attempt reclaimable.
  Also handles the already-published-but-still-open-attempt retry
  case as an attempt-cleanup path.

- skillsGetRouterV1Handler exact-version path (skillsV1.ts) falls back
  to an owner-visible pending-publication diagnostic via
  getSkillBySlugInternal + describeOwnerVisibleSkillVersionState when
  the public getBySlug lookup misses entirely, so a brand-new hidden
  skill (pending its first publish) no longer 404s for its owner.

Tests added/updated in publishAttempts.test.ts, maintenance.test.ts,
and httpApiV1.handlers.test.ts for all four findings.
Cap-exhausted failed attempts keep a fresh updatedAt and failure count,
so isActiveAttemptLive blocked orphan repair for ~20m even though the
dispatcher cannot reclaim them. Gate the live check on active statuses
only, matching the direct-ID precheck, and cover the failed+cap path.
Refresh stale take(10) comments to match the paginated fallback, and
remove attemptCloseWarning from the successful repair path now that
claim-active/version-mismatch throw and roll back in the same mutation.
Require clean TruffleHog+ClawScan before any repair-eligible status, write a transactional recovery audit with actor, and only claim operator repair when the failed attempt is actually eligible. Add openclaw#3466 CLEAN-but-pending runtime coverage and a throwaway-dev proof harness.
@trippyogi
trippyogi force-pushed the fix/3349-publish-version-projection branch from 778ecf6 to 8c312d8 Compare August 20, 2026 04:23
Remove throwaway proof seed/script and restore Convex generated
files to match origin/main so the openclaw#3401 tip stays publishable.
@clawsweeper clawsweeper Bot removed the merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. label Aug 20, 2026
@trippyogi

Copy link
Copy Markdown
Author

@Patrick-Erichsen addressed the three blockers from your latest pass on cleaned head 57bfb9bd (base d6a8c685).

  1. Finalized attempts now require clean prepublication checks. Repair eligibility requires recorded TruffleHog + ClawScan checks to be clean for every eligible status, including finalized. Added coverage for an inconsistent finalized attempt with incomplete checks; recovery stays blocked with no projection, pending-snapshot cleanup, followups, or recovery audit.

  2. Recovery is transactionally audited. The authenticated admin actor is carried into the atomic recovery mutation. Successful recovery writes actor, version, attempt, prior publication/attempt state, and result to auditLogs in the same transaction as publication / attempt cleanup. Dry-run and rejected recovery paths do not write a recovery audit.

  3. Owner diagnostics now match actual repairability. The owner-only 409 operator-repair message is only returned for a failed attempt with clean recorded checks and a repairable finalization failure. Failed/incomplete scanner attempts no longer claim orphan repair is available. Nonowners still get the generic 404.

Also added the #3466 CLEAN-but-pending.publication runtime regression: a clean parent moderation verdict alone does not cross the recovery boundary while the recorded ClawScan check is incomplete.

Convex proof

Real dev deployment: dev:blissful-turtle-103
Proofed tip: 8c312d8b5df88a60e384368656c0276da93269de
Final cleaned head: 57bfb9bdec1e0ec6dbb2e7884c291ad6e1c935d7
Base: d6a8c68501d4244664ce443e686c24292f607471
Captured: 2026-08-20T04:14:40.193Z

57bfb9bd contains no functional #3401 changes from the proofed tip. It only removes the temporary dev proof harness and restores generated Convex files to current-main output.

No production deployment or repair was run.

1. Finalized + incomplete checks stay blocked
{
  "pendingChecks": {
    "reason": "attempt-checks-incomplete",
    "repaired": false,
    "status": "pending_checks"
  },
  "finalizedWithIncompleteChecks": {
    "repair": {
      "reason": "attempt-checks-incomplete",
      "repaired": false,
      "status": "finalized"
    },
    "attempt": {
      "status": "finalized",
      "checks": {
        "trufflehog": "clean",
        "clawscan": "pending"
      },
      "repairEligible": false
    },
    "version": {
      "version": "1.0.1",
      "publicationStatus": "pending",
      "hasPendingPublicationSnapshot": true
    },
    "recoveryAudits": []
  }
}

This directly covers the finalized-attempt bypass: no projection, no cleanup, and no recovery audit when both recorded checks are not clean.

2. Capped clean finalization failure → successful repair

Before:

{
  "skill": {
    "latestVersion": "1.0.0",
    "versionsStat": 1
  },
  "version": {
    "version": "1.0.1",
    "publicationStatus": "pending",
    "hasPendingPublicationSnapshot": true
  },
  "attempt": {
    "status": "failed",
    "finalizationFailureCount": 5,
    "checks": {
      "trufflehog": "clean",
      "clawscan": "clean"
    },
    "repairEligible": true,
    "hasResult": false
  }
}

Repair:

{
  "repaired": true,
  "slug": "orphan-repair-proof",
  "version": "1.0.1",
  "result": {
    "versionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g",
    "publicationStatus": "published"
  }
}

After:

{
  "skill": {
    "latestVersion": "1.0.1",
    "latestVersionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g",
    "latestTagVersionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g",
    "versionsStat": 2
  },
  "version": {
    "version": "1.0.1",
    "publicationStatus": "published",
    "hasPendingPublicationSnapshot": false
  },
  "attempt": {
    "status": "finalized",
    "hasResult": true,
    "resultVersionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g"
  }
}

A second repair/sweep did not republish the version.

3. Transactional recovery audit
{
  "actorUserId": "zx71adwhfnxytkvzvs6eyveaw98ctb36",
  "action": "skill.orphaned_pending_version.repair",
  "metadata": {
    "skillId": "xx75gsf08g3k755rexfztjgerx8cvcwb",
    "version": "1.0.1",
    "publishAttemptId": "r97cyjhzzmhnj4xfrnwmep53918cv239",
    "priorPublicationStatus": "pending",
    "priorAttemptStatus": "failed",
    "hadPendingPublication": true,
    "resultVersionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g",
    "resultPublicationStatus": "published"
  }
}

Dry-run returned repaired: false, reason: "dry-run" with no recovery audit. The finalized/incomplete-check case also left recoveryAudits: [].

4. Owner / anonymous HTTP behavior
{
  "incompleteChecks_owner": {
    "status": 423
  },
  "incompleteChecks_anonymous": {
    "status": 404
  },
  "failedIncompleteChecks_owner": {
    "status": 409,
    "message": "publication checks did not complete successfully; orphan repair is not available until recorded TruffleHog and ClawScan checks are both clean"
  },
  "failedIncompleteChecks_anonymous": {
    "status": 404
  },
  "cappedFailedClean_owner": {
    "status": 409,
    "message": "publication finalization failed; an operator can repair it with maintenance:repairOrphanedPendingSkillVersion"
  },
  "cappedFailedClean_anonymous": {
    "status": 404
  },
  "after_anonymousVersion": {
    "status": 200,
    "version": "1.0.1"
  },
  "after_skill": {
    "tags.latest": "1.0.1",
    "stats.versions": 2
  }
}
5. Security followups from real Convex scheduler

Read-only _scheduled_functions inspection on dev:blissful-turtle-103 found exactly four jobs for repaired version xs7eactfxz3e56geg1p0tvjpkn8ctp9g:

{
  "counts": {
    "vt": 1,
    "enqueueSkillVersionScanInternal": 3,
    "total": 4
  },
  "jobs": [
    {
      "name": "securityScan.js:enqueueSkillVersionScanInternal",
      "scheduledTime": 1787199263783,
      "state": { "kind": "success" },
      "args": {
        "source": "publish",
        "versionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g"
      }
    },
    {
      "name": "vt.js:scanWithVirusTotal",
      "scheduledTime": 1787199263783,
      "state": { "kind": "success" },
      "args": {
        "versionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g"
      }
    },
    {
      "name": "securityScan.js:enqueueSkillVersionScanInternal",
      "scheduledTime": 1787199265783,
      "state": { "kind": "success" },
      "args": {
        "source": "publish",
        "preserveActiveJob": true,
        "preserveExistingJob": true,
        "versionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g"
      }
    },
    {
      "name": "securityScan.js:enqueueSkillVersionScanInternal",
      "scheduledTime": 1787199278783,
      "state": { "kind": "success" },
      "args": {
        "source": "publish",
        "preserveActiveJob": true,
        "preserveExistingJob": true,
        "versionId": "xs7eactfxz3e56geg1p0tvjpkn8ctp9g"
      }
    }
  ]
}

That is exactly:

  • vt.js:scanWithVirusTotal
  • securityScan.js:enqueueSkillVersionScanInternal at +0ms
  • backup enqueue at +2s
  • backup enqueue at +15s

The normal-finalizer path remains covered against duplicate scheduling.

Validation

Local Windows full-suite runs have unrelated environment/format noise, so the repository Linux workflows remain the final full-suite gate.

No production deployment or repair was run.

lechiffre-cfo pushed a commit to lechiffre-cfo/clawhub that referenced this pull request Aug 22, 2026
…air flow

- releaseFinalizationClaimPatch (publishAttempts.ts) now caps
  consecutive finalization failures only for kind==="skill". Package
  attempts have no repair path yet, so they retry unbounded instead of
  becoming a permanent orphan.

- isActiveAttemptLive (publishAttempts.ts) no longer abandons a
  non-terminal attempt just because finalizationFailureCount > 0. It
  now checks staleness against updatedAt (falling back to createdAt)
  using ACTIVE_ATTEMPT_RETRYABLE_STALE_MS for attempts with retry
  activity, or ACTIVE_ATTEMPT_UNCLAIMED_GRACE_MS for zero-failure
  attempts, so a below-cap dispatcher retry (or a long pending_checks
  phase that just transitioned cleanly) is not raced by manual repair.
  Added findActiveSkillPublishAttemptByIdInternal so the repair
  pre-check can look up a known publishAttemptId directly instead of
  only scanning by slug/version.

- repairOrphanedPendingSkillVersionHandler (maintenance.ts) now
  publishes the pending version and closes the orphaned attempt
  atomically via a new publishPendingVersionAndCloseAttemptInternal
  mutation (skills.ts), closing before follow-ups are scheduled. This
  removes the publish-before-close race where a crash between publish
  and close left the version published but the attempt reclaimable.
  Also handles the already-published-but-still-open-attempt retry
  case as an attempt-cleanup path.

- skillsGetRouterV1Handler exact-version path (skillsV1.ts) falls back
  to an owner-visible pending-publication diagnostic via
  getSkillBySlugInternal + describeOwnerVisibleSkillVersionState when
  the public getBySlug lookup misses entirely, so a brand-new hidden
  skill (pending its first publish) no longer 404s for its owner.

Tests added/updated in publishAttempts.test.ts, maintenance.test.ts,
and httpApiV1.handlers.test.ts for all four findings.
lechiffre-cfo pushed a commit to lechiffre-cfo/clawhub that referenced this pull request Aug 22, 2026
Require clean TruffleHog+ClawScan before any repair-eligible status, write a transactional recovery audit with actor, and only claim operator repair when the failed attempt is actually eligible. Add openclaw#3466 CLEAN-but-pending runtime coverage and a throwaway-dev proof harness.
lechiffre-cfo pushed a commit to lechiffre-cfo/clawhub that referenced this pull request Aug 22, 2026
Remove throwaway proof seed/script and restore Convex generated
files to match origin/main so the openclaw#3401 tip stays publishable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill publish returns success and reserves version, but version is absent from latest/version index

2 participants