Skip to content

Commit 781c9eb

Browse files
andresberriosclaude
andcommitted
fix(agents-server): close the same wake-registration gap at cron & webhook-source sites
upsertCronSchedule and upsertWebhookSourceSubscription used the same unregister-then-register sequence that dropped wakes on parallel sub-agent spawn: the manifest-anchored wake registration is briefly absent from the in-memory cache across the register()'s DB round-trip, so a cron tick or webhook event arriving in that window would be missed. These are single API calls rather than concurrent bursts, so the race is far narrower than the spawn path, but the gap is the same class of bug — and both also race against the manifest-sync reconcile triggered by their own writeManifestEntry. Route both through WakeRegistry.reconcileManifestRegistration, which registers the desired reg first (never leaving the cache empty) and then prunes only stale rows for the manifest key. Delete-only sites (deleteSchedule, deleteWebhookSourceSubscription, deletePgSyncObservation, upsertFutureSendSchedule) have no re-register and so no gap; left as-is. Cron/webhook/manifest suites pass (30/30). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent deb8c53 commit 781c9eb

1 file changed

Lines changed: 32 additions & 27 deletions

File tree

packages/agents-server/src/entity-manager.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2977,23 +2977,26 @@ export class EntityManager {
29772977
const spec = resolveCronScheduleSpec(req.expression, req.timezone)
29782978

29792979
const manifestKey = `schedule:${req.id}`
2980-
await this.wakeRegistry.unregisterByManifestKey(
2980+
// Gap-free reconcile (register-first, then prune stale). The old
2981+
// unregister-then-register briefly left this manifest key's wake absent from
2982+
// the cache; a cron tick landing in that window would be missed. See
2983+
// WakeRegistry.reconcileManifestRegistration.
2984+
await this.wakeRegistry.reconcileManifestRegistration(
29812985
entityUrl,
29822986
manifestKey,
2987+
{
2988+
subscriberUrl: entityUrl,
2989+
sourceUrl: getCronStreamPath(spec.expression, spec.timezone),
2990+
condition: {
2991+
on: `change`,
2992+
},
2993+
debounceMs: req.debounceMs,
2994+
timeoutMs: req.timeoutMs,
2995+
oneShot: false,
2996+
manifestKey,
2997+
},
29832998
this.tenantId
29842999
)
2985-
await this.wakeRegistry.register({
2986-
tenantId: this.tenantId,
2987-
subscriberUrl: entityUrl,
2988-
sourceUrl: getCronStreamPath(spec.expression, spec.timezone),
2989-
condition: {
2990-
on: `change`,
2991-
},
2992-
debounceMs: req.debounceMs,
2993-
timeoutMs: req.timeoutMs,
2994-
oneShot: false,
2995-
manifestKey,
2996-
})
29973000
await this.getOrCreateCronStream(spec.expression, spec.timezone)
29983001

29993002
const txid = randomUUID()
@@ -3162,24 +3165,26 @@ export class EntityManager {
31623165
)
31633166

31643167
// The manifest is the durable source of truth. Register side effects after
3165-
// it is appended so failures can be repaired by manifest replay.
3166-
await this.wakeRegistry.unregisterByManifestKey(
3168+
// it is appended so failures can be repaired by manifest replay. Gap-free
3169+
// reconcile (register-first, then prune stale) so a webhook event landing
3170+
// during a re-subscribe isn't missed — see
3171+
// WakeRegistry.reconcileManifestRegistration.
3172+
await this.wakeRegistry.reconcileManifestRegistration(
31673173
entityUrl,
31683174
manifestKey,
3175+
{
3176+
subscriberUrl: entityUrl,
3177+
sourceUrl: req.subscription.sourceUrl,
3178+
condition: {
3179+
on: `change`,
3180+
collections: [`webhook_event`],
3181+
ops: [`insert`],
3182+
},
3183+
oneShot: false,
3184+
manifestKey,
3185+
},
31693186
this.tenantId
31703187
)
3171-
await this.wakeRegistry.register({
3172-
tenantId: this.tenantId,
3173-
subscriberUrl: entityUrl,
3174-
sourceUrl: req.subscription.sourceUrl,
3175-
condition: {
3176-
on: `change`,
3177-
collections: [`webhook_event`],
3178-
ops: [`insert`],
3179-
},
3180-
oneShot: false,
3181-
manifestKey,
3182-
})
31833188

31843189
return { txid, subscription: req.subscription }
31853190
}

0 commit comments

Comments
 (0)