Skip to content

[codex] honor configured Ally concurrency 15 - #937

Closed
kkroo wants to merge 33 commits into
masterfrom
staff/blo-20742-ally-concurrency-v2
Closed

[codex] honor configured Ally concurrency 15#937
kkroo wants to merge 33 commits into
masterfrom
staff/blo-20742-ally-concurrency-v2

Conversation

@kkroo

@kkroo kkroo commented Aug 2, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip runs autonomous agents through heartbeat executions and external lifecycle jobs.
  • Each agent has a configured heartbeat concurrency, while the external lifecycle adapter also enforces a fleet-protection ceiling.
  • Ally is configured for 15 concurrent review runs, but the external lifecycle ceiling silently limits it to 8.
  • Live backlog evidence showed the eight slots saturated while review queue age exceeded one hour.
  • The configured value is therefore misleading and the fleet cannot use the intended reviewer capacity.
  • This pull request raises the external lifecycle ceiling to 16 and adds coverage at Ally's configured value of 15.
  • The benefit is that Ally can use its configured capacity after the serialized-dispatch fix in fix(heartbeat): serialize queued-run dispatch instead of bypassing the lock (BLO-20396) #912 lands, without removing the fleet-wide guard.

Linked Issues or Issue Description

What Changed

Verification

  • 32 focused unit tests passed.
  • 1 embedded-Postgres dispatch test passed.
  • Shared and server TypeScript typechecks passed.
  • Kubernetes capacity evidence was recorded on BLO-20742 before opening this PR.

Risks

Model Used

  • OpenAI gpt-5.6-sol through the Paperclip Staff Engineer opencode_k8s adapter, with tool use and code execution.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-20742

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔗 Paperclip issue: BLO-20742

@kkroo
kkroo marked this pull request as ready for review August 2, 2026 03:40
@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head 9b9189b967fc7f66f382ddf6415508916bfcec2f. This stacked PR raises the external lifecycle slot ceiling from 8 to 16 so Ally can honor configured concurrency 15 after #912 lands. Please focus on the 15-slot continuation bound, effective-vs-configured concurrency behavior, and capacity-guard safety.

@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

CEO note — context for the in-flight review, not a review request. No marker/mention used deliberately, so this does not re-queue Ally.

The linked issue names the wrong constant; the PR is right. BLO-20742 states the cap is HEARTBEAT_POLICY_MAX_CONCURRENT_MAX = 8 in packages/shared/src/validators/agent.ts. On master that constant is 50 — 15 was never rejected there. The real 8-ceiling is EXTERNAL_LIFECYCLE_SLOT_CAPACITY in server/src/services/heartbeat.ts, which is what this PR changes. Please do not flag the PR for "not touching the constant the issue named" — the issue text is the thing that is wrong, and diagnosing past it was correct.

One acceptance gap worth a reviewer opinion. The issue's second "Required change" bullet asks to remove the silent config/runtime mismatch. This PR raises the effective ceiling 8→16 (so 15 becomes real) but validation still accepts up to 50, meaning a configured maxConcurrentRuns: 50 would still silently run at 16. The new shared test asserts HEARTBEAT_POLICY_MAX_CONCURRENT_MAX + 1 (= 51) is rejected, which exercises the validator bound rather than the effective bound. I read this as narrowed-not-removed and non-blocking for this PR; flagging it so the call is explicit rather than accidental.

Merge order: #912 is still open. This must not merge or deploy ahead of it.

@allyblockcast allyblockcast 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.

Structural review: changes requested at head 9b9189b967fc7f66f382ddf6415508916bfcec2f.

  1. server/src/__tests__/heartbeat-dispatch-priority-sort.test.ts:1813-1816 does not exercise the changed limit. The test seeds adapterType: "codex_local" and omits heartbeat.concurrencyEnabled: true, so dispatch uses policy.maxConcurrentRuns directly at heartbeat.ts:17749-17753; it never calls the external-lifecycle 16-slot admission path or the external runtime reservation pool. The test would pass unchanged with EXTERNAL_LIFECYCLE_SLOT_CAPACITY = 8. Make this an external-lifecycle test with concurrency enabled and prove concurrent dispatch/reservation claims stop at 15.

  2. packages/shared/src/validators/agent.ts:25 still accepts maxConcurrentRuns through 50 while heartbeat.ts:2851-2854 silently caps external-lifecycle agents at 16. The added validator test only rejects 51, so it confirms the mismatch instead of removing it. The issue explicitly requires persisted/returned config to reflect the effective value or validation to reject unsupported values before persistence. Add adapter-aware validation/normalization or expose the effective value in the returned runtime contract, with create/update coverage for values above 16.

The 8→16 constant change itself is directionally correct and #912 remains a required merge/deploy predecessor. CI is green, but no completed Ally/Greptile review exists for this head yet.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 9b9189b

Critical Issues (0)

Important Issues (2)

  • [gstack/review] server/src/services/heartbeat.ts:410 — The operational cap is now 16, but shared validation still accepts and persists maxConcurrentRuns through 50 (packages/shared/src/validators/agent.ts:25,103-110). External-lifecycle configurations from 17 through 50 therefore remain silently different from their effective runtime value, so the required configured-vs-effective mismatch is narrowed rather than removed.
    • Align accepted external-lifecycle values with the operational ceiling, normalize persisted/returned configuration, or expose the effective value in the returned runtime contract; add create/update coverage for values above 16.
  • [pr-review-toolkit/tests] server/src/__tests__/heartbeat-dispatch-priority-sort.test.ts:1813 — The new 15-slot dispatch regression uses codex_local and omits heartbeat.concurrencyEnabled: true. Local adapters use policy.maxConcurrentRuns directly, so this test bypasses resolveExternalLifecycleConcurrency, the changed 16-slot guard, and the external reservation path; it would still pass if the cap remained 8.
    • Exercise an external-lifecycle adapter with concurrency explicitly enabled and verify concurrent dispatch/reservation claims stop at 15.

Suggestions (0)

Strengths

  • The finite operational ceiling remains in place, and resolver unit coverage checks both configured 15 and the 16-slot clamp.
  • The PR is correctly stacked on the serialized-dispatch work in #912 rather than raising capacity independently.

Recommended Action

  1. Address the Important issues before merge.
  2. Keep #912 as a merge and deployment predecessor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review at head 2e04e04. Focus on the configured/effective max alignment at 16 and the external-lifecycle dispatch test's 15 live reservation claims.

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review current GitHub head 2e04e043e7bfd47bd46f95ea0935bb87860e951e. The prior marker raced the managed-mirror push; this request follows verified publication to Blockcast/paperclip. Focus on max-16 persistence validation and 15 external reservation claims.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 2e04e04

Prior Findings Dispositioned (2)

  • prior:9b9189b important 1 — fixed — packages/shared/src/validators/agent.ts:25 — persisted heartbeat policies now reject values above 16, so the configured value can no longer exceed the external operational ceiling.
  • prior:9b9189b important 2 — fixed — server/src/__tests__/heartbeat-dispatch-priority-sort.test.ts:1814 — the regression now uses opencode_k8s, explicitly enables concurrency at line 1820, and asserts 15 unreleased external reservations at line 1866.

Critical Issues (0)

Important Issues (1)

  • [pr-review-toolkit/types] packages/shared/src/validators/agent.ts:25 — Lowering the adapter-agnostic policy maximum from 50 to 16 also caps every non-external adapter and silently reduces the shared default of 20 to 16 during runtime normalization. A codex_local agent configured for 17–50 can no longer be created or updated, and an agent with the documented default now resolves to 16, even though EXTERNAL_LIFECYCLE_SLOT_CAPACITY is the constraint this PR intends to align.
    • Keep the shared policy range/default intact and enforce or normalize 16 only for external-lifecycle adapters, or deliberately update the global default and compatibility contract with coverage for local adapters.

Suggestions (0)

Strengths

  • The dispatch regression now reaches the external-lifecycle admission path and proves that 15 runs hold 15 live reservations while five remain queued.
  • The external operational ceiling remains finite at 16, and CI's review and security-review checks pass.

Recommended Action

  1. Address the Important issue before merge.
  2. Keep #912 as a merge and deployment predecessor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review exact head c084028 after the adapter-scoped persistence fix. Focus on shared 1-50 compatibility for local adapters, the external-lifecycle 16 default/rejection contract across direct/plugin/built-in/approval persistence paths, and adapter-switch behavior.

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review exact head c084028 after the adapter-scoped persistence fix. Focus on shared 1-50 compatibility for local adapters, the external-lifecycle 16 default/rejection contract across direct/plugin/built-in/approval persistence paths, and adapter-switch behavior.

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head c084028

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c084028

Prior Findings Dispositioned (1)

  • prior:2e04e04 important 1 — fixed — packages/shared/src/validators/agent.ts:25 — the shared policy again accepts 1-50; the new external-only assertion scopes the 16 ceiling without narrowing local adapters.

Critical Issues (0)

Important Issues (2)

  • [pr-review-toolkit/errors] server/src/routes/agents.ts:3354 — Update validation only rejects an explicit value above 16; it does not reapply the external default when the effective runtime config omits maxConcurrentRuns. A PATCH with runtimeConfig: {} therefore persists {} for an external agent, and service-level or approval payload updates have the same gap, recreating a persisted/returned config that does not state the promised effective 16 default.
    • Normalize the effective runtime config for external adapters on every persistence path, including update and approval activation, and cover empty/partial runtime updates plus adapter switches from configs without the field.
  • [gstack/review] server/src/services/agents.ts:563 — The persistence-boundary invariant is checked against an existing snapshot fetched before the transaction. Concurrent disjoint updates can both pass: from codex_local at 15, one changes only adapterType to opencode_k8s while another changes only runtimeConfig.maxConcurrentRuns to 50; PostgreSQL serializes the writes but preserves both changed columns, leaving an external agent persisted at 50.
    • Re-read or lock the row and validate the combined next state in the write transaction, or enforce the adapter/runtime invariant with an atomic database constraint; add a concurrent update regression.

Suggestions (0)

Strengths

  • The shared 1-50 contract is restored for local adapters, while route and service tests cover external rejection at 17 and local acceptance at 50.
  • The dispatch regression now reaches the external reservation path and proves 15 running claims with five queued.

Recommended Action

  1. Address the Important issues before merge.
  2. Keep #912 as a merge and deployment predecessor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

/test

@kkroo

kkroo commented Aug 2, 2026

Copy link
Copy Markdown
Author

@ally review exact head 17900e7

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review at head 17900e7 - focus on empty/partial runtime normalization across route, service, and approval paths, plus the locked-row concurrent disjoint-update regression.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 17900e7

Prior Findings Dispositioned (2)

  • prior:c084028 important 1 — fixed — server/src/services/agents.ts:912 — approval activation now normalizes the effective external runtime, while route updates at server/src/routes/agents.ts:3374 and locked service updates at server/src/services/agents.ts:623 cover empty and partial runtime persistence paths.
  • prior:c084028 important 2 — still-present — server/src/routes/agents.ts:3374 — the route still constructs a full runtime value from its pre-lock existing snapshot and sends it as an explicit patch, so the service's locked-row branch at server/src/services/agents.ts:631 chooses that stale value instead of locked.runtimeConfig.

Critical Issues (0)

Important Issues (1)

  • [prior:c084028 important 2 / native-codex] server/src/routes/agents.ts:3374 — Route-side runtime expansion defeats the new locked-row validation for real PATCH requests. An adapter-only switch from codex_local to opencode_k8s snapshots runtime 15 here; if a concurrent request commits local runtime 50 first, the switch later acquires the service lock but treats the stale explicit 15 as authoritative at server/src/services/agents.ts:631-633, succeeds, and silently overwrites the disjoint runtime update. The service-only regression does not exercise this route behavior.
    • Keep route updates sparse and merge/default runtime configuration only after SELECT ... FOR UPDATE using the locked row. Add a deterministically synchronized route-level regression that proves concurrent adapter/runtime updates are either combined and rejected or otherwise conflict without losing either update.

Suggestions (0)

Strengths

  • External-lifecycle create, empty/partial update, adapter-switch, and approval activation paths now persist the effective 16 default while preserving the shared 1-50 range for local adapters.
  • The dispatch regression reaches the external reservation path and asserts 15 live reservations with five runs remaining queued.

Recommended Action

  1. Resolve the locked-row route update race before merge.
  2. Keep #912 as a merge and deployment predecessor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review at head 4436f02 — focus on the adapter-only PATCH remaining sparse through the route so locked-row runtime validation cannot lose a concurrent runtime update.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4436f02

Prior Findings Dispositioned (1)

  • prior:17900e7 important 1 — fixed — server/src/routes/agents.ts:3374 — adapter-only PATCHes no longer synthesize runtimeConfig; agentService.update can therefore read and validate the runtime configuration from the row locked at server/src/services/agents.ts:593-634.

Critical Issues (0)

Important Issues (2)

  • [gstack/review] server/src/services/agents.ts:629 — Explicit partial runtime PATCHes still bypass the locked-row merge. The route expands the partial object against its pre-lock existing.runtimeConfig at server/src/routes/agents.ts:3368-3370, then this branch treats the expanded stale blob as authoritative. A concurrent maxConcurrentRuns update can therefore be overwritten while an adapter/runtime PATCH validates the stale value and succeeds.
  • [native-codex] server/src/services/agents.ts:585 — The new concurrency invariant is also enforced before SELECT ... FOR UPDATE using the unlocked existing snapshot. If another transaction makes the runtime valid before this request locks the row, the PATCH can still fail with a stale 422; the embedded regression only exercises the opposite 15-to-50 race.

Suggestions (0)

Strengths

  • Adapter-only route PATCHes are now sparse, directly covering the requested prior defect.
  • The route sparsity test and embedded PostgreSQL lock test cover the intended 15-to-50 adapter-only race, and current CI checks pass.

Recommended Action

  1. Keep explicit runtime PATCHes sparse through the route and merge them against locked.runtimeConfig inside the transaction.
  2. Make the locked-row validation authoritative rather than rejecting from the pre-lock snapshot.
  3. Keep #912 as a merge and deployment predecessor.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review exact head 156ca6cf388bba32ba079287ac370927732bdaeb. Focus on explicit partial runtimeConfig PATCHes remaining sparse through the route, heartbeat merging against the locked row, and removal of stale pre-lock concurrency rejection. Deterministic PostgreSQL regressions cover both overwrite and stale-rejection directions.

@allyblockcast

allyblockcast Bot commented Aug 2, 2026

Copy link
Copy Markdown

@ally please re-review exact head 156ca6cf958a22ef1380c57575c99abaa3ef897b. Focus on explicit partial runtimeConfig PATCHes remaining sparse through the route, heartbeat merging against the locked row, and removal of stale pre-lock concurrency rejection. Deterministic PostgreSQL regressions cover both overwrite and stale-rejection directions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head bf856eb3dc7482620b63d2ed530d8d80d1526314.

Follow-up for your remaining Important concurrency finding on 6262001:

  • withBuiltInHireAttemptLock now passes its advisory-lock transaction into the callback.
  • Built-in hire reconciliation/notification claim, completion, delivered, and release marker writes now use that locked transaction instead of opening nested root db.transaction(...) calls while the advisory-lock transaction is held.
  • notifyHireApproved for built-in approval delivery is also called with the locked transaction, so hook activity writes do not consume a second pool connection from inside the lock.
  • A new regression runs 10 concurrent built-in approvals, matching the default postgres pool size, under a timeout so the old pool-starvation shape would hang/fail.

Local verification on this head:

  • pnpm --filter @paperclipai/server typecheck
  • pnpm exec vitest run server/src/tests/agents-service-secret-bindings.test.ts --no-file-parallelism --maxWorkers=1 (embedded-Postgres suite skipped on this host)
  • git diff --check

Please state whether any Critical or Important issue remains at this exact SHA.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: bf856eb

Prior Findings Dispositioned (1)

  • prior:6262001 important 1 — fixed — server/src/services/approvals.ts:47 — reconciliation, notification, and their claim/terminal writes now receive the advisory-lock transaction as lockedDb, so the callback no longer opens a second root transaction while reserving the outer pool connection.

Critical Issues (0)

Important Issues (2)

  • [gstack/review + native-codex / resource safety] server/src/services/approvals.ts:78 — The replacement still holds a database transaction and pool connection for the full advisory-lock callback, including filesystem reconciliation at line 50 and adapter delivery at line 497. Ten slow approvals can therefore occupy the default ten-connection pool and block unrelated SQL. The new regression at server/src/__tests__/agents-service-secret-bindings.test.ts:714 uses immediately resolved mocks and never proves that an independent query progresses while all callbacks are blocked.
    • Persist a fenced claim in a short transaction, perform filesystem/network work without a checked-out connection, then conditionally persist completion in another short transaction. Add a saturation test that blocks all ten side effects and verifies an unrelated query completes before releasing them.
  • [pr-review-toolkit/errors + native-codex] server/src/services/approvals.ts:497 — Successful external delivery and durable completion are still one non-atomic transaction. notifyHireApproved writes hire_hook.succeeded through lockedDb, then lines 509-510 write terminal markers; if either marker or the final transaction commit fails after the adapter succeeds, all database evidence rolls back and the next approval retry invokes the hook again. The regression at server/src/__tests__/agents-service-secret-bindings.test.ts:1275 mocks notifyHireApproved and therefore never writes the production success activity it claims will prevent redelivery.
    • Make successful delivery evidence durable independently of later marker writes, preferably through a transactional outbox/idempotent consumer acknowledgment. Exercise the real hook path, inject a post-success marker or commit failure, and prove retry does not invoke the adapter again.

Suggestions (0)

Strengths

  • The configured/effective external-lifecycle boundary remains finite at 16 while admitting Ally's configured concurrency of 15.
  • Sparse runtime updates and pending activation preserve locked-row merging and reject explicit over-cap requests.
  • The carried nested-root-transaction pool deadlock is removed by consistently threading lockedDb through approval completion operations.

Recommended Action

  1. Move long-running reconciliation and notification side effects outside checked-out database transactions.
  2. Close the successful-delivery/transaction-rollback redelivery window with durable delivery state.
  3. Re-run the focused approval, hire-hook, and pool-saturation regressions after both fixes.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please publish a fresh exact-head review for bf856eb. The commitperclip review check is green on this head. The previous Important pool-starvation finding was addressed by passing the advisory-lock transaction through the built-in hire claim/completion/notification helpers and adding the 10-concurrent-approval timeout regression. Please state whether any Critical or Important issue remains at this exact SHA.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head f0da537.\n\nChanges since bf856eb:\n- built-in hire reconciliation/notification now claim and complete in short DB transactions, with filesystem/adapter work outside checked-out transactions\n- notification success is recorded through the root db before terminal approval markers\n- added pool-saturation regression and a real notifyHireApproved hook-path regression for post-success marker failure\n\nLocal checks:\n- pnpm --filter @paperclipai/server typecheck\n- pnpm exec vitest run server/src/tests/agents-service-secret-bindings.test.ts server/src/tests/approvals-built-in-hire-hook.test.ts server/src/tests/hire-hook.test.ts --no-file-parallelism --maxWorkers=1 (hire-hook passed; embedded Postgres suites skipped on this host)\n- git diff --check

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: f0da537

Prior Findings Dispositioned (2)

  • prior:bf856eb important 1 — fixed — server/src/services/approvals.ts:52 — filesystem reconciliation now runs after the short claim transaction has committed and without a checked-out transaction or pool connection; the saturation regression also verifies an unrelated query progresses while ten reconciliations are blocked.
  • prior:bf856eb important 2 — fixed — server/src/services/hire-hook.ts:92 — successful adapter delivery now records hire_hook.succeeded independently before approval terminal-marker repair; hasBuiltInHireNotificationDelivered uses that durable activity on retry, so a later marker failure does not redeliver.

Critical Issues (0)

Important Issues (1)

  • [gstack/review + native-codex / concurrency] server/src/services/approvals.ts:78 — The new live-attempt fence is only the process-local activeBuiltInHireAttemptLocks set. In a multi-replica deployment, another process does not see that lock; once either five-minute database claim expires, it can claim the same approval and execute builtInAgentService.ensure or notifyHireApproved concurrently with the original live owner. The lease-crossing tests instantiate one service in one process, so the local set masks this distributed race.
    • Use a distributed lock or renewable, ownership-fenced lease around the side-effect boundary. Add a two-service-instance regression sharing one database that advances beyond the lease while the first reconciliation and notification remain blocked, and prove only one side effect executes.

Suggestions (0)

Strengths

  • Long-running filesystem and adapter work no longer pins database pool connections, with focused saturation coverage.
  • Successful hook activity can repair missing approval terminal markers without redelivery.
  • External-lifecycle concurrency remains finitely capped while allowing Ally’s configured capacity of 15, and sparse updates preserve locked-row merging.

Recommended Action

  1. Replace the process-local live-attempt fence with a cross-replica ownership mechanism before merge.
  2. Add deterministic multi-instance lease-expiry coverage for both reconciliation and notification.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Ally, please re-review exact head b6e5ecde160d81ae127fd4920174277cf4abbede.\n\nChanges since f0da537:\n- removed the process-local built-in hire attempt fence\n- added short-transaction lease renewal while reconciliation or notification side effects are running, so another replica sees a fresh DB claim instead of taking over after the original five-minute lease\n- completion/release remain ownership-fenced by the latest claim attempt id\n- updated lease-expiry regressions to use two approvalService instances sharing one DB and fake the renewal interval past the prior lease window\n\nLocal checks:\n- pnpm --filter @paperclipai/server typecheck\n- pnpm exec vitest run server/src/tests/agents-service-secret-bindings.test.ts server/src/tests/approvals-built-in-hire-hook.test.ts server/src/tests/hire-hook.test.ts --no-file-parallelism --maxWorkers=1 (hire-hook passed; embedded Postgres suites skipped on this host)\n- git diff --check

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b6e5ecd

Prior Findings Dispositioned (1)

  • prior:f0da537 important 1 — still-present — server/src/services/approvals.ts:92 — claim renewal is now database-backed, but every renewal failure is swallowed while the filesystem or adapter side effect continues. If a replica loses database access or its event loop stalls beyond the five-minute lease, another replica can acquire the expired claim and execute the same side effect concurrently; the resumed stale owner is not fenced before continuing.

Critical Issues (0)

Important Issues (1)

  • [prior:f0da537 important 1 / gstack/review + native-codex] server/src/services/approvals.ts:92 — Lease renewal does not fence a stale side-effect owner. Failed renewals are ignored, and withBuiltInHireClaimLeaseRenewal has no ownership check or cancellation path before the running reconciliation/notification continues. The regressions at server/src/__tests__/agents-service-secret-bindings.test.ts:976 and server/src/__tests__/agents-service-secret-bindings.test.ts:1030 only advance healthy fake timers, so they prove renewal under normal scheduling rather than safety after renewal loss or process suspension.
    • Treat renewal loss as ownership loss and prevent the stale owner from continuing or committing the side effect, or move the side effect behind a durable idempotent/outbox boundary. Add a two-service regression that suppresses the first owner's renewals past lease expiry, lets the second owner take over, then resumes the first and proves only one side effect can execute or become externally visible.

Suggestions (0)

Strengths

  • The replacement removes the process-local-only lock and renews claims through shared database state without holding a pool connection during long-running side effects.
  • The focused tests now instantiate separate service objects and cover healthy renewal beyond the original lease window.
  • The original external-lifecycle boundary remains finite at 16 while permitting Ally's configured concurrency of 15.

Recommended Action

  1. Fence stale owners when renewal fails or is delayed beyond the lease.
  2. Add deterministic renewal-loss/takeover coverage for both reconciliation and notification.

No Critical issue remains at this exact SHA; one Important distributed-concurrency issue remains.

@allyblockcast allyblockcast 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.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 91a334a

Prior Findings Dispositioned (1)

  • prior:f0da537 important 1 — still-present — server/src/services/approvals.ts:82 — ownership is checked before and after filesystem reconciliation, but renewal can fail while ensure is already running. A replacement owner can then acquire the expired claim and execute the same side effect; the stale owner's post-call assertion only detects duplication after externally visible mutation. Notification has the same window around notifyHireApproved at line 718.

Critical Issues (0)

Important Issues (2)

  • [prior:f0da537 important 1 / pr-review-toolkit-errors + gstack/review + native-codex] server/src/services/approvals.ts:82 — Pre/post ownership assertions do not fence a side effect already in progress. Renewal loss during reconcileApprovedBuiltInAgent or notifyHireApproved permits a replacement owner to run concurrently, and the post-call assertion cannot undo filesystem mutation or adapter delivery. The new tests pause before the side effect, so they do not cover ownership loss inside the operation.
    • Enforce the attempt token at the side-effect boundary through durable idempotency or an atomic outbox/fencing contract, and add tests that block inside reconciliation and notification while another service takes over after lease expiry.
  • [pr-review-toolkit-code + gstack/review + native-codex] server/src/services/approvals.ts:67 — Completed reconciliation prevents notification recovery. claimBuiltInHireReconciliation returns null both when another owner holds the claim and when reconciliation is already complete; this unconditional return skips deliverBuiltInHireNotification at line 92. After a failed delivery or a crash between reconciliation completion and notification, every retry exits here, contradicting the added retry and earlier-process recovery tests.
    • Distinguish an active reconciliation claim from already-completed reconciliation. Skip only the reconciliation work when complete, then continue into notification claim/delivery recovery.

Suggestions (0)

Strengths

  • External-lifecycle capacity remains finite at 16 while allowing Ally's configured concurrency of 15.
  • Approval resolution, pending-agent activation, and budget updates are committed atomically.
  • Claim renewal and pre-side-effect ownership checks improve ordinary cross-replica contention handling, and notification payloads carry a stable idempotency key.

Recommended Action

  1. Make lease ownership enforceable during the filesystem and adapter side effects.
  2. Preserve notification recovery after reconciliation has already completed.
  3. Re-run the focused approval, hire-hook, and cross-service lease-loss tests after both fixes.

No Critical issue remains at this exact SHA; two Important issues remain.

@kkroo
kkroo force-pushed the staff/blo-20742-ally-concurrency-v2 branch from 91a334a to 285574f Compare August 3, 2026 17:57
@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head 285574f13272ee7a366f38c4450adee742fda56c. The separately tracked BLO-21320 built-in hire-completion lease/outbox experiment has been removed from this PR after repeated exact-head reviews found unenforceable lease fencing and delivery-idempotency gaps. This restores the scoped concurrency + atomic activation head, including the CI-required sparse route-test alignment (serialized shard passed locally: 30 files, 390 tests). The pre-existing post-commit completion debt remains unchanged from master and tracked in BLO-21320. Please report explicitly whether any Critical or Important issue introduced or worsened by this exact head remains.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review exact head a122a251ef2b8612a34e6d0324c15bd3d4bfa4c5. This is the scoped 285574f13 concurrency/atomic-activation head merged cleanly with current master; the two base commits touch only the Paperclip evidence skill and ReviewQueueCard UI, with no overlap in this PR's server/runtime paths. The BLO-21320 hire-completion lease experiment remains absent. Please report whether any Critical or Important issue introduced or worsened by this exact head remains.

@kkroo
kkroo force-pushed the staff/blo-20742-ally-concurrency-v2 branch from 58ad511 to a122a25 Compare August 3, 2026 18:09
@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

@ally please review restored exact head a122a251ef2b8612a34e6d0324c15bd3d4bfa4c5. A concurrent process had again appended the separately tracked BLO-21320 hire-completion experiment; it has been removed. This exact scoped head is current with master, server typecheck passes, and serialized shard 3/4 passes locally (30 files, 390 tests, 330.28s). Please report explicitly whether any Critical or Important issue introduced or worsened by this exact head remains.

@kkroo

kkroo commented Aug 3, 2026

Copy link
Copy Markdown
Author

Superseded by #992, which isolated the scoped BLO-20742 concurrency change from unrelated concurrent branch updates. #992 passed exact-head Ally review and all required CI, and merged as f0fb9bb.

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.

3 participants