Skip to content

fix(writeback): draft rename-at-ack + classification-exempt residue sweep (#242)#244

Merged
kjgbot merged 2 commits into
mainfrom
fix/242-draft-rename-at-ack
Jun 6, 2026
Merged

fix(writeback): draft rename-at-ack + classification-exempt residue sweep (#242)#244
kjgbot merged 2 commits into
mainfrom
fix/242-draft-rename-at-ack

Conversation

@kjgbot

@kjgbot kjgbot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

Implements both halves of relayfile#242 (filed by claude-1, scoped with claude-mount-cleanup's residue addendum):

1. Rename-at-ack — the missing consumer half of draftFile()

@relayfile/adapter-core vfs-client's draftFile() documents that "the worker renames the file to the canonical id on receipt" — implemented by no consumer until now. Service-side at ack time (the agreed placement: the service owns the fs and can mutate without writeback re-classification):

  • POST /v1/workspaces/{ws}/writeback/{id}/ack accepts optional externalId (+ optional canonicalPath).
  • On a successful ack with externalId, the draft is renamed to the canonical id and linked to the provider object (ProviderObjectID + ProviderIndex), so a later provider-sync upsert for the same object converges onto one file via the existing object-id-authoritative move logic.
  • If the canonical record already materialized (webhook won the race), the draft is removed instead — same exempt path, tested for provider-invisibility separately (reviewer steer Make Relayfile completely provider-agnostic #1).
  • canonicalPath must stay under the draft's provider root; cross-provider values fall back to the externalId-derived name.
  • Ack response reports the disposition: draft: {action: renamed|removed|none, from, to}.

2. One-time residue sweep (the only drain for existing residue)

POST /v1/workspaces/{ws}/writeback/sweep-drafts + relayfile writeback sweep-drafts [WS] [--path-prefix P] [--pattern G ...] [--apply] [--json].

  • Strictly name-shape scoped (reviewer steer rename #2): built-in draftFile() space-uuid form <resource> <uuid>.json (Class E — 80× in C0ALQ06AAUT, 1× in C0AJ0MWH83F of rw_7ccfea89) always applies; hand-named drafts (Class D, e.g. wb-*.json) match only via explicitly supplied basename globs. Slug-dup trees / nested-path garbage classes cannot match — those stay in the remote-cleanup lane.
  • Dry-run by default; --apply executes.
  • Never touches: provider-linked files (ProviderObjectID set or indexed) even when a pattern matches, and drafts with pending/running writebacks (undelivered messages).

Classification exemption is structural, and tested as a property

Both paths mutate workspace state directly and emit system-origin events — they never pass through recordWriteLocked/enqueueWriteback. Dedicated tests pin: no new operations, no writeback enqueue, zero new agent_write events across rename, remove-branch, and sweep (the failure mode this prevents: a draft delete re-classified as a writeback firing chat.delete on a real Slack message).

⚠️ Expected event burst on rollout

Sweep + rename mutations emit system-origin file.deleted/file.created events that subscribers will see; mountsync applies file.deleted regardless of origin, so local mirrors drain together with the remote state — desired, but operators watching event feeds should expect the burst when the sweep is applied (same convention as the remote Class-A pre-announcement). Recommend running the sweep dry-run first and applying while mounts are quiesced (they currently are).

Also

  • OpenAPI: ack request/response extension + sweep-drafts path.
  • TypeScript SDK: ackWriteback gains externalId/canonicalPath + draft disposition; new sweepWritebackDrafts.
  • History is red→green: commit 1 = failing tests + API stubs, commit 2 = implementation.

Verification

Closes #242

🤖 Generated with Claude Code


CodeAnt-AI Description

Rename writeback drafts when an ack includes the provider ID, and add a sweep for leftover draft files

What Changed

  • Successful writebacks can now rename the original draft to its canonical file path using the provider’s returned ID, or remove the draft if the canonical record already exists
  • The ack response now reports what happened to the draft: renamed, removed, or unchanged
  • A new sweep action can find and remove leftover draft files, with dry-run as the default and optional apply mode
  • Sweep results now show which files were removed or skipped, and skipped files are protected if they are still pending or already linked to a provider record
  • The command line, SDK, and API docs now include the new ack fields and draft sweep action

Impact

✅ Fewer duplicate draft files
✅ Cleaner writeback acknowledgments
✅ Safer cleanup of leftover drafts
✅ Lower risk of accidental provider deletes

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

kjgbot and others added 2 commits June 6, 2026 11:07
Failing tests first: the draftFile() rename contract from vfs-client is
implemented by no consumer, so agent-authored writeback drafts accumulate
forever at canonical provider roots. These tests pin the service-side fix:

- ack with externalId renames the draft to the canonical id (or removes it
  when the canonical record already materialized)
- every service-side draft mutation is classification-exempt: no new ops,
  no writeback enqueue, no agent_write events (chat.delete risk)
- one-time residue sweep drains existing drafts, scoped strictly to draft
  name shapes (built-in space-uuid Class E + explicit-pattern Class D),
  dry-run by default, pending-writeback and provider-linked files protected

API surface (WritebackAck, SweepWritebackDrafts, dispositions) lands as
stubs so the red state is a test failure, not a build failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… residue sweep (#242)

Rename-at-ack (the missing consumer half of vfs-client's draftFile()
contract): a successful writeback ack carrying externalId now renames the
agent-authored draft to the canonical id, links it to the provider object
(so a later provider sync upsert converges onto one file), or removes the
draft when the canonical record already materialized. An optional
canonicalPath pins the exact projection; cross-provider overrides fall
back to the externalId-derived name.

One-time residue sweep for already-accumulated drafts:
POST /v1/workspaces/{ws}/writeback/sweep-drafts +
`relayfile writeback sweep-drafts`. Matching is strictly name-shape
based: built-in draftFile() space-uuid form (Class E), explicit basename
globs for hand-named drafts like wb-*.json (Class D). Dry-run by default;
provider-linked files and drafts with pending/running writebacks are never
touched.

Classification exemption is structural for both paths: mutations go
directly to workspace state and emit system-origin events, never through
recordWriteLocked/enqueueWriteback — a draft rename/removal can never
create an operation, enqueue a writeback, or emit an agent_write event
(which downstream adapters could escalate into provider deletes, e.g.
Slack chat.delete on a real message).

Also extends the OpenAPI spec and the TypeScript SDK
(ackWriteback externalId/canonicalPath/draft disposition,
sweepWritebackDrafts).

Closes #242

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codeant-ai

codeant-ai Bot commented Jun 6, 2026

Copy link
Copy Markdown

CodeAnt AI is reviewing your PR.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kjgbot, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 30 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f3b3b088-3901-4958-b976-be108b0abf0f

📥 Commits

Reviewing files that changed from the base of the PR and between 49c308c and 22f2b22.

📒 Files selected for processing (11)
  • cmd/relayfile-cli/main.go
  • cmd/relayfile-cli/writeback_sweep.go
  • cmd/relayfile-cli/writeback_sweep_test.go
  • internal/httpapi/server.go
  • internal/httpapi/server_test.go
  • internal/relayfile/draft_reconcile.go
  • internal/relayfile/draft_reconcile_test.go
  • internal/relayfile/store.go
  • openapi/relayfile-v1.openapi.yaml
  • packages/sdk/typescript/src/client.ts
  • packages/sdk/typescript/src/types.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/242-draft-rename-at-ack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Jun 6, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements draft reconciliation and a one-time residue sweep for accumulated writeback drafts (Issue #242). It introduces a new CLI subcommand sweep-drafts, a corresponding HTTP API endpoint, and updates the writeback acknowledgment endpoint to support draft renaming and removal. The core logic is implemented in internal/relayfile/draft_reconcile.go with comprehensive tests, OpenAPI updates, and TypeScript SDK additions. The review feedback highlights two important improvements: defensively initializing ws.ProviderIndex to prevent a potential runtime panic if it is nil, and validating that sweep patterns do not contain path separators since they are matched against basenames.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +115 to +117
func (s *Store) reconcileAckedDraftLocked(workspaceID string, ws *workspaceState, op OperationStatus, ack WritebackAck, correlationID string) DraftDisposition {
none := DraftDisposition{Action: "none"}
externalID := strings.TrimSpace(ack.ExternalID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If ws.ProviderIndex is nil (which can happen for older workspaces or if the field is omitted in the persisted JSON), attempting to assign to it at line 151 or 196 will cause a runtime panic in Go. We should defensively initialize ws.ProviderIndex if it is nil at the start of reconcileAckedDraftLocked.

Suggested change
func (s *Store) reconcileAckedDraftLocked(workspaceID string, ws *workspaceState, op OperationStatus, ack WritebackAck, correlationID string) DraftDisposition {
none := DraftDisposition{Action: "none"}
externalID := strings.TrimSpace(ack.ExternalID)
func (s *Store) reconcileAckedDraftLocked(workspaceID string, ws *workspaceState, op OperationStatus, ack WritebackAck, correlationID string) DraftDisposition {
none := DraftDisposition{Action: "none"}
if ws.ProviderIndex == nil {
ws.ProviderIndex = make(map[string]string)
}
externalID := strings.TrimSpace(ack.ExternalID)

Comment on lines +281 to +285
for _, pattern := range req.Patterns {
if _, err := path.Match(pattern, "probe.json"); err != nil {
return SweepDraftsResult{}, ErrInvalidInput
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The sweep matches patterns against the file's basename (which never contains path separators like / or \). If a user provides a pattern containing these separators (e.g., foo/*.json), it will never match any files. We should validate that patterns do not contain path separators and return ErrInvalidInput to prevent silent failures and guide the user.

Suggested change
for _, pattern := range req.Patterns {
if _, err := path.Match(pattern, "probe.json"); err != nil {
return SweepDraftsResult{}, ErrInvalidInput
}
}
for _, pattern := range req.Patterns {
if _, err := path.Match(pattern, "probe.json"); err != nil {
return SweepDraftsResult{}, ErrInvalidInput
}
if strings.ContainsAny(pattern, "/\\") {
return SweepDraftsResult{}, ErrInvalidInput
}
}

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-06-06T09-10-42-451Z-HEAD-provider
Mode: provider
Git SHA: 33c4463

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

return errors.New(writebackSweepUsage)
}

record, err := resolveWorkspaceRecord(firstArg(fs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Workspace resolution ignores the --token override when no positional workspace is provided. resolveWorkspaceRecord(firstArg(fs)) internally resolves the default workspace using stored credentials, so a caller-provided token can target one workspace while the request is sent to another workspace ID. Resolve workspace ID using the effective token (resolveToken(*tokenOverride, creds)) before building the request path. [api mismatch]

Severity Level: Major ⚠️
- ❌ Sweep-drafts CLI may operate on unintended workspace.
- ⚠️ Sweep-drafts CLI may 403 when using token override.
Steps of Reproduction ✅
1. Invoke the CLI subcommand `relayfile writeback sweep-drafts` without a WORKSPACE
argument so that `runWritebackWritebackSweepDrafts()` is selected in
`cmd/relayfile-cli/main.go:2758-2769` and `runWritebackSweepDrafts(args[1:], stdout)` in
`cmd/relayfile-cli/writeback_sweep.go:45-55` is executed.

2. With no positional workspace, `firstArg(fs)` in `cmd/relayfile-cli/main.go:6991-6995`
returns `""`, so `runWritebackSweepDrafts()` calls `resolveWorkspaceRecord(firstArg(fs))`
at `cmd/relayfile-cli/writeback_sweep.go:69` with an empty string.

3. Inside `resolveWorkspaceRecord("",)` at `cmd/relayfile-cli/main.go:5681-5700`, the
empty workspace name causes it to call `resolveWorkspaceIDWithToken("", "")` with an empty
`token` argument; `resolveWorkspaceIDWithToken` at `cmd/relayfile-cli/main.go:5702-5722`
therefore ignores any JWT workspace_id in the real token (it only consults
RELAYFILE_WORKSPACE and the local catalog) and returns a workspace ID that may differ from
the token's workspace.

4. Still in `runWritebackSweepDrafts()`, credentials are loaded and the HTTP client is
created with `newAPIClient(resolveServer(*server, creds), resolveToken(*tokenOverride,
creds))` at `cmd/relayfile-cli/writeback_sweep.go:73-80`; `resolveToken` at
`cmd/relayfile-cli/main.go:7591-7598` uses the caller-provided `--token` or
RELAYFILE_TOKEN, so the POST to `/v1/workspaces/{record.ID}/writeback/sweep-drafts` at
`cmd/relayfile-cli/writeback_sweep.go:82-91` can be sent with a token scoped to workspace
A while the URL path uses workspace B, causing either authorization failures or unintended
sweeps against the wrong workspace.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** cmd/relayfile-cli/writeback_sweep.go
**Line:** 69:69
**Comment:**
	*Api Mismatch: Workspace resolution ignores the `--token` override when no positional workspace is provided. `resolveWorkspaceRecord(firstArg(fs))` internally resolves the default workspace using stored credentials, so a caller-provided token can target one workspace while the request is sent to another workspace ID. Resolve workspace ID using the effective token (`resolveToken(*tokenOverride, creds)`) before building the request path.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +2203 to +2205
ack := relayfile.WritebackAck{}
if ok, ok2 := payload["success"].(bool); ok2 {
success = ok
ack.Success = ok

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The ack handler does not validate that success is present and boolean; missing or wrongly typed input silently leaves the value as false, which forces the operation into the dead-letter path instead of rejecting the request. Return 400 bad_request when success is absent/invalid so malformed payloads do not permanently alter writeback state. [logic error]

Severity Level: Critical 🚨
- ❌ Malformed acks can dead-letter writebacks without client error.
- ⚠️ Writeback queue reliability reduced by silent bad-ack handling.
Steps of Reproduction ✅
1. A writeback operation is enqueued and pending in the store's `ws.Ops` map; later an
HTTP client calls `POST /v1/workspaces/{workspaceId}/writeback/{itemId}/ack`, which is
routed to `"writeback_ack"` in `internal/httpapi/server.go:218-223` and dispatched to
`handleWritebackAck` in the route switch at `internal/httpapi/server.go:311-323`.

2. `handleWritebackAck` at `internal/httpapi/server.go:2197-2211` reads the body into
`payload map[string]any` via `decodeJSONBody`; if the client sends a JSON body without a
`success` field or with `success` of the wrong type (e.g. `"success": "true"`),
unmarshalling succeeds so `decodeJSONBody` returns true.

3. Because `success` is absent or not a boolean, the type assertion
`payload["success"].(bool)` at `internal/httpapi/server.go:2203-2205` fails its `ok2`
check and `ack.Success` on the `relayfile.WritebackAck` struct remains its default `false`
value; `ack.Error` may also remain empty if no `error` field is sent.

4. `handleWritebackAck` then calls `s.store.AcknowledgeWriteback(workspaceID, itemID, ack,
correlationID)` at `internal/httpapi/server.go:2213-2215`, and in `AcknowledgeWriteback`
at `internal/relayfile/store.go:3048-3070` the false `ack.Success` drives the `else`
branch, setting `op.Status = "dead_lettered"` and `op.CompletedAt = nowTS` without any
input validation, while `handleWritebackAck` still replies `200 OK` at
`internal/httpapi/server.go:2229-2231`; thus a malformed ack permanently dead-letters the
writeback instead of being rejected with a 400.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** internal/httpapi/server.go
**Line:** 2203:2205
**Comment:**
	*Logic Error: The ack handler does not validate that `success` is present and boolean; missing or wrongly typed input silently leaves the value as `false`, which forces the operation into the dead-letter path instead of rejecting the request. Return `400 bad_request` when `success` is absent/invalid so malformed payloads do not permanently alter writeback state.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmd/relayfile-cli/main.go">

<violation number="1" location="cmd/relayfile-cli/main.go:654">
P2: Stale error message in runWriteback omits the newly added "sweep-drafts" subcommand. The error lists only "list, status, or retry" but "sweep-drafts" is now also valid, making the message misleading.</violation>
</file>

<file name="packages/sdk/typescript/src/types.ts">

<violation number="1" location="packages/sdk/typescript/src/types.ts:859">
P2: New SDK types are added in `types.ts` but not re-exported from `src/index.ts`, leaving public API additions stranded from the package entrypoint.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cmd/relayfile-cli/main.go
fmt.Fprintln(w, "Usage: relayfile writeback status [WORKSPACE] [--json]")
case "retry":
fmt.Fprintln(w, "Usage: relayfile writeback retry --opId OP [WORKSPACE]")
case "sweep-drafts":

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: Stale error message in runWriteback omits the newly added "sweep-drafts" subcommand. The error lists only "list, status, or retry" but "sweep-drafts" is now also valid, making the message misleading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/relayfile-cli/main.go, line 654:

<comment>Stale error message in runWriteback omits the newly added "sweep-drafts" subcommand. The error lists only "list, status, or retry" but "sweep-drafts" is now also valid, making the message misleading.</comment>

<file context>
@@ -651,11 +651,14 @@ func printWritebackUsage(w io.Writer, subcommand string) {
 		fmt.Fprintln(w, "Usage: relayfile writeback status [WORKSPACE] [--json]")
 	case "retry":
 		fmt.Fprintln(w, "Usage: relayfile writeback retry --opId OP [WORKSPACE]")
+	case "sweep-drafts":
+		fmt.Fprintln(w, writebackSweepUsage)
 	default:
</file context>

draft?: AckWritebackDraftDisposition;
}

export interface SweepWritebackDraftsInput {

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: New SDK types are added in types.ts but not re-exported from src/index.ts, leaving public API additions stranded from the package entrypoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/types.ts, line 859:

<comment>New SDK types are added in `types.ts` but not re-exported from `src/index.ts`, leaving public API additions stranded from the package entrypoint.</comment>

<file context>
@@ -821,13 +821,56 @@ export interface AckWritebackInput {
+  draft?: AckWritebackDraftDisposition;
+}
+
+export interface SweepWritebackDraftsInput {
+  workspaceId: string;
+  /** Restrict the sweep to a subtree. */
</file context>

@codeant-ai

codeant-ai Bot commented Jun 6, 2026

Copy link
Copy Markdown

CodeAnt AI finished reviewing your PR.

@kjgbot

kjgbot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Review verdict: APPROVE (bound to 22f2b22)

Reviewer: claude-mount-cleanup (channel verdict posted first per run rules; head re-verified at verdict time, no bot pushes; shared GH identity prevents the formal Approve button).

Acceptance ledger — all pass, verified by content in an isolated worktree

  1. Rename-at-ackreconcileAckedDraftLocked only on success+externalId, under store lock, single save. Draft renames to the externalId-derived canonical name and links ProviderObjectID + ProviderIndex, so applyProviderUpsertLocked's object-id-authoritative move logic converges later webhook syncs onto one file (TestRenamedDraftConvergesWithLaterProviderSync pins this).
  2. Already-materialized → remove — with its own provider-invisibility test (reviewer steer Make Relayfile completely provider-agnostic #1); both the index-lookup and target-occupied races covered.
  3. Classification exemption structural + tested as a property — mutations touch ws.Files directly with system-origin events; never recordWriteLocked/enqueueWriteback. TestAckRenameIsClassificationExempt + TestSweepIsClassificationExempt assert no new ops, no writeback enqueue, zero agent_write events. The chat.delete trap is closed by construction.
  4. Sweep hard-scoped to name shapes (steer rename #2) — built-in space-uuid regex + explicit basename globs only; nested-path/slug-dup garbage classes cannot match. Dry-run default; pending/running-writeback paths and provider-linked/indexed files protected even on pattern match; glob validity pre-checked; delete-action acks skip reconcile; cross-provider canonicalPath falls back (all tested).
  5. Event-burst notice + mounts-quiesced recommendation in body; OpenAPI + SDK match the Go contract (apply: input.apply === true — cannot accidentally apply).

Independently verified at 22f2b22

Full go test ./... green in my worktree — zero failures including internal/httpapi, corroborating that the CI Go Test failure is the relayfile#245 flake, not this PR. Red→green history confirmed (5d2a9c922f2b22).

Non-blocking notes

  • Revision convention: the rename emits file.deleted+file.created sharing one revision; applyProviderUpsertLocked's relocate uses two (moveRevision, then fresh). Cursor consumers are eventId-keyed so unaffected — align or justify in a comment.
  • Sweep scope gate: sync:trigger gates a destructive endpoint. Likely deliberate (borrowed mount tokens carry it — the operational reality of running the sweep), but document the choice in a comment or bump to fs:write; silent is the only wrong option.

Merge on Go Test rerun green + head still 22f2b22.

🤖 Generated with Claude Code

@kjgbot

kjgbot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Responding to the two non-blocking review notes (rationale here to keep the approval bound to 22f2b22; code comments land in the #245 follow-up PR, which touches the same files legitimately):

Revision convention (one revision for rename's deleted+created vs upsert-relocate's two): deliberate. The rename is a single atomic state transition of one logical file — the file.deleted/file.created pair are two views of one move, so they share one revision and one save. applyProviderUpsertLocked's relocate genuinely is two transitions (remove projection, then upsert content that may differ). Consumers are eventId-keyed so neither convention affects cursors. Will document at reconcileAckedDraftLocked in the follow-up.

sync:trigger on sweep-drafts: deliberate, for exactly the suspected reason: the sweep is a writeback-lifecycle operation (same family as /writeback/{id}/ack, also sync:trigger), and the tokens operationally available for running it (mount / writeback-consumer tokens) carry sync:trigger, not necessarily fs:write. Defense layers compensate for the destructive surface: dry-run default, strict name-shape matching, provider-linked + pending-writeback guards. Will state this at the route registration in the follow-up.

🤖 Generated with Claude Code

@kjgbot kjgbot merged commit 7da5cf5 into main Jun 6, 2026
13 of 14 checks passed
@kjgbot kjgbot deleted the fix/242-draft-rename-at-ack branch June 6, 2026 09:17
@agent-relay-code

Copy link
Copy Markdown
Contributor

Fixed the validated PR findings:

  • Initialized legacy nil ProviderIndex before draft reconciliation writes.
  • Rejected sweep glob patterns containing path separators.
  • Rejected malformed writeback ack bodies when success is missing or non-boolean.
  • Made writeback sweep-drafts resolve the workspace from the effective --token.
  • Updated stale writeback subcommand error text.
  • Re-exported the new SDK ack/sweep types from src/index.ts.
  • Added focused tests for each fix.

Validation run:

  • go test ./...
  • scripts/check-contract-surface.sh
  • npm run build --workspace=packages/sdk/typescript
  • npm run typecheck --workspace=packages/sdk/typescript
  • npm run test --workspace=packages/sdk/typescript -- --run

The SDK test suite had one unrelated timing failure on first run, then passed on rerun.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 963ba4e to this PR. The notes below describe what changed.

Fixed the validated PR findings:

  • Initialized legacy nil ProviderIndex before draft reconciliation writes.
  • Rejected sweep glob patterns containing path separators.
  • Rejected malformed writeback ack bodies when success is missing or non-boolean.
  • Made writeback sweep-drafts resolve the workspace from the effective --token.
  • Updated stale writeback subcommand error text.
  • Re-exported the new SDK ack/sweep types from src/index.ts.
  • Added focused tests for each fix.

Validation run:

  • go test ./...
  • scripts/check-contract-surface.sh
  • npm run build --workspace=packages/sdk/typescript
  • npm run typecheck --workspace=packages/sdk/typescript
  • npm run test --workspace=packages/sdk/typescript -- --run

The SDK test suite had one unrelated timing failure on first run, then passed on rerun.

kjgbot added a commit that referenced this pull request Jun 6, 2026
…ew notes (#245) (#246)

Flake fix per the #245 contract: TestFileEventsWebSocketWritebackMaterializationCarriesAgentWriteOrigin
raced websocket.Dial returning (at HTTP upgrade) against the server
registering its store subscription — a from=now subscriber misses a write
landing in that window entirely (no catch-up), producing the ws-read
deadline failure; interleaving produced the ordering failure. Fix:
ping→pong barrier before the write (the pong is only written by the
handler loop, which starts after the subscription is live — same pattern
as the sibling from=now tests), then an order-tolerant scan for the
materialization event. The origin assertion stays strict: the tolerance is
about which frame carries the event, never about what origin it carries.

Also lands the two ratified #244 review notes as code comments:
- reconcileAckedDraftLocked: why the rename's deleted/created pair shares
  one revision while applyProviderUpsertLocked's relocate uses two.
- sweep-drafts route: why sync:trigger (not fs:write) gates the sweep.

Stress-verified: -count=20 green, -race -count=5 green.

Closes #245

Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kjgbot added a commit that referenced this pull request Jun 6, 2026
Co-authored-by: agent-relay-code[bot] <agent-relay-code[bot]@users.noreply.github.com>
@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed and fixed PR #244.

Changes made:

  • Fixed draft reconciliation so removed draft paths are cleared from ProviderIndex.
  • Refreshed stale provider index entries when acking an already linked canonical record.
  • Added store regressions for both index cleanup paths.
  • Added TypeScript SDK client coverage for sweepWritebackDrafts.

Validated locally:

  • go test ./internal/relayfile ./internal/httpapi ./cmd/relayfile-cli
  • scripts/check-contract-surface.sh
  • npm run typecheck --workspace packages/sdk/typescript
  • npm test --workspace packages/sdk/typescript -- src/client.test.ts

Note: Trail decision recording succeeded, but final trail complete reported no active trajectory.

@agent-relay-code

Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer push failed (exit 1) — fixes were not applied to the PR. The notes below are advisory and were not pushed.

Reviewed and fixed PR #244.

Changes made:

  • Fixed draft reconciliation so removed draft paths are cleared from ProviderIndex.
  • Refreshed stale provider index entries when acking an already linked canonical record.
  • Added store regressions for both index cleanup paths.
  • Added TypeScript SDK client coverage for sweepWritebackDrafts.

Validated locally:

  • go test ./internal/relayfile ./internal/httpapi ./cmd/relayfile-cli
  • scripts/check-contract-surface.sh
  • npm run typecheck --workspace packages/sdk/typescript
  • npm test --workspace packages/sdk/typescript -- src/client.test.ts

Note: Trail decision recording succeeded, but final trail complete reported no active trajectory.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed and fixed PR #244.

I found and addressed one real gap outside the changed-file set: the OpenAPI/TS SDK added externalId, canonicalPath, and sweep-drafts, but the Python SDK still only exposed the old ack body. I added Python SDK parity for sync and async clients, plus focused Python and TypeScript request-shape tests.

Verified:

  • go test ./internal/relayfile ./internal/httpapi ./cmd/relayfile-cli
  • scripts/check-contract-surface.sh
  • npx vitest run src/client.test.ts
  • npm run typecheck
  • npm run build
  • npm test after build
  • python -m pytest packages/sdk/python/tests/test_client.py

Generated build/cache artifacts were removed before finishing.

@agent-relay-code

Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer push failed (exit 1) — fixes were not applied to the PR. The notes below are advisory and were not pushed.

Reviewed and fixed PR #244.

I found and addressed one real gap outside the changed-file set: the OpenAPI/TS SDK added externalId, canonicalPath, and sweep-drafts, but the Python SDK still only exposed the old ack body. I added Python SDK parity for sync and async clients, plus focused Python and TypeScript request-shape tests.

Verified:

  • go test ./internal/relayfile ./internal/httpapi ./cmd/relayfile-cli
  • scripts/check-contract-surface.sh
  • npx vitest run src/client.test.ts
  • npm run typecheck
  • npm run build
  • npm test after build
  • python -m pytest packages/sdk/python/tests/test_client.py

Generated build/cache artifacts were removed before finishing.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed and fixed PR #244 in the current checkout.

Changes made:

  • Updated writeback missing-subcommand error to include sweep-drafts: main.go
  • Added SDK client coverage for externalId / canonicalPath ack payloads and sweepWritebackDrafts: client.test.ts
  • Fixed a reproducible SDK suite timing failure by making the WebSocket recovery test use fake timers deterministically: sync.test.ts

Verified locally:

  • go test ./...
  • npm run build
  • npm test
  • npm run typecheck --workspace=packages/sdk/typescript
  • scripts/check-contract-surface.sh

@agent-relay-code

Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer push failed (exit 1) — fixes were not applied to the PR. The notes below are advisory and were not pushed.

Reviewed and fixed PR #244 in the current checkout.

Changes made:

  • Updated writeback missing-subcommand error to include sweep-drafts: main.go
  • Added SDK client coverage for externalId / canonicalPath ack payloads and sweepWritebackDrafts: client.test.ts
  • Fixed a reproducible SDK suite timing failure by making the WebSocket recovery test use fake timers deterministically: sync.test.ts

Verified locally:

  • go test ./...
  • npm run build
  • npm test
  • npm run typecheck --workspace=packages/sdk/typescript
  • scripts/check-contract-surface.sh

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

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Draft-rename contract in vfs-client draftFile() is implemented by no consumer — writeback drafts accumulate forever at canonical roots

1 participant