Skip to content

Add writeback draft content identity to mount bulk writes#253

Merged
khaliqgant merged 5 commits into
mainfrom
fix/writeback-content-identity
Jun 6, 2026
Merged

Add writeback draft content identity to mount bulk writes#253
khaliqgant merged 5 commits into
mainfrom
fix/writeback-content-identity

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

  • add optional contentIdentity to the relayfile bulk write wire type and OpenAPI schema
  • emit daemon-authoritative mount-writeback-create-draft identities for mountsync bulk writeback draft uploads
  • pin the cross-repo golden vector, snapshot.Hash lowercase SHA-256 hex semantics, and explicit ttlSeconds: 2592000 in Go tests

Locked #1977 contract

  • kind: mount-writeback-create-draft
  • key: workspaceId:normalizedRemotePath:contentHash
  • golden key: ws_test:/slack/channels/C123/messages/messages 5ab77d67.json:751f9591557700f69b5ceefcdec7ead8563a10f0a712c501a5028699be021511
  • contentHash: lowercase SHA-256 hex of raw/decoded file bytes, using existing snapshot.Hash directly
  • ttlSeconds: 2592000 (30 days), fixed-from-first-write coverage for ordinary replay windows; Cloud Phase 3 owns dirty STARTED sandboxes of any age

Sequencing / deploy path

Cloud Phase 1 receiver must merge and deploy first; this daemon field is inert until Cloud bulk writes honor contentIdentity. After this PR merges: publish relayfile-mount, bump RELAYFILE_MOUNT_VERSION in the Cloud snapshot config, run rebuild-snapshot.yml, promote the snapshot pin, then deploy Cloud so the new snapshot reaches prod. hn-monitor verification must be cross-run/persisted-draft replay, not a same-window false pass.

Verification

  • go test ./internal/mountsync -run 'TestBulkWrite_ContentIdentity|TestBulkWrite_FlushSendsContentIdentity|TestBulkWrite_ChunksBySerializedRequestSize'\n- go test ./internal/relayfile ./internal/httpapi ./internal/mountsync\n- go test ./cmd/... ./internal/mountfuse\n- scripts/check-contract-surface.sh\n- go test ./...\n- go build ./...\n\nReview gate: rf-review + claude-1 before merge. Do not merge before Cloud Phase 1 is live.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 21 minutes and 42 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: 1155b96b-c134-477b-9645-e83f474a006a

📥 Commits

Reviewing files that changed from the base of the PR and between a360a9e and 630f097.

📒 Files selected for processing (5)
  • internal/mountsync/syncer.go
  • internal/mountsync/syncer_test.go
  • internal/relayfile/draft_reconcile.go
  • packages/core/src/dedup.coverage.test.ts
  • packages/core/src/dedup.ts
📝 Walkthrough

Walkthrough

This PR adds persistent content identity tracking through the writeback pipeline. A new ContentIdentity type (with kind, key, and optional TTL) flows from bulk write requests through store operations, writeback queues, provider callbacks, and the pending writeback API. Mount sync now computes workspace-scoped identities for drafted content using keying on (workspaceID, path, hash).

Changes

ContentIdentity Writeback Propagation

Layer / File(s) Summary
ContentIdentity type contract and schema
internal/relayfile/store.go, internal/mountsync/types.go, packages/sdk/typescript/src/types.ts, openapi/relayfile-v1.openapi.yaml
New ContentIdentity struct with kind, key, and optional ttlSeconds. Extends BulkWriteFile, OperationStatus, WritebackAction, and WritebackQueueItem with optional identity fields. OpenAPI schema and TypeScript interface updated.
Store writeback pipeline integration
internal/relayfile/store.go
Store initialization reconstructs queued tasks with ContentIdentity. BulkWrite delegates to recordWriteWithContentIdentityLocked, which populates OperationStatus.ContentIdentity and embeds it in writeback tasks. GetPendingWritebacks conditionally includes contentIdentity in response. processWriteback constructs WritebackAction with ContentIdentity cloned from task or operation.
Mount sync bulk write generation
internal/mountsync/syncer.go
Adds mount-writeback-create-draft kind and TTL constants. Threads workspace ID through bulk write chunking via chunkPendingBulkWrites(workspace, ...) and file generation via bulkWriteFilesForPending(workspace, ...). Computes workspace-scoped ContentIdentity using (workspaceID, normalizedRemotePath, contentHash) keying. Updates chunk boundary checks to account for identity field size.
Store writeback tests
internal/relayfile/store_test.go
Two tests verify ContentIdentity flows through writeback callbacks and pending writeback API. First asserts ProviderWriteAction receives matching WritebackAction.ContentIdentity. Second verifies ExternalWritebackMode creates pending writeback with expected identity.
Mount sync bulk write tests
internal/mountsync/syncer_test.go
Three tests verify bulk write identity computation and propagation. Golden-vector test asserts computed kind/key/TTL. Determinism test verifies key stability across identical reuploads and key variance with content/path/workspace changes. Flush test asserts SyncOnce produces bulkWriteBatches with expected ContentIdentity.
SDK contract and client tests
packages/sdk/typescript/src/client.test.ts
Updates TypeScript client tests to verify ContentIdentity included in writeFile and bulkWrite requests with kind, derived key, and ttlSeconds. Request body assertions updated to expect full identity payload.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

size:XL

Poem

🐰 A writeback wish, from tail to toes—
Identity now through pipelines flows,
From requests made to tasks queued tight,
Content identity shines so bright!
Workspace-scoped keys, tested true,
All batches know just what to do.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add writeback draft content identity to mount bulk writes' accurately and specifically describes the main change—adding contentIdentity support to bulk writeback operations for mount drafts.
Description check ✅ Passed The description comprehensively covers the changeset: it explains the core feature (contentIdentity addition), implementation details (identity kind/key/TTL), deployment sequencing, verification steps, and review requirements—all directly aligned with the code changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/writeback-content-identity

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.

@khaliqgant
Copy link
Copy Markdown
Member Author

Additional daemon ground truth from review: preparePendingBulkWrite() already short-circuits clean, tracked, same-hash drafts (exists && !tracked.Dirty && tracked.Hash == snapshot.Hash). So this PR targets the #1977 snapshot-restore/untracked replay path where the draft is presented as new/dirty and gets bulk-flushed again. That keeps the Phase 2 scope focused: emit stable contentIdentity for the re-flush path; Cloud Phase 3 owns dirty STARTED sandbox reuse beyond the finite TTL.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 6, 2026

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-06-06T19-53-13-398Z-HEAD-provider
Mode: provider
Git SHA: db75e05

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.

@khaliqgant khaliqgant force-pushed the fix/writeback-content-identity branch from 5a0f89d to 52cb9ad Compare June 6, 2026 19:26
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 7 files

Re-trigger cubic

@agent-relay-code
Copy link
Copy Markdown
Contributor

Reviewed PR #253 and fixed a real propagation gap: contentIdentity was added by mountsync and accepted by HTTP, but relayfile dropped it before writeback execution. It now persists through operations and queue items, reaches provider write actions, and appears in pending writeback API responses. OpenAPI was updated for the operation and pending writeback schemas.

Added regression coverage in internal/relayfile/store_test.go for provider-action propagation and external pending writeback visibility.

Local verification passed:
go test ./internal/mountsync ./internal/relayfile ./internal/httpapi
scripts/check-contract-surface.sh
go test ./...

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@agent-relay-code
Copy link
Copy Markdown
Contributor

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

Reviewed PR #253 and fixed a real propagation gap: contentIdentity was added by mountsync and accepted by HTTP, but relayfile dropped it before writeback execution. It now persists through operations and queue items, reaches provider write actions, and appears in pending writeback API responses. OpenAPI was updated for the operation and pending writeback schemas.

Added regression coverage in internal/relayfile/store_test.go for provider-action propagation and external pending writeback visibility.

Local verification passed:
go test ./internal/mountsync ./internal/relayfile ./internal/httpapi
scripts/check-contract-surface.sh
go test ./...

@agent-relay-code
Copy link
Copy Markdown
Contributor

Reviewed PR #253 and made one focused fix: packages/core/src/dedup.ts now includes optional ttlSeconds on ContentIdentity, with the type coverage test updated. This keeps @relayfile/core aligned with the new OpenAPI/TypeScript SDK content identity contract.

Validation run locally:

  • scripts/check-contract-surface.sh
  • PATH=/tmp/go1.22.12/bin:$PATH go test ./internal/mountsync ./internal/httpapi ./internal/relayfile
  • npm test --workspace packages/core -- --run src/dedup.coverage.test.ts
  • npm run build --workspace packages/core
  • npm test --workspace packages/sdk/typescript -- src/client.test.ts
  • npm run build --workspace packages/sdk/typescript
  • npm run typecheck --workspace packages/sdk/typescript
  • git diff --check

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@agent-relay-code
Copy link
Copy Markdown
Contributor

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

Reviewed PR #253 and made one focused fix: packages/core/src/dedup.ts now includes optional ttlSeconds on ContentIdentity, with the type coverage test updated. This keeps @relayfile/core aligned with the new OpenAPI/TypeScript SDK content identity contract.

Validation run locally:

  • scripts/check-contract-surface.sh
  • PATH=/tmp/go1.22.12/bin:$PATH go test ./internal/mountsync ./internal/httpapi ./internal/relayfile
  • npm test --workspace packages/core -- --run src/dedup.coverage.test.ts
  • npm run build --workspace packages/core
  • npm test --workspace packages/sdk/typescript -- src/client.test.ts
  • npm run build --workspace packages/sdk/typescript
  • npm run typecheck --workspace packages/sdk/typescript
  • git diff --check

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/relayfile/store.go (1)

1417-1468: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject invalid contentIdentity values before recording the writeback.

BulkWrite now persists input.ContentIdentity as-is into ws.Ops and queued writebacks. That means empty kind/key or a negative ttlSeconds can leak back out through operation and pending-writeback responses even though the new contract requires non-empty identifiers and a non-negative TTL.

Proposed fix
 	for _, input := range files {
 		path := normalizePath(input.Path)
 		if strings.TrimSpace(input.Path) == "" {
 			errorsOut = append(errorsOut, BulkWriteError{
 				Path:    input.Path,
@@
 		if err := validateEncodedContent(input.Content, encoding); err != nil {
 			errorsOut = append(errorsOut, BulkWriteError{
 				Path:    path,
 				Code:    "invalid_content",
 				Message: err.Error(),
 			})
 			continue
 		}
+		if input.ContentIdentity != nil {
+			identity := input.ContentIdentity
+			if strings.TrimSpace(identity.Kind) == "" || strings.TrimSpace(identity.Key) == "" || identity.TTLSeconds < 0 {
+				errorsOut = append(errorsOut, BulkWriteError{
+					Path:    path,
+					Code:    "invalid_content_identity",
+					Message: "contentIdentity.kind, contentIdentity.key, and contentIdentity.ttlSeconds must be valid",
+				})
+				continue
+			}
+		}
 		contentType := strings.TrimSpace(input.ContentType)
 		if contentType == "" {
 			contentType = "text/markdown"
 		}
🤖 Prompt for 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.

In `@internal/relayfile/store.go` around lines 1417 - 1468, Reject invalid
input.ContentIdentity before calling recordWriteWithContentIdentityLocked by
validating its fields in the BulkWrite loop: ensure ContentIdentity.Kind and
ContentIdentity.Key are non-empty and ContentIdentity.TtlSeconds is >= 0; if
validation fails append a BulkWriteError (e.g., Code "invalid_content_identity"
and a clear Message) for that path and continue. Update the validation logic
near the loop that constructs file and before the call to
s.recordWriteWithContentIdentityLocked(ws, path, revision, eventType,
file.Provider, "", input.ContentIdentity) so invalid identities are never
persisted into ws.Ops or queued writebacks.
🤖 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 @.trajectories/completed/2026-06/traj_y5jru5dh9ku6/summary.md:
- Line 1: The trajectory title currently reads "Trajectory: Review and fix PR
`#243`" while the summary body references "PR `#253`"; update the inconsistent PR
reference so both places use the same PR number (choose the correct canonical ID
and replace the other), specifically edit the title string "Trajectory: Review
and fix PR `#243`" or the summary body occurrence of "PR `#253`" to match, and
verify no other occurrences in this file (summary.md) contain the mismatched PR
ID so completion metadata is consistent.

In @.trajectories/completed/2026-06/traj_y5jru5dh9ku6/trajectory.json:
- Line 5: The structured trajectory metadata has a PR number mismatch: the JSON
field "title" (and the related task.title) currently references PR `#243` while
the retrospective text refers to PR `#253`; update the "title"/task.title value to
match the retrospective (PR `#253`) or vice‑versa so both fields are identical
(e.g., change task.title/"title" to "Review and fix PR `#253`") to ensure
consistent historical indexing.

In `@internal/mountsync/syncer_test.go`:
- Around line 1842-1844: The test currently asserts identity.Kind against the
internal constant mountWritebackCreateDraftContentIdentityKind which can mask
contract drift; update the assertions in internal/mountsync/syncer_test.go to
compare identity.Kind to the literal string "mount-writeback-create-draft"
instead of the constant (replace uses of
mountWritebackCreateDraftContentIdentityKind with the wire literal) in both
places around the existing identity.Kind checks so the test verifies the actual
on-the-wire value.

In `@internal/relayfile/store.go`:
- Around line 3095-3106: The map currently assigns the internal *ContentIdentity
pointer directly to "contentIdentity" (using item.ContentIdentity and
op.ContentIdentity), which can let callers mutate store state; instead clone the
ContentIdentity instance before inserting into itemOut. Locate the block that
builds itemOut in store.go (the itemOut map and the conditional that checks
item.ContentIdentity and op.ContentIdentity) and replace direct assignments with
a shallow/deep copy (use the same cloning approach used in the writeback
path—e.g., call the existing ContentIdentity.Clone or construct a new
ContentIdentity and copy fields) so the map contains an independent copy rather
than the original pointer.

In `@packages/sdk/typescript/src/types.ts`:
- Around line 59-63: The response interfaces are missing the new contentIdentity
field: update the TypeScript types by importing/using ContentIdentity and adding
contentIdentity?: ContentIdentity to both OperationStatusResponse and
WritebackItem so SDK consumers can access the server/OpenAPI-provided
contentIdentity object.

---

Outside diff comments:
In `@internal/relayfile/store.go`:
- Around line 1417-1468: Reject invalid input.ContentIdentity before calling
recordWriteWithContentIdentityLocked by validating its fields in the BulkWrite
loop: ensure ContentIdentity.Kind and ContentIdentity.Key are non-empty and
ContentIdentity.TtlSeconds is >= 0; if validation fails append a BulkWriteError
(e.g., Code "invalid_content_identity" and a clear Message) for that path and
continue. Update the validation logic near the loop that constructs file and
before the call to s.recordWriteWithContentIdentityLocked(ws, path, revision,
eventType, file.Provider, "", input.ContentIdentity) so invalid identities are
never persisted into ws.Ops or queued writebacks.
🪄 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: a699d1e4-4d40-4487-96d9-5f64274b6125

📥 Commits

Reviewing files that changed from the base of the PR and between 66f7117 and a360a9e.

📒 Files selected for processing (12)
  • .trajectories/active/traj_y5jru5dh9ku6/trajectory.json
  • .trajectories/completed/2026-06/traj_y5jru5dh9ku6/summary.md
  • .trajectories/completed/2026-06/traj_y5jru5dh9ku6/trajectory.json
  • .trajectories/index.json
  • internal/mountsync/syncer.go
  • internal/mountsync/syncer_test.go
  • internal/mountsync/types.go
  • internal/relayfile/store.go
  • internal/relayfile/store_test.go
  • openapi/relayfile-v1.openapi.yaml
  • packages/sdk/typescript/src/client.test.ts
  • packages/sdk/typescript/src/types.ts
💤 Files with no reviewable changes (2)
  • .trajectories/active/traj_y5jru5dh9ku6/trajectory.json
  • .trajectories/index.json

Comment thread .trajectories/completed/2026-06/traj_y5jru5dh9ku6/summary.md Outdated
"id": "traj_y5jru5dh9ku6",
"version": 1,
"task": {
"title": "Review and fix PR #243"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix PR number mismatch in structured trajectory metadata.

task.title points to PR #243, but retrospective text documents PR #253. Keep both fields aligned to avoid incorrect historical indexing.

Suggested fix
-    "title": "Review and fix PR `#243`"
+    "title": "Review and fix PR `#253`"

Also applies to: 53-53

🤖 Prompt for 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.

In @.trajectories/completed/2026-06/traj_y5jru5dh9ku6/trajectory.json at line 5,
The structured trajectory metadata has a PR number mismatch: the JSON field
"title" (and the related task.title) currently references PR `#243` while the
retrospective text refers to PR `#253`; update the "title"/task.title value to
match the retrospective (PR `#253`) or vice‑versa so both fields are identical
(e.g., change task.title/"title" to "Review and fix PR `#253`") to ensure
consistent historical indexing.

Comment on lines +1842 to +1844
if identity.Kind != mountWritebackCreateDraftContentIdentityKind {
t.Fatalf("content identity kind = %q, want %q", identity.Kind, mountWritebackCreateDraftContentIdentityKind)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pin the ContentIdentity.Kind wire literal in these contract tests

Both assertions currently compare against an internal constant, so a constant drift could pass tests while still breaking the locked wire contract. Assert the literal "mount-writeback-create-draft" in the test path.

🔧 Suggested patch
@@
-	if identity.Kind != mountWritebackCreateDraftContentIdentityKind {
-		t.Fatalf("content identity kind = %q, want %q", identity.Kind, mountWritebackCreateDraftContentIdentityKind)
+	if identity.Kind != "mount-writeback-create-draft" {
+		t.Fatalf("content identity kind = %q, want %q", identity.Kind, "mount-writeback-create-draft")
 	}
@@
-	if identity.Kind != mountWritebackCreateDraftContentIdentityKind {
-		t.Fatalf("flushed identity kind = %q, want %q", identity.Kind, mountWritebackCreateDraftContentIdentityKind)
+	if identity.Kind != "mount-writeback-create-draft" {
+		t.Fatalf("flushed identity kind = %q, want %q", identity.Kind, "mount-writeback-create-draft")
 	}

Also applies to: 1947-1949

🤖 Prompt for 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.

In `@internal/mountsync/syncer_test.go` around lines 1842 - 1844, The test
currently asserts identity.Kind against the internal constant
mountWritebackCreateDraftContentIdentityKind which can mask contract drift;
update the assertions in internal/mountsync/syncer_test.go to compare
identity.Kind to the literal string "mount-writeback-create-draft" instead of
the constant (replace uses of mountWritebackCreateDraftContentIdentityKind with
the wire literal) in both places around the existing identity.Kind checks so the
test verifies the actual on-the-wire value.

Comment on lines +3095 to +3106
itemOut := map[string]any{
"id": item.OpID,
"workspaceId": item.WorkspaceID,
"path": item.Path,
"revision": item.Revision,
"correlationId": item.CorrelationID,
})
}
if item.ContentIdentity != nil {
itemOut["contentIdentity"] = item.ContentIdentity
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = op.ContentIdentity
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clone contentIdentity before returning pending items.

The response currently exposes the queue/op's internal *ContentIdentity pointer directly. In-process callers can mutate store state through the returned map. Return a clone here, the same way the writeback path already does.

Proposed fix
 		itemOut := map[string]any{
 			"id":            item.OpID,
 			"workspaceId":   item.WorkspaceID,
 			"path":          item.Path,
 			"revision":      item.Revision,
 			"correlationId": item.CorrelationID,
 		}
 		if item.ContentIdentity != nil {
-			itemOut["contentIdentity"] = item.ContentIdentity
+			itemOut["contentIdentity"] = cloneContentIdentity(item.ContentIdentity)
 		} else if op.ContentIdentity != nil {
-			itemOut["contentIdentity"] = op.ContentIdentity
+			itemOut["contentIdentity"] = cloneContentIdentity(op.ContentIdentity)
 		}
 		result = append(result, itemOut)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
itemOut := map[string]any{
"id": item.OpID,
"workspaceId": item.WorkspaceID,
"path": item.Path,
"revision": item.Revision,
"correlationId": item.CorrelationID,
})
}
if item.ContentIdentity != nil {
itemOut["contentIdentity"] = item.ContentIdentity
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = op.ContentIdentity
}
itemOut := map[string]any{
"id": item.OpID,
"workspaceId": item.WorkspaceID,
"path": item.Path,
"revision": item.Revision,
"correlationId": item.CorrelationID,
}
if item.ContentIdentity != nil {
itemOut["contentIdentity"] = cloneContentIdentity(item.ContentIdentity)
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = cloneContentIdentity(op.ContentIdentity)
}
🤖 Prompt for 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.

In `@internal/relayfile/store.go` around lines 3095 - 3106, The map currently
assigns the internal *ContentIdentity pointer directly to "contentIdentity"
(using item.ContentIdentity and op.ContentIdentity), which can let callers
mutate store state; instead clone the ContentIdentity instance before inserting
into itemOut. Locate the block that builds itemOut in store.go (the itemOut map
and the conditional that checks item.ContentIdentity and op.ContentIdentity) and
replace direct assignments with a shallow/deep copy (use the same cloning
approach used in the writeback path—e.g., call the existing
ContentIdentity.Clone or construct a new ContentIdentity and copy fields) so the
map contains an independent copy rather than the original pointer.

Comment on lines 59 to 63
export interface ContentIdentity {
kind: string;
key: string;
ttlSeconds?: number;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expose contentIdentity on the response types as well.

The server and OpenAPI now return this object from operation-status and pending-writeback payloads, but OperationStatusResponse and WritebackItem still omit it in this file. SDK consumers won't be able to read the new field even though the API sends it.

Proposed fix
 export interface OperationStatusResponse {
   opId: string;
   path?: string;
   revision?: string;
   action?: WritebackActionType;
   provider?: string;
   status: OperationStatus;
   attemptCount: number;
   nextAttemptAt?: string | null;
   lastError?: string | null;
   providerResult?: Record<string, unknown>;
+  contentIdentity?: ContentIdentity;
   correlationId?: string;
   createdAt?: string;
   updatedAt?: string;
   completedAt?: string | null;
 }
@@
 export interface WritebackItem {
   id: string;
   workspaceId: string;
   path: string;
   revision: string;
   correlationId: string;
+  contentIdentity?: ContentIdentity;
   state?: WritebackListState;
   provider?: string;
   action?: WritebackActionType;
   ts?: string;
   code?: WritebackDeadLetterErrorCode | string;
🤖 Prompt for 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.

In `@packages/sdk/typescript/src/types.ts` around lines 59 - 63, The response
interfaces are missing the new contentIdentity field: update the TypeScript
types by importing/using ContentIdentity and adding contentIdentity?:
ContentIdentity to both OperationStatusResponse and WritebackItem so SDK
consumers can access the server/OpenAPI-provided contentIdentity object.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 9 files (changes from recent commits).

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="internal/relayfile/store.go">

<violation number="1" location="internal/relayfile/store.go:3103">
P2: Return a cloned `ContentIdentity` here instead of the internal pointer to prevent callers from mutating store state through `GetPendingWritebacks` results.</violation>
</file>

<file name="openapi/relayfile-v1.openapi.yaml">

<violation number="1" location="openapi/relayfile-v1.openapi.yaml:2463">
P2: Update the TypeScript SDK response interfaces to include `contentIdentity`; the API contract now exposes this field, and leaving SDK response types unchanged will hide it from SDK consumers.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +3103 to +3105
itemOut["contentIdentity"] = item.ContentIdentity
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = op.ContentIdentity
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: Return a cloned ContentIdentity here instead of the internal pointer to prevent callers from mutating store state through GetPendingWritebacks results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/relayfile/store.go, line 3103:

<comment>Return a cloned `ContentIdentity` here instead of the internal pointer to prevent callers from mutating store state through `GetPendingWritebacks` results.</comment>

<file context>
@@ -3077,13 +3092,19 @@ func (s *Store) GetPendingWritebacks(workspaceID string) []map[string]any {
-		})
+		}
+		if item.ContentIdentity != nil {
+			itemOut["contentIdentity"] = item.ContentIdentity
+		} else if op.ContentIdentity != nil {
+			itemOut["contentIdentity"] = op.ContentIdentity
</file context>
Suggested change
itemOut["contentIdentity"] = item.ContentIdentity
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = op.ContentIdentity
itemOut["contentIdentity"] = cloneContentIdentity(item.ContentIdentity)
} else if op.ContentIdentity != nil {
itemOut["contentIdentity"] = cloneContentIdentity(op.ContentIdentity)

@@ -2460,6 +2460,8 @@ components:
providerResult:
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: Update the TypeScript SDK response interfaces to include contentIdentity; the API contract now exposes this field, and leaving SDK response types unchanged will hide it from SDK consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At openapi/relayfile-v1.openapi.yaml, line 2463:

<comment>Update the TypeScript SDK response interfaces to include `contentIdentity`; the API contract now exposes this field, and leaving SDK response types unchanged will hide it from SDK consumers.</comment>

<file context>
@@ -2460,6 +2460,8 @@ components:
         providerResult:
           type: object
           additionalProperties: true
+        contentIdentity:
+          $ref: '#/components/schemas/ContentIdentity'
         correlationId:
</file context>

Comment thread .trajectories/completed/2026-06/traj_y5jru5dh9ku6/trajectory.json Outdated
Comment thread .trajectories/completed/2026-06/traj_y5jru5dh9ku6/summary.md Outdated
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 6, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@khaliqgant khaliqgant merged commit 538cfee into main Jun 6, 2026
9 checks passed
@khaliqgant khaliqgant deleted the fix/writeback-content-identity branch June 6, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant