AR-272 add direct relayfile writeback push#282
Conversation
|
Warning Review limit reached
More reviews will be available in 59 minutes and 46 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds five webhook management methods ( ChangesTypeScript SDK Webhook Methods
Go CLI writeback push and workspace status
Agent Trajectory Logs
Sequence Diagram(s)sequenceDiagram
participant CLI as relayfile-cli
participant Outbox as .relay/outbox
participant AuthAPI as Delegated Token API
participant DataPlane as Bulk Write API
participant OpAPI as Op Status API
rect rgba(70, 130, 180, 0.5)
note over CLI,Outbox: Receipt lifecycle
CLI->>Outbox: write pending receipt (commandId)
CLI->>AuthAPI: POST bootstrap delegated token
AuthAPI-->>CLI: delegated credentials
end
rect rgba(60, 179, 113, 0.5)
note over CLI,DataPlane: Bulk write
CLI->>DataPlane: POST /bulk-write (content + contentIdentity)
DataPlane-->>CLI: WriteResult (opId, status, writeback.state)
end
rect rgba(210, 105, 30, 0.5)
note over CLI,Outbox: Polling and receipt finalization
CLI->>OpAPI: GET op/{opId}/status (poll)
OpAPI-->>CLI: completed
CLI->>Outbox: move receipt → acked (or failed on error)
CLI-->>CLI: emit JSON or human output
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces new CLI subcommands, including writeback push for pushing local files to a remote workspace and workspace status for reporting workspace health. It also adds TypeScript SDK support for webhook management and dead-letter queues, along with content identity tracking for draft creation. The review feedback suggests ensuring cross-platform correctness on Windows by using path.Base instead of filepath.Base for remote paths, and refactoring the writeback polling mechanism to accept and respect a context.Context for robust timeout and cancellation handling.
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.
| func isWritebackDraftPath(remotePath string) bool { | ||
| base := filepath.Base(normalizeWritebackFailurePath(remotePath)) | ||
| return strings.HasPrefix(base, "factory-create-") && strings.HasSuffix(base, ".json") || | ||
| relayfile.IsDraftFilePath(remotePath) | ||
| } |
There was a problem hiding this comment.
On Windows, filepath.Base expects backslash (\\) as the path separator. Since remotePath is a slash-separated remote path, filepath.Base will fail to correctly extract the base name on Windows (it will return the entire path instead of just the filename).
To ensure cross-platform correctness, use path.Base instead of filepath.Base for remote paths, matching the pattern used in internal/mountsync/syncer.go.
| func isWritebackDraftPath(remotePath string) bool { | |
| base := filepath.Base(normalizeWritebackFailurePath(remotePath)) | |
| return strings.HasPrefix(base, "factory-create-") && strings.HasSuffix(base, ".json") || | |
| relayfile.IsDraftFilePath(remotePath) | |
| } | |
| func isWritebackDraftPath(remotePath string) bool { | |
| base := path.Base(normalizeWritebackFailurePath(remotePath)) | |
| return strings.HasPrefix(base, "factory-create-") && strings.HasSuffix(base, ".json") || | |
| relayfile.IsDraftFilePath(remotePath) | |
| } |
| "bytes" | ||
| "context" | ||
| "crypto/rand" | ||
| "crypto/sha256" | ||
| "encoding/base64" | ||
| "encoding/hex" | ||
| "encoding/json" |
There was a problem hiding this comment.
Import the path package to support cross-platform remote path manipulation. Remote paths in this codebase are slash-separated, and using the standard path package is the established pattern for handling them correctly on all operating systems (including Windows).
| "bytes" | |
| "context" | |
| "crypto/rand" | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "encoding/hex" | |
| "encoding/json" | |
| "bytes" | |
| "context" | |
| "crypto/rand" | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "encoding/hex" | |
| "encoding/json" | |
| "path" |
| if opID != "" { | ||
| op, err := waitForWritebackOperation(commandClient, opID, *timeout) | ||
| if err != nil { | ||
| failed := pendingReceipt | ||
| failed.OpID = opID | ||
| failed.Status = "failed" | ||
| failed.LastAttemptAt = time.Now().UTC().Format(time.RFC3339Nano) | ||
| failed.AttemptCount = 1 | ||
| failed.LastError = sanitizeCLIReceiptError(err) | ||
| failed.DispatchStatus = "failed" | ||
| failed.CorrelationID = firstNonBlank(response.CorrelationID, failed.CorrelationID) | ||
| if receiptErr := writeWritebackPushReceipt(resolved.MountRoot, failed); receiptErr != nil { | ||
| return fmt.Errorf("%w; additionally failed to write failure receipt: %v", err, receiptErr) | ||
| } | ||
| return err | ||
| } | ||
| revision = firstNonBlank(revision, op.Revision) | ||
| dispatchStatus = firstNonBlank(op.Status, dispatchStatus) | ||
| } |
There was a problem hiding this comment.
Update the call site of waitForWritebackOperation to pass a context with the configured timeout. This allows the polling loop to respect the timeout and handle cancellations gracefully.
if opID != "" {
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
op, err := waitForWritebackOperation(ctx, commandClient, opID)
if err != nil {
failed := pendingReceipt
failed.OpID = opID
failed.Status = "failed"
failed.LastAttemptAt = time.Now().UTC().Format(time.RFC3339Nano)
failed.AttemptCount = 1
failed.LastError = sanitizeCLIReceiptError(err)
failed.DispatchStatus = "failed"
failed.CorrelationID = firstNonBlank(response.CorrelationID, failed.CorrelationID)
if receiptErr := writeWritebackPushReceipt(resolved.MountRoot, failed); receiptErr != nil {
return fmt.Errorf("%w; additionally failed to write failure receipt: %v", err, receiptErr)
}
return err
}
revision = firstNonBlank(revision, op.Revision)
dispatchStatus = firstNonBlank(op.Status, dispatchStatus)
}| func waitForWritebackOperation(commandClient *workspaceCommandClient, opID string, timeout time.Duration) (writebackOperationStatus, error) { | ||
| if strings.TrimSpace(opID) == "" { | ||
| return writebackOperationStatus{Status: "succeeded"}, nil | ||
| } | ||
| if timeout <= 0 { | ||
| timeout = 90 * time.Second | ||
| } | ||
| deadline := time.Now().Add(timeout) | ||
| for { | ||
| var op writebackOperationStatus | ||
| err := commandClient.getWorkspaceJSON(context.Background(), func(workspaceID string) string { | ||
| return fmt.Sprintf("/v1/workspaces/%s/ops/%s", url.PathEscape(workspaceID), url.PathEscape(opID)) | ||
| }, &op) | ||
| if err != nil { | ||
| return op, err | ||
| } | ||
| status := strings.TrimSpace(op.Status) | ||
| switch status { | ||
| case "succeeded": | ||
| return op, nil | ||
| case "failed", "dead_lettered", "canceled": | ||
| return op, fmt.Errorf("writeback operation %s %s", opID, status) | ||
| } | ||
| if time.Now().After(deadline) { | ||
| return op, fmt.Errorf("timed out waiting for writeback operation %s", opID) | ||
| } | ||
| time.Sleep(500 * time.Millisecond) | ||
| } | ||
| } |
There was a problem hiding this comment.
The polling loop in waitForWritebackOperation currently uses context.Background() for HTTP requests and a hardcoded time.Sleep(500 * time.Millisecond). This has two drawbacks:
- It does not respect context cancellation or timeouts if the CLI is interrupted (e.g., via Ctrl+C).
- Any transient network error during polling immediately aborts the entire push operation.
By passing a context.Context to waitForWritebackOperation, we can use it for the HTTP requests, handle timeouts/cancellations gracefully via select, and make the polling resilient to transient network glitches by continuing the loop on transient errors until the context is done.
func waitForWritebackOperation(ctx context.Context, commandClient *workspaceCommandClient, opID string) (writebackOperationStatus, error) {
if strings.TrimSpace(opID) == "" {
return writebackOperationStatus{Status: "succeeded"}, nil
}
for {
var op writebackOperationStatus
err := commandClient.getWorkspaceJSON(ctx, func(workspaceID string) string {
return fmt.Sprintf("/v1/workspaces/%s/ops/%s", url.PathEscape(workspaceID), url.PathEscape(opID))
}, &op)
if err == nil {
status := strings.TrimSpace(op.Status)
switch status {
case "succeeded":
return op, nil
case "failed", "dead_lettered", "canceled":
return op, fmt.Errorf("writeback operation %s %s", opID, status)
}
}
select {
case <-ctx.Done():
if err != nil {
return op, fmt.Errorf("timed out waiting for writeback operation %s: %w", opID, err)
}
return op, ctx.Err()
case <-time.After(500 * time.Millisecond):
}
}
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f5eb1eb86
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return []string{"fs:write:/**"} | ||
| } | ||
| provider := strings.TrimSpace(parts[0]) | ||
| return []string{fmt.Sprintf("fs:write:/%s/**", provider)} |
There was a problem hiding this comment.
Add ops:read to writeback push credentials
With the default delegated-auth path this requests a token scoped only to fs:write:/<provider>/**, but the same command later polls GET /v1/workspaces/{workspaceId}/ops/{opId} whenever bulk write returns an op id. I checked the workspace route switch in internal/httpapi/server.go, where that ops route requires ops:read, so a normal delegated push can successfully write the file and then fail/record a failed receipt on the status poll. Please request/validate an ops:read relayfile scope for this command before waiting on the operation.
Useful? React with 👍 / 👎.
| async registerWebhook(input: RegisterWebhookInput): Promise<RegisterWebhookResponse> { | ||
| return this.request<RegisterWebhookResponse>({ | ||
| method: "POST", | ||
| path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/webhooks`, |
There was a problem hiding this comment.
Remove or implement unhandled webhook routes
These new SDK methods point at /v1/workspaces/{id}/webhooks and related /webhooks/dlq routes, but I checked the service route switch in internal/httpapi/server.go and the only workspace webhook route currently handled is /webhooks/ingest; the authoritative OpenAPI file also has no subscription or DLQ paths. SDK consumers calling registerWebhook, listWebhooks, deleteWebhook, or the DLQ helpers against this service will therefore get 404s, so the server/OpenAPI contract needs to be added before exporting these methods.
Useful? React with 👍 / 👎.
| for _, path := range []string{ | ||
| filepath.Join(localDir, ".relayfile-mount-state.json"), | ||
| filepath.Join(localDir, mountsync.DefaultMountStateDirName, "state.json"), | ||
| } { | ||
| payload, err := os.ReadFile(path) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| var state struct { | ||
| IncrementalReadNotReadySince map[string]string `json:"incrementalReadNotReadySince"` | ||
| IncrementalBacklogDraining bool `json:"incrementalBacklogDraining"` | ||
| } | ||
| if json.Unmarshal(payload, &state) != nil { | ||
| continue | ||
| } | ||
| return len(state.IncrementalReadNotReadySince), state.IncrementalBacklogDraining | ||
| } | ||
| return 0, false |
There was a problem hiding this comment.
🚩 readLocalMountCursorHealth second path may never exist in practice
The function checks filepath.Join(localDir, mountsync.DefaultMountStateDirName, "state.json") which resolves to localDir/.relayfile-mount-state/state.json. However, DefaultMountStateDir() at internal/mountsync/state_path.go:43-48 returns $HOME/.relayfile-mount-state/ (not under localDir). The private mount state directory is typically NOT a subdirectory of the local mirror. The first path (localDir/.relayfile-mount-state.json) is the legacy location where the syncer writes internal state and is the one that will match in practice. The second path gracefully falls through via continue but may be dead code.
Was this helpful? React with 👍 or 👎 to provide feedback.
| async registerWebhook(input: RegisterWebhookInput): Promise<RegisterWebhookResponse> { | ||
| return this.request<RegisterWebhookResponse>({ | ||
| method: "POST", | ||
| path: `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/webhooks`, | ||
| correlationId: input.correlationId, | ||
| body: { | ||
| url: input.url, | ||
| pathGlobs: input.pathGlobs, | ||
| secret: input.secret | ||
| }, | ||
| signal: input.signal | ||
| }); | ||
| } | ||
|
|
||
| async listWebhooks( | ||
| workspaceId: string, | ||
| options: ListWebhooksOptions = {} | ||
| ): Promise<WebhookSubscription[]> { | ||
| return this.request<WebhookSubscription[]>({ | ||
| method: "GET", | ||
| path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`, | ||
| correlationId: options.correlationId, | ||
| signal: options.signal | ||
| }); | ||
| } | ||
|
|
||
| async deleteWebhook( | ||
| workspaceId: string, | ||
| subscriptionId: string, | ||
| options: DeleteWebhookOptions = {} | ||
| ): Promise<void> { | ||
| await this.performRequest({ | ||
| method: "DELETE", | ||
| path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(subscriptionId)}`, | ||
| correlationId: options.correlationId, | ||
| signal: options.signal | ||
| }); | ||
| } | ||
|
|
||
| async getWebhookDeadLetters( | ||
| workspaceId: string, | ||
| options: GetWebhookDeadLettersOptions = {} | ||
| ): Promise<WebhookDeliveryDeadLetterFeedResponse> { | ||
| const query = buildQuery({ | ||
| cursor: options.cursor, | ||
| limit: options.limit | ||
| }); | ||
| return this.request<WebhookDeliveryDeadLetterFeedResponse>({ | ||
| method: "GET", | ||
| path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/dlq${query}`, | ||
| correlationId: options.correlationId, | ||
| signal: options.signal | ||
| }); | ||
| } | ||
|
|
||
| async replayWebhookDeadLetter( | ||
| workspaceId: string, | ||
| deliveryId: string, | ||
| correlationId?: string, | ||
| signal?: AbortSignal | ||
| ): Promise<QueuedResponse> { | ||
| return this.request<QueuedResponse>({ | ||
| method: "POST", | ||
| path: `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/dlq/${encodeURIComponent(deliveryId)}/replay`, | ||
| correlationId, | ||
| signal | ||
| }); | ||
| } |
There was a problem hiding this comment.
🚩 SDK webhook methods assume server-side endpoints exist without OpenAPI verification
The TypeScript SDK adds registerWebhook, listWebhooks, deleteWebhook, getWebhookDeadLetters, and replayWebhookDeadLetter methods targeting endpoints like POST /v1/workspaces/{id}/webhooks and GET /v1/workspaces/{id}/webhooks/dlq. These are client-side additions only — there are no corresponding changes to internal/httpapi/server.go or openapi/relayfile-v1.openapi.yaml in this PR. This is fine if these endpoints already exist server-side and the SDK is catching up, but if they don't, these methods will fail at runtime.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Findings
Addressed Comments
Verification
|
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/relayfile-cli/main.go`:
- Around line 3016-3029: The error handling in the waitForWritebackOperation
call block incorrectly marks operations as failed when encountering timeout or
transient GET errors. Instead of immediately writing a failed receipt when
waitForWritebackOperation returns an error, you must distinguish between
transient errors (timeout, network issues) and terminal operation states. Only
write a failed receipt for confirmed terminal states like failed, dead_lettered,
or canceled; for timeout or network errors, leave the receipt as pending with
the opID, lastError set to the sanitized error, and a needsAttention flag set to
true. This ensures the local outbox state remains consistent with the cloud
operation state when the final status is unknown. Apply the same logic to the
sibling location at lines 3064-3091, ensuring both sites handle transient vs
terminal errors consistently by checking the operation's actual status before
writing the receipt.
- Around line 4412-4421: The stuck event count is currently only being read from
private cursor files via the readLocalMountCursorHealth function, but the public
sync state field IncrementalReadNotReadySince should be checked first. Modify
the code to read the stuck event count from state.IncrementalReadNotReadySince
as the baseline, then merge or take the maximum of that value with the result
from readLocalMountCursorHealth to ensure the report captures stuck events from
both the public state and private cursor health data.
- Around line 2853-2875: The writebackPushReceipt struct includes Content and
Encoding fields that expose user file contents in persisted and printed receipts
(acked/failed receipts and JSON output), creating a security/privacy risk. Clear
the Content and Encoding fields to empty strings in the writebackPushReceipt
struct before writing acked/failed receipts to disk or outputting as JSON, while
keeping these fields populated only for transient pending receipts. Apply this
redaction at the locations referenced in the comment (lines 3046-3047 and 3286)
where acked/failed receipts are persisted or printed, ensuring that user file
contents are not retained beyond the writeback operation itself.
- Around line 4390-4394: The resolveWorkspaceLikeStatus function currently falls
through to loadCredentials when called with an empty workspace value, but this
fails in Agent Relay scenarios where credentials.json is unavailable. Modify the
workspace resolution logic to check and resolve the workspace in this order
before falling back to legacy credentials: first check the RELAYFILE_WORKSPACE
environment variable, then check for a catalog default workspace, then check for
the active Agent Relay workspace, and only call loadCredentials if none of those
provide a valid local record. This ensures resolveWorkspaceLikeStatus handles
the empty input case by attempting these modern resolution methods before
attempting the legacy credentials fallback.
- Around line 3133-3139: The file validation in the writebackPushResolvedPath
resolution logic only checks if the path is a directory, but allows special
files like FIFOs, device nodes, and sockets which can cause os.ReadFile to hang
or read unintended data. After calling os.Stat and obtaining the info object,
replace the directory check with a validation that requires the file to be a
regular file by checking info.Mode().IsRegular(), which will reject both
directories and all special file types in a single check.
- Around line 3188-3200: The joinRemotePath function produces double slashes in
paths when the normalized root ends with a slash (e.g., `/linear/` concatenated
with `rel` becomes `/linear//file.json`). After calling
normalizeWritebackFailurePath on the root parameter, trim any trailing slashes
from root using strings.TrimSuffix before the logic that checks if root is empty
or constructs the final path. This ensures that the concatenation at the end of
the function (when returning root + "/" + rel or "/" + rel) never produces
double slashes.
- Around line 3094-3101: The ensureWritebackDelegatedCredentials function
validates that an existing delegated bundle has the required
requiredRelayfileScopes before returning, but the bootstrap path does not
perform the same validation after bootstrapDelegatedCredentialsFromAgentRelay
returns. After calling bootstrapDelegatedCredentialsFromAgentRelay to obtain a
new delegated bundle, you must verify that the returned bundle satisfies the
scope requirements by using the delegatedBundleHasScopes check before returning
success. If the bootstrapped bundle lacks the required scopes, the function
should return an appropriate error instead of proceeding.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b66b34cf-8a7c-4224-8262-538434b84770
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
.agentworkforce/trajectories/active/traj_dzjkresnewd3/trajectory.json.trajectories/completed/2026-06/traj_2ldfbem11vzl.json.trajectories/completed/2026-06/traj_2ldfbem11vzl.md.trajectories/index.jsoncmd/relayfile-cli/main.gocmd/relayfile-cli/main_test.gointernal/mountsync/syncer.gointernal/mountsync/syncer_test.gopackages/sdk/typescript/src/client.test.tspackages/sdk/typescript/src/client.tspackages/sdk/typescript/src/index.tspackages/sdk/typescript/src/types.ts
Review fixes (Gemini): - isWritebackDraftPath: use path.Base (slash-separated remote paths) instead of filepath.Base for Windows correctness; import "path". - waitForWritebackOperation: accept context.Context with the configured timeout; poll is now cancellable and tolerates transient errors until the deadline rather than aborting the push on the first glitch. Spec completion (AR-272 Parts 2a/2b/3, this repo): - Part 2a in-cycle stuck-event drain: a read 404 on a provider-layout-alias path (by-state/by-id/...) is a stale index event, so the cycle skips it immediately and keeps draining instead of holding the events cursor for the full read-not-ready TTL and exiting after one event. Canonical (non-alias) 404s keep the conservative retry behavior. - Part 2b: add `relayfile writeback skip-stuck [WS] [--max N] [--json]` operator escape hatch that walks the cursor past every read-404 event immediately (Syncer.SkipStuck) and reports the count skipped. - Part 3: mount cycle logs a stuck-event drain summary (N skipped; backlog remains -> use skip-stuck) instead of exiting silently mid-drain. Part 2c (by-state index emitter) is writer-side and lives in the cloud repo, out of scope here. Tests: alias in-cycle drain, skip-stuck consecutive 404 drain, draft-path slash handling; full ./cmd/relayfile-cli + ./internal/mountsync suites pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d96f1d4 to
1460bc6
Compare
|
Findings
Addressed comments
Verification No files were edited. Local verification passed with a temporary Go 1.22.12 toolchain on
I did not print |
- ops:read in delegated join scopes: a successful /fs/bulk returns an opID this
command then polls at GET /ops/{opId} (requires ops:read). [Codex]
- Redact file body from terminal receipts: clear Content/Encoding before
writing acked/failed receipts and JSON output; pending receipt keeps the body
and is written 0600. [CodeRabbit]
- Keep dispatched op pending on unknown final status: a poll timeout or
transient GET error after an opID is "unknown", not "failed" — record
pending+needsAttention instead of diverging local state with a failed
receipt. Only failed/dead_lettered/canceled write a failed receipt.
[CodeRabbit]
- Re-validate delegated scopes after bootstrap: fail loudly if the minted
bundle omits the required relayfile:fs:write scope. [CodeRabbit]
- resolveWritebackPushPath rejects non-regular files (FIFO/device/socket)
before os.ReadFile. [CodeRabbit]
- joinRemotePath trims a trailing slash on remoteRoot to avoid //double
slashes in the bulk-write path. [CodeRabbit]
- workspace status: use public IncrementalReadNotReadySince as the stuck-event
baseline, max'd with private cursor health; resolve env/agent-relay/catalog
default before legacy credentials so status works in the delegated flow.
[CodeRabbit]
SDK webhook-route findings (Codex/Devin on client.ts) are not applicable: that
code merged separately via #281 and is no longer in this PR's diff.
Tests: receipt redaction/perms/routing, isTerminalWritebackOpStatus, updated
push scope assertion. Full ./cmd/relayfile-cli + ./internal/mountsync pass;
contract check passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review addressed + spec completion (pushed
|
The v0.8.29 release bumped @relayfile/mount-* and @relayfile/core to 0.8.29 in the workspace package.json files but did not update the root package-lock.json, which still pinned 0.8.28. Because packages/sdk/typescript is a root workspace member, `npm ci` resolves against the root lockfile and failed the SDK deps install — breaking the contract, SDK Typecheck, E2E, and provider-eval jobs (Go Build/Test were unaffected). Regenerated via `npm install --package-lock-only`; the diff is the 0.8.28->0.8.29 bump with registry integrity hashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relayfile Eval ReviewRun: Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0 Human Review CasesNo reviewable human-review cases captured Relayfile output. |
Summary
relayfile writeback push <local-path>to send local mount writebacks directly to the cloud/fs/bulkendpoint and record pending/acked/failed receipts in the local outbox audit trail.relayfile workspace statuswith local sync health, stuck-event count fromincrementalReadNotReadystate, outbox pending/failed/acked counts, and last error.Scope
This is PR-A for AR-272. It intentionally does not include PR-B fresh-404 cursor drain / skip-stuck or PR-C by-state emitter retraction.
Reviewer Notes
writeback pushbootstraps delegated relayfile credentials through the Agent Relay cloud session, not legacycloud-credentials.json. The CLI test writes a stale legacy credential file and verifies the cloud session token is used.fs:write:/linear/**; the returned delegated bundle is required to include the compiled relayfile scoperelayfile:fs:write:/linear/**before the push proceeds.mount-writeback-create-draftkeyed by workspace, remote path, and content hash forfactory-create-*.jsonand existing draft-path writebacks.Tests
go test ./cmd/relayfile-cli ./internal/mountsyncE2E
Not run against real Linear from this development environment. Direct push, cloud-session credential selection, provider-scoped delegated-token minting, operation polling, outbox receipt writing, and status reporting are covered with synthetic fixtures and fake cloud/relayfile servers.