feat(AP-24): artifact pinning and revision retention#104
Conversation
Add pinned_at on artifacts and revision_retention_days on workspaces. Dashboard members can pin/unpin via web API; pinned artifacts skip auto deletion. Jobs retention cron marks non-current revisions retained, writes rd: denylist entries, and enqueues revision-scoped byte purge. Co-authored-by: Isaac Suttell <isaac@isaacsuttell.com>
AP-24 Phase 4: Add pinning and non-current Revision retention
ContextPhase 4 needs Pinned Artifacts, non-current Revision retention, and auto-deletion behavior that respects pinning. Source docs
ScopeAdd pinning state, retention policy for non-current Revisions, and auto-deletion behavior that respects Pinned Artifacts. Out of scopeDo not add billing plan-specific pinning limits unless a billing ticket has already provided the entitlement seam. DependenciesBlocked by jobs queue topology for automated retention enforcement. Implementation notesUse AcceptancePinned Artifacts are exempt from Auto Deletion, non-current Revisions are retained/deleted according to policy, and tests cover pinned/unpinned cases. VerificationRun DB/API/jobs tests and Remote Cursor handoffStart by reading |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
WalkthroughThis PR implements artifact pinning and non-current revision retention. It adds DB schema/migration and types (artifacts.pinned_at, workspaces.revision_retention_days), a PINNED_ARTIFACT_CAP config, new artifact pin/unpin repository methods and query helpers, web API routes/handlers and OpenAPI/error enums, retention discovery job and purge-side-effect helpers (denylist + revision-scoped byte purge enqueue), plus tests and docs updates. Sequence Diagram(s)sequenceDiagram
participant Client
participant API as Web API
participant Repo as Repository
participant DB as Database
Client->>API: POST /v1/web/artifacts/{id}/pin (Idempotency-Key)
API->>API: validate member principal, idempotency
API->>Repo: pinWebArtifact(actor,idempotencyKey,artifactId)
Repo->>DB: tryPinUnderCap / setPinnedAt
DB-->>Repo: pinned / cap_exceeded / not_found
Repo-->>API: WebArtifactDetail or error
API-->>Client: 200 WebArtifactDetail or 409 pinned_artifact_cap_exceeded
sequenceDiagram
participant Cron
participant Jobs as runRetentionDiscovery
participant Cmd as lifecycle.revision.retain
participant KV as DENYLIST
participant Queue as BYTE_PURGE_QUEUE
Cron->>Jobs: invoke runRetentionDiscovery(env, now)
Jobs->>DB: query eligible non-current published revisions
DB-->>Jobs: retentionCandidates[]
loop per candidate
Jobs->>KV: writeRevisionDenylist(rd:<revisionId>)
KV-->>Jobs: ok / fail
alt denylist ok
Jobs->>Queue: enqueueRevisionBytePurge(message with revision prefix)
Queue-->>Jobs: queued
Jobs->>Cmd: runCommand(systemActor, "lifecycle.revision.retain", revisionId)
Cmd-->>Jobs: retained? / replay?
end
end
Jobs-->>Cron: report discovered/enqueued/cap_hit
Possibly related issues
Possibly related PRs
🚥 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 docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-104.isaac-a46.workers.dev |
Co-authored-by: Isaac Suttell <isaac@isaacsuttell.com>
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-104.isaac-a46.workers.dev |
Co-authored-by: Isaac Suttell <isaac@isaacsuttell.com>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/ops/status/phase-backlog.md (1)
3-3:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the "Last updated" marker to reflect AP-24.
The "Last updated" line still references AP-22 (jobs lifecycle byte purge ownership), but this change marks AP-24 (pinning and revision retention) as complete on line 118. The marker should reflect the most recent update.
📝 Proposed fix
-Last updated: 2026-05-27 (AP-22 jobs lifecycle byte purge ownership). +Last updated: 2026-05-27 (AP-24 pinning and revision retention).🤖 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 `@docs/ops/status/phase-backlog.md` at line 3, Update the "Last updated" marker to reflect AP-24: replace the existing line "Last updated: 2026-05-27 (AP-22 jobs lifecycle byte purge ownership)" with "Last updated: 2026-05-27 (AP-24 pinning and revision retention)" so the header matches the change noted for AP-24 (the completion referenced elsewhere in this file).
🤖 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 `@apps/api/src/index.ts`:
- Around line 653-655: The call to runIdempotent wraps pinWebArtifact but
db.pinWebArtifact inside pinWebArtifact can throw
Error("pinned_artifact_cap_exceeded") which currently bubbles out as a 500;
update pinWebArtifact to catch errors from db.pinWebArtifact and map that
specific error to the proper contract error/response before returning (or
rethrowing) so runIdempotent never returns the raw DB error. Locate the
pinWebArtifact function (the call site used in runIdempotent) and add error
handling around the db.pinWebArtifact call to detect the
"pinned_artifact_cap_exceeded" message and convert it to the expected contract
error type/value.
In `@apps/jobs/README.md`:
- Line 25: Update the README table row for the "Retention" cron (schedule `0 * *
* *`) to fully describe the job: state that it marks non-current published
revisions as retained when `revision_retention_days` is set on a workspace,
writes `rd:` denylist keys for those retained revisions, and enqueues
revision-scoped byte purge tasks (the revision retention discovery job in the
stack context). Keep the cron schedule unchanged and use the exact terms
`revision_retention_days`, `rd:` denylist keys, and "revision-scoped byte purge"
so readers can map this description to the implementation.
In `@apps/jobs/src/discovery/retention.ts`:
- Around line 45-91: The current flow updates a revision to 'retained' inside
the runCommand handler (operation "lifecycle.revision.retain") before calling
applyRevisionPurgeSideEffects, which can fail and leave the row stranded because
discovery only looks for status='published'. Fix by either moving the DB update
so the status change to 'retained' happens only after
applyRevisionPurgeSideEffects succeeds, or by adding a recovery marker when the
handler succeeds but side-effects fail: e.g., have the handler record a durable
"retention_pending_purge" flag/column or an audit field (or re-emit a retryable
command) so a reconciler can re-run applyRevisionPurgeSideEffects; update code
references: modify the handler passed to runCommand, the SQL update of revisions
(currently setting status='retained'), and the call to
applyRevisionPurgeSideEffects to ensure side-effect failures either roll back
the status change or create a retryable recovery marker for later processing.
- Around line 17-20: The current preflight only checks env.BYTE_PURGE_QUEUE so
if env.DENYLIST is missing revisions can be marked retained while purge-side
effects are skipped; add a guard in the same preflight block to verify
env.DENYLIST (alongside BYTE_PURGE_QUEUE), call logOpError with the same or a
clear event key (e.g., "cron.queue_binding_missing" or "cron.denylist_missing")
when DENYLIST is absent, and return the same early result ({ discovered: 0,
enqueued: 0, cap_hit: false }) to avoid proceeding to retention logic (the code
path that marks revisions retained around the retention processing at/near Line
55).
In `@docs/ops/status/implementation.md`:
- Line 24: Update the `apps/jobs` retention description to explicitly state that
when `revision_retention_days` is set the retention cron not only marks
revisions as retained but also writes `rd:` denylist keys and enqueues a
revision-scoped byte purge job; mirror the wording/level of detail from the
changelog entry (lines mentioning the denylist `rd:` keys and enqueueing byte
purge) so the docs and changelog are consistent.
In `@packages/db/src/index.test.ts`:
- Around line 1015-1047: Add tests that enforce the 50-pin workspace cap by
exercising LocalRepository.pinWebArtifact until PINNED_ARTIFACT_CAP is reached
and asserting the repository returns or throws the expected
pinned_artifact_cap_exceeded error; use publishLocalArtifact to create
artifacts, resolveWebMember/verifyApiKey/getWebMemberByWorkOsUserId to obtain
actors, then call pinWebArtifact repeatedly and assert the final call fails with
the cap error and earlier calls succeed, and also add tests that check
idempotency of pinWebArtifact/unpinWebArtifact (calling with same idempotencyKey
returns the same result) and error cases (API key actor rejected,
cross-workspace artifact access, and missing artifact) to cover those branches.
In `@packages/db/src/queries/artifacts.ts`:
- Around line 138-146: The null-check in countPinned uses a raw SQL template
(sql`${artifacts.pinnedAt} is not null`); replace it with the Drizzle helper
isNotNull(artifacts.pinnedAt) for type safety and clarity and add the
corresponding import for isNotNull where other Drizzle helpers are imported so
the where clause becomes and(eq(artifacts.workspaceId, workspaceId),
eq(artifacts.status, "active"), isNotNull(artifacts.pinnedAt)).
In `@packages/db/src/repository/core.ts`:
- Around line 460-464: The check-then-write around
pinnedCount/PINNED_ARTIFACT_CAP is racy: replace the separate
entities.artifacts.countPinned(...) + setPinnedAt(artifact.id, ...) pattern with
an atomic DB-side operation (or a transaction with a proper lock) that enforces
the cap in one step; for example, perform a conditional update/insert that only
sets pinned_at when the number of pinned artifacts for member.workspace_id is <
PINNED_ARTIFACT_CAP, or execute setPinnedAt inside a serialized transaction that
locks the workspace’s artifact rows (or a workspace counter row) so only one
concurrent caller can increment/pin at the boundary, and throw
"pinned_artifact_cap_exceeded" when the DB conditional update affects 0 rows.
Ensure you modify the code paths using entities.artifacts.countPinned,
entities.artifacts.setPinnedAt, artifact.id and member.workspace_id accordingly.
---
Outside diff comments:
In `@docs/ops/status/phase-backlog.md`:
- Line 3: Update the "Last updated" marker to reflect AP-24: replace the
existing line "Last updated: 2026-05-27 (AP-22 jobs lifecycle byte purge
ownership)" with "Last updated: 2026-05-27 (AP-24 pinning and revision
retention)" so the header matches the change noted for AP-24 (the completion
referenced elsewhere in this file).
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: 5f5675ee-c463-4579-88b3-192e4a454b2a
📒 Files selected for processing (44)
apps/api/src/index.tsapps/jobs/README.mdapps/jobs/src/cron.tsapps/jobs/src/discovery/auto-deletion.tsapps/jobs/src/discovery/retention.tsapps/jobs/src/lifecycle.test.tsapps/jobs/src/lifecycle/revision-byte-purge-enqueue.tsapps/jobs/src/lifecycle/revision-denylist.tsapps/jobs/src/lifecycle/revision-prefix.tsapps/jobs/src/lifecycle/revision-purge-side-effects.tsdocs/ops/project-status.mddocs/ops/status/changelog.mddocs/ops/status/coverage.mddocs/ops/status/implementation.mddocs/ops/status/phase-backlog.mdpackages/config/src/index.tspackages/contracts/openapi/api.jsonpackages/contracts/openapi/content.jsonpackages/contracts/openapi/upload.jsonpackages/contracts/src/common.tspackages/contracts/src/mvp-contracts.test.tspackages/contracts/src/openapi/api.tspackages/contracts/src/routes.tspackages/db/migrations/0013_pinning_and_revision_retention.sqlpackages/db/snapshot/schema.sqlpackages/db/src/access-links.test.tspackages/db/src/agent-view.test.tspackages/db/src/index.test.tspackages/db/src/policy.tspackages/db/src/queries/artifacts.tspackages/db/src/queries/index.test.tspackages/db/src/queries/revisions.tspackages/db/src/queries/workspaces.tspackages/db/src/repository/core.tspackages/db/src/repository/interface.tspackages/db/src/repository/local-access-links.test.tspackages/db/src/repository/local-entities.tspackages/db/src/repository/ports.tspackages/db/src/repository/postgres-entities.test.tspackages/db/src/repository/postgres-entities.tspackages/db/src/repository/web-transforms.tspackages/db/src/schema.tspackages/db/src/types.tspackages/worker-runtime/src/errors.ts
| it("pins and unpins artifacts for web members", async () => { | ||
| const repo = new LocalRepository({ apiKeyPepper: "pepper" }); | ||
| const session = await repo.resolveWebMember({ | ||
| workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ", | ||
| email: "user@example.com", | ||
| idempotencyKey: "workos-jti:pin", | ||
| now: "2026-01-01T00:00:00.000Z", | ||
| }); | ||
| const keySecret = session.default_api_key?.secret; | ||
| const apiActor = keySecret ? await repo.verifyApiKey(keySecret) : null; | ||
| const webActor = await repo.getWebMemberByWorkOsUserId({ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ" }); | ||
| if (!apiActor || !webActor) { | ||
| throw new Error("expected actors"); | ||
| } | ||
| const published = await publishLocalArtifact(repo, apiActor, "pin-me", "2026-01-01T00:00:01.000Z"); | ||
|
|
||
| const pinned = await repo.pinWebArtifact({ | ||
| actor: webActor, | ||
| idempotencyKey: "idem-pin", | ||
| artifactId: published.artifact_id, | ||
| now: new Date("2026-01-02T00:00:00.000Z"), | ||
| }); | ||
| expect(pinned).toMatchObject({ id: published.artifact_id, pinned: true, auto_delete_at: null }); | ||
|
|
||
| const unpinned = await repo.unpinWebArtifact({ | ||
| actor: webActor, | ||
| idempotencyKey: "idem-unpin", | ||
| artifactId: published.artifact_id, | ||
| now: new Date("2026-01-03T00:00:00.000Z"), | ||
| }); | ||
| expect(unpinned).toMatchObject({ id: published.artifact_id, pinned: false }); | ||
| expect(unpinned.auto_delete_at).not.toBeNull(); | ||
| }); |
There was a problem hiding this comment.
Add test coverage for the 50-artifact pinning cap.
The test validates the happy path for pin/unpin, but the PR objectives explicitly state that pinning is capped at 50 per workspace (PINNED_ARTIFACT_CAP). There is no test exercising this cap enforcement or verifying that pinned_artifact_cap_exceeded is raised when the limit is reached. Given that branch coverage is currently below the 80% threshold, adding cap enforcement coverage would help close that gap.
Additionally, consider adding tests for:
- Idempotency of
pinWebArtifactandunpinWebArtifact(mentioned in the review stack context as implemented) - Error cases: API key actor rejection, cross-workspace artifact access, missing artifact
🧪 Suggested test structure for cap enforcement
+ it("enforces the 50-artifact pinning cap per workspace", async () => {
+ const repo = new LocalRepository({ apiKeyPepper: "pepper" });
+ const session = await repo.resolveWebMember({
+ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ",
+ email: "user@example.com",
+ idempotencyKey: "workos-jti:cap",
+ now: "2026-01-01T00:00:00.000Z",
+ });
+ const keySecret = session.default_api_key?.secret;
+ const apiActor = keySecret ? await repo.verifyApiKey(keySecret) : null;
+ const webActor = await repo.getWebMemberByWorkOsUserId({ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ" });
+ if (!apiActor || !webActor) {
+ throw new Error("expected actors");
+ }
+
+ // Publish and pin 50 artifacts
+ for (let i = 0; i < 50; i++) {
+ const published = await publishLocalArtifact(repo, apiActor, `artifact-${i}`, `2026-01-01T00:00:${String(i).padStart(2, "0")}.000Z`);
+ await repo.pinWebArtifact({
+ actor: webActor,
+ idempotencyKey: `idem-pin-${i}`,
+ artifactId: published.artifact_id,
+ now: new Date(`2026-01-02T00:00:${String(i).padStart(2, "0")}.000Z`),
+ });
+ }
+
+ // 51st pin should fail with cap exceeded
+ const overflow = await publishLocalArtifact(repo, apiActor, "overflow", "2026-01-01T01:00:00.000Z");
+ await expect(
+ repo.pinWebArtifact({
+ actor: webActor,
+ idempotencyKey: "idem-pin-overflow",
+ artifactId: overflow.artifact_id,
+ now: new Date("2026-01-02T01:00:00.000Z"),
+ }),
+ ).rejects.toThrow("pinned_artifact_cap_exceeded");
+ });📝 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.
| it("pins and unpins artifacts for web members", async () => { | |
| const repo = new LocalRepository({ apiKeyPepper: "pepper" }); | |
| const session = await repo.resolveWebMember({ | |
| workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ", | |
| email: "user@example.com", | |
| idempotencyKey: "workos-jti:pin", | |
| now: "2026-01-01T00:00:00.000Z", | |
| }); | |
| const keySecret = session.default_api_key?.secret; | |
| const apiActor = keySecret ? await repo.verifyApiKey(keySecret) : null; | |
| const webActor = await repo.getWebMemberByWorkOsUserId({ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ" }); | |
| if (!apiActor || !webActor) { | |
| throw new Error("expected actors"); | |
| } | |
| const published = await publishLocalArtifact(repo, apiActor, "pin-me", "2026-01-01T00:00:01.000Z"); | |
| const pinned = await repo.pinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: "idem-pin", | |
| artifactId: published.artifact_id, | |
| now: new Date("2026-01-02T00:00:00.000Z"), | |
| }); | |
| expect(pinned).toMatchObject({ id: published.artifact_id, pinned: true, auto_delete_at: null }); | |
| const unpinned = await repo.unpinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: "idem-unpin", | |
| artifactId: published.artifact_id, | |
| now: new Date("2026-01-03T00:00:00.000Z"), | |
| }); | |
| expect(unpinned).toMatchObject({ id: published.artifact_id, pinned: false }); | |
| expect(unpinned.auto_delete_at).not.toBeNull(); | |
| }); | |
| it("pins and unpins artifacts for web members", async () => { | |
| const repo = new LocalRepository({ apiKeyPepper: "pepper" }); | |
| const session = await repo.resolveWebMember({ | |
| workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ", | |
| email: "user@example.com", | |
| idempotencyKey: "workos-jti:pin", | |
| now: "2026-01-01T00:00:00.000Z", | |
| }); | |
| const keySecret = session.default_api_key?.secret; | |
| const apiActor = keySecret ? await repo.verifyApiKey(keySecret) : null; | |
| const webActor = await repo.getWebMemberByWorkOsUserId({ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ" }); | |
| if (!apiActor || !webActor) { | |
| throw new Error("expected actors"); | |
| } | |
| const published = await publishLocalArtifact(repo, apiActor, "pin-me", "2026-01-01T00:00:01.000Z"); | |
| const pinned = await repo.pinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: "idem-pin", | |
| artifactId: published.artifact_id, | |
| now: new Date("2026-01-02T00:00:00.000Z"), | |
| }); | |
| expect(pinned).toMatchObject({ id: published.artifact_id, pinned: true, auto_delete_at: null }); | |
| const unpinned = await repo.unpinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: "idem-unpin", | |
| artifactId: published.artifact_id, | |
| now: new Date("2026-01-03T00:00:00.000Z"), | |
| }); | |
| expect(unpinned).toMatchObject({ id: published.artifact_id, pinned: false }); | |
| expect(unpinned.auto_delete_at).not.toBeNull(); | |
| }); | |
| it("enforces the 50-artifact pinning cap per workspace", async () => { | |
| const repo = new LocalRepository({ apiKeyPepper: "pepper" }); | |
| const session = await repo.resolveWebMember({ | |
| workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ", | |
| email: "user@example.com", | |
| idempotencyKey: "workos-jti:cap", | |
| now: "2026-01-01T00:00:00.000Z", | |
| }); | |
| const keySecret = session.default_api_key?.secret; | |
| const apiActor = keySecret ? await repo.verifyApiKey(keySecret) : null; | |
| const webActor = await repo.getWebMemberByWorkOsUserId({ workosUserId: "user_01J5K7Y8G9H0ABCDEFGHJKMNPQ" }); | |
| if (!apiActor || !webActor) { | |
| throw new Error("expected actors"); | |
| } | |
| // Publish and pin 50 artifacts | |
| for (let i = 0; i < 50; i++) { | |
| const published = await publishLocalArtifact(repo, apiActor, `artifact-${i}`, `2026-01-01T00:00:${String(i).padStart(2, "0")}.000Z`); | |
| await repo.pinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: `idem-pin-${i}`, | |
| artifactId: published.artifact_id, | |
| now: new Date(`2026-01-02T00:00:${String(i).padStart(2, "0")}.000Z`), | |
| }); | |
| } | |
| // 51st pin should fail with cap exceeded | |
| const overflow = await publishLocalArtifact(repo, apiActor, "overflow", "2026-01-01T01:00:00.000Z"); | |
| await expect( | |
| repo.pinWebArtifact({ | |
| actor: webActor, | |
| idempotencyKey: "idem-pin-overflow", | |
| artifactId: overflow.artifact_id, | |
| now: new Date("2026-01-02T01:00:00.000Z"), | |
| }), | |
| ).rejects.toThrow("pinned_artifact_cap_exceeded"); | |
| }); |
🤖 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/db/src/index.test.ts` around lines 1015 - 1047, Add tests that
enforce the 50-pin workspace cap by exercising LocalRepository.pinWebArtifact
until PINNED_ARTIFACT_CAP is reached and asserting the repository returns or
throws the expected pinned_artifact_cap_exceeded error; use publishLocalArtifact
to create artifacts, resolveWebMember/verifyApiKey/getWebMemberByWorkOsUserId to
obtain actors, then call pinWebArtifact repeatedly and assert the final call
fails with the cap error and earlier calls succeed, and also add tests that
check idempotency of pinWebArtifact/unpinWebArtifact (calling with same
idempotencyKey returns the same result) and error cases (API key actor rejected,
cross-workspace artifact access, and missing artifact) to cover those branches.
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-104.isaac-a46.workers.dev |
Exercise artifact countPinned/setPinnedAt query paths and retention/auto-deletion cap_hit, no-op updates, and per-row error handling so CI stays above the 80% branch threshold. Co-authored-by: Isaac Suttell <isaac@isaacsuttell.com>
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-104.isaac-a46.workers.dev |
Map pinned_artifact_cap_exceeded to 409 on web pin routes, preflight retention bindings, run revision purge side effects before marking retained, enforce pin cap atomically, and update jobs/docs/tests. Co-authored-by: Isaac Suttell <isaac@isaacsuttell.com>
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-104.isaac-a46.workers.dev |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary
Implements Phase 4 pinning and non-current revision retention.
Base:
origin/mainat1ad2436(AP-23 merged).Pinning
artifacts.pinned_at+ migration0013_pinning_and_revision_retention.sqlPOST /v1/web/artifacts/{id}/pinand/unpin(50/workspace cap)pinned_at is not nullRevision retention
workspaces.revision_retention_days(nullable)rd:denylist → revision-scoped byte purgeTests / coverage
pnpm test:coveragebranch coverage 80.18% (was 78.84%)pnpm verify(76 tasks)Deploy / hosted
0013before releaseLinear Issue: AP-24
Summary by CodeRabbit
New Features
Documentation