@cat-factory/worker 0.99.1 → 0.99.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +333 -301
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/container-assembly.d.ts +61 -0
- package/dist/infrastructure/container-assembly.d.ts.map +1 -0
- package/dist/infrastructure/container-assembly.js +389 -0
- package/dist/infrastructure/container-assembly.js.map +1 -0
- package/dist/infrastructure/container-registries.d.ts +1 -2
- package/dist/infrastructure/container-registries.d.ts.map +1 -1
- package/dist/infrastructure/container.d.ts +263 -2
- package/dist/infrastructure/container.d.ts.map +1 -1
- package/dist/infrastructure/container.js +61 -374
- package/dist/infrastructure/container.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -168,6 +168,332 @@ const GITHUB_SYNC_QUEUE_NAME = 'cat-factory-github-sync';
|
|
|
168
168
|
* DELETEs ~720×/day against the single D1 writer. Routed by `controller.cron`.
|
|
169
169
|
*/
|
|
170
170
|
const RETENTION_CRON = '0 3 * * *';
|
|
171
|
+
/**
|
|
172
|
+
* Daily pass: prune the unbounded ledgers/projections to their retention
|
|
173
|
+
* windows. The tables exist regardless of whether GitHub/agents are
|
|
174
|
+
* configured, so this runs unconditionally; an unused table reclaims nothing.
|
|
175
|
+
*/
|
|
176
|
+
function runDailyRetentionSweeps(env, ctx, clock) {
|
|
177
|
+
// ADR 0026 D6.1: the Worker has no boot moment, so the O(1) ENCRYPTION_KEY drift check
|
|
178
|
+
// rides the daily cron. It seeds the fingerprint on first run and logs a definitive
|
|
179
|
+
// drift signal on a key change. Independent of (and cheaper than) the retention work.
|
|
180
|
+
if (env.ENCRYPTION_KEY) {
|
|
181
|
+
const encryptionKey = env.ENCRYPTION_KEY;
|
|
182
|
+
ctx.waitUntil(checkKeyFingerprint({
|
|
183
|
+
store: new D1KeyFingerprintStore({ db: env.DB }),
|
|
184
|
+
masterKeyBase64: encryptionKey,
|
|
185
|
+
logger: keyFingerprintLogger,
|
|
186
|
+
}).catch((error) => logger.error({ cron: 'key-fingerprint', err: errInfo(error) }, 'key fingerprint check failed')));
|
|
187
|
+
// ADR 0026 D6.2: the drift sweep — decrypt every sealed credential and raise/clear ONE
|
|
188
|
+
// `key_drift` card per affected workspace. Rides the same daily cron as the fingerprint.
|
|
189
|
+
ctx.waitUntil(sweepKeyDriftAndRaise(buildContainer(env), (info) => new WebCryptoSecretCipher({ masterKeyBase64: encryptionKey, info }), keyFingerprintLogger).catch((error) => logger.error({ cron: 'key-drift', err: errInfo(error) }, 'key drift sweep failed')));
|
|
190
|
+
}
|
|
191
|
+
// This branch never calls buildContainer (no request container is built for the
|
|
192
|
+
// sweep), so do the same fail-fast the build does: a clear error beats an opaque
|
|
193
|
+
// NPE deep in a telemetry repo when the binding is unbound.
|
|
194
|
+
const telemetryDb = requireTelemetryDb(env);
|
|
195
|
+
ctx.waitUntil(sweepRetention({
|
|
196
|
+
tokenUsageRepository: new D1TokenUsageRepository({ db: env.DB }),
|
|
197
|
+
rateLimitRepository: new D1RateLimitRepository({
|
|
198
|
+
db: env.DB,
|
|
199
|
+
idGenerator: new CryptoIdGenerator(),
|
|
200
|
+
}),
|
|
201
|
+
commitRepository: new D1CommitProjectionRepository({ db: env.DB }),
|
|
202
|
+
// Telemetry tables live in the dedicated TELEMETRY_DB database.
|
|
203
|
+
llmCallMetricRepository: new D1LlmCallMetricRepository({ db: telemetryDb }),
|
|
204
|
+
agentContextSnapshotRepository: new D1AgentContextSnapshotRepository({
|
|
205
|
+
db: telemetryDb,
|
|
206
|
+
}),
|
|
207
|
+
agentSearchQueryRepository: new D1AgentSearchQueryRepository({ db: telemetryDb }),
|
|
208
|
+
// Modeled subscription quota-cycle counters live in the main DB (migration 0047).
|
|
209
|
+
subscriptionQuotaCycleRepository: new D1SubscriptionQuotaCycleRepository({ db: env.DB }),
|
|
210
|
+
pipelineScheduleRepository: new D1PipelineScheduleRepository({ db: env.DB }),
|
|
211
|
+
passwordResetTokenRepository: new D1PasswordResetTokenRepository({ db: env.DB }),
|
|
212
|
+
notificationRepository: new D1NotificationRepository({ db: env.DB }),
|
|
213
|
+
// Prune the separate provisioning-log database when its binding is present.
|
|
214
|
+
...(env.PROVISIONING_DB
|
|
215
|
+
? {
|
|
216
|
+
provisioningLogRepository: new D1ProvisioningLogRepository({
|
|
217
|
+
db: env.PROVISIONING_DB,
|
|
218
|
+
}),
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
clock,
|
|
222
|
+
policy: loadConfig(env).retention,
|
|
223
|
+
})
|
|
224
|
+
.then((result) => logger.info({ cron: 'retention', ...result }, 'retention sweep complete'))
|
|
225
|
+
.catch((error) => logger.error({ cron: 'retention', err: errInfo(error) }, 'retention sweep failed')));
|
|
226
|
+
// Binary-artifact retention (UI screenshots + reference designs) is per-workspace, and
|
|
227
|
+
// the blob backend is per-account (R2 or S3), so it resolves each workspace's store. Run
|
|
228
|
+
// whenever storage could be configured: the R2 default (ARTIFACT_BUCKET) OR a per-account
|
|
229
|
+
// S3 backend (which needs the encryption key to unseal its credentials).
|
|
230
|
+
if (env.ARTIFACT_BUCKET || env.ENCRYPTION_KEY) {
|
|
231
|
+
const settingsRepo = new D1WorkspaceSettingsRepository({ db: env.DB });
|
|
232
|
+
ctx.waitUntil(sweepBinaryArtifactRetention({
|
|
233
|
+
resolveStore: buildCloudflareArtifactStoreResolver(env, env.DB, clock, new CryptoIdGenerator()),
|
|
234
|
+
listWorkspaceIds: () => new D1WorkspaceRepository({ db: env.DB })
|
|
235
|
+
.listVisible(null)
|
|
236
|
+
.then((ws) => ws.map((w) => w.id)),
|
|
237
|
+
retentionDaysFor: (workspaceId) => settingsRepo
|
|
238
|
+
.get(workspaceId)
|
|
239
|
+
.then((s) => s?.artifactRetentionDays ?? DEFAULT_WORKSPACE_SETTINGS.artifactRetentionDays),
|
|
240
|
+
now: clock.now(),
|
|
241
|
+
})
|
|
242
|
+
.then((removed) => logger.info({ cron: 'retention', binaryArtifacts: removed }, 'artifact retention sweep complete'))
|
|
243
|
+
.catch((error) => logger.error({ cron: 'retention', err: errInfo(error) }, 'artifact retention sweep failed')));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Re-drive any agent run — execution OR bootstrap — whose Workflows instance
|
|
248
|
+
* died. One sweep over the unified agent_runs table dispatches by kind.
|
|
249
|
+
*/
|
|
250
|
+
function redriveStuckAgentRuns(env, ctx, clock) {
|
|
251
|
+
if (env.EXECUTION_WORKFLOW || env.BOOTSTRAP_WORKFLOW || env.ENV_CONFIG_REPAIR_WORKFLOW) {
|
|
252
|
+
const execLookup = env.EXECUTION_WORKFLOW ? new WorkflowsLookup(env.EXECUTION_WORKFLOW) : null;
|
|
253
|
+
const bootLookup = env.BOOTSTRAP_WORKFLOW ? new WorkflowsLookup(env.BOOTSTRAP_WORKFLOW) : null;
|
|
254
|
+
const repairLookup = env.ENV_CONFIG_REPAIR_WORKFLOW
|
|
255
|
+
? new WorkflowsLookup(env.ENV_CONFIG_REPAIR_WORKFLOW)
|
|
256
|
+
: null;
|
|
257
|
+
const execRunner = env.EXECUTION_WORKFLOW
|
|
258
|
+
? new WorkflowsWorkRunner({ workflow: env.EXECUTION_WORKFLOW, queue: env.EXECUTION_QUEUE })
|
|
259
|
+
: null;
|
|
260
|
+
const bootRunner = env.BOOTSTRAP_WORKFLOW
|
|
261
|
+
? new WorkflowsBootstrapRunner(env.BOOTSTRAP_WORKFLOW)
|
|
262
|
+
: null;
|
|
263
|
+
const repairRunner = env.ENV_CONFIG_REPAIR_WORKFLOW
|
|
264
|
+
? new WorkflowsEnvConfigRepairRunner(env.ENV_CONFIG_REPAIR_WORKFLOW)
|
|
265
|
+
: null;
|
|
266
|
+
ctx.waitUntil(sweepStuckRuns({
|
|
267
|
+
agentRunRepository: new D1AgentRunRepository({ db: env.DB }),
|
|
268
|
+
instanceState: (ref) => {
|
|
269
|
+
const lookup = ref.kind === 'bootstrap'
|
|
270
|
+
? bootLookup
|
|
271
|
+
: ref.kind === 'env-config-repair'
|
|
272
|
+
? repairLookup
|
|
273
|
+
: execLookup;
|
|
274
|
+
// No binding for this kind → can't classify, so treat as alive (skip).
|
|
275
|
+
return lookup ? lookup.instanceState(ref.id) : Promise.resolve('alive');
|
|
276
|
+
},
|
|
277
|
+
redrive: async (ref) => {
|
|
278
|
+
if (ref.kind === 'bootstrap')
|
|
279
|
+
await bootRunner?.startRun(ref.workspaceId, ref.id);
|
|
280
|
+
else if (ref.kind === 'env-config-repair')
|
|
281
|
+
await repairRunner?.startRun(ref.workspaceId, ref.id);
|
|
282
|
+
else
|
|
283
|
+
await execRunner?.startRun(ref.workspaceId, ref.id);
|
|
284
|
+
},
|
|
285
|
+
// The durable instance is terminal and can't be recreated → finalize the
|
|
286
|
+
// run as stopped so it stops showing `running` forever (also reclaims any
|
|
287
|
+
// leftover container). Reuses the same stop path the user-facing button hits.
|
|
288
|
+
finalizeOrphan: async (ref) => {
|
|
289
|
+
const container = buildContainer(env);
|
|
290
|
+
const reason = 'The run was stopped automatically: its durable driver ended without finalizing it.';
|
|
291
|
+
if (ref.kind === 'bootstrap') {
|
|
292
|
+
if (container.bootstrap) {
|
|
293
|
+
await container.bootstrap.service.stop(ref.workspaceId, ref.id, {
|
|
294
|
+
reason,
|
|
295
|
+
kind: 'unknown',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (ref.kind === 'env-config-repair') {
|
|
300
|
+
if (container.envConfigRepair) {
|
|
301
|
+
await container.envConfigRepair.service.stop(ref.workspaceId, ref.id, {
|
|
302
|
+
reason,
|
|
303
|
+
kind: 'unknown',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
await container.executionService.stopRun(ref.workspaceId, ref.id, {
|
|
309
|
+
reason,
|
|
310
|
+
kind: 'unknown',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
// An execution whose instance stays missing past this deadline is failed
|
|
315
|
+
// `stalled` rather than re-created forever (symmetric with the Node sweeper).
|
|
316
|
+
failStalled: async (ref) => {
|
|
317
|
+
const container = buildContainer(env);
|
|
318
|
+
await container.executionService.failRun(ref.workspaceId, ref.id, 'Run stalled: its durable driver was lost and automatic recovery could not resume it.', 'stalled', null);
|
|
319
|
+
},
|
|
320
|
+
clock,
|
|
321
|
+
leaseMs: SWEEP_LEASE_MS,
|
|
322
|
+
hardStallMs: SWEEP_HARD_STALL_MS,
|
|
323
|
+
orphanedSince: runSweepOrphanedSince,
|
|
324
|
+
})
|
|
325
|
+
// Surface what the sweep did — the key signal for "are runs getting stuck?"
|
|
326
|
+
// Only log when it actually acted.
|
|
327
|
+
.then(({ redriven, finalized, stalled }) => {
|
|
328
|
+
if (redriven > 0 || finalized > 0 || stalled > 0) {
|
|
329
|
+
logger.warn({ cron: 'run-sweeper', redriven, finalized, stalled }, 'swept stuck runs');
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => logger.error({ cron: 'run-sweeper', err: errInfo(error) }, 'run sweep failed')));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Env-test self-tests live in their own table (not agent_runs), so the unified run
|
|
337
|
+
* sweep never sees them — this sibling sweep re-drives a run whose Workflows instance
|
|
338
|
+
* was lost and finalizes (cleanup + failed) one whose instance is terminal.
|
|
339
|
+
*/
|
|
340
|
+
function redriveStuckEnvTests(env, ctx, clock) {
|
|
341
|
+
if (env.ENV_TEST_WORKFLOW) {
|
|
342
|
+
const envTestLookup = new WorkflowsLookup(env.ENV_TEST_WORKFLOW);
|
|
343
|
+
const envTestRunner = new WorkflowsEnvironmentTestRunner(env.ENV_TEST_WORKFLOW);
|
|
344
|
+
ctx.waitUntil(sweepStuckEnvTests({
|
|
345
|
+
repository: new D1EnvironmentTestRunRepository({ db: env.DB }),
|
|
346
|
+
instanceState: (runId) => envTestLookup.instanceState(runId),
|
|
347
|
+
redrive: (workspaceId, runId) => envTestRunner.startRun(workspaceId, runId),
|
|
348
|
+
finalizeOrphan: async (workspaceId, runId) => {
|
|
349
|
+
const container = buildContainer(env);
|
|
350
|
+
await container.environments?.environmentTest?.expire(workspaceId, runId, 'The environment test was stopped automatically: its durable driver ended without finalizing it.');
|
|
351
|
+
},
|
|
352
|
+
clock,
|
|
353
|
+
leaseMs: SWEEP_LEASE_MS,
|
|
354
|
+
})
|
|
355
|
+
.then(({ redriven, finalized }) => {
|
|
356
|
+
if (redriven > 0 || finalized > 0) {
|
|
357
|
+
logger.warn({ cron: 'env-test-sweeper', redriven, finalized }, 'swept stuck env-test runs');
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.catch((error) => logger.error({ cron: 'env-test-sweeper', err: errInfo(error) }, 'env-test sweep failed')));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Reclaim expired personal-credential activations (individual-usage subscriptions).
|
|
365
|
+
* Each is a short-lived, system-encrypted per-run copy of a user's token; the TTL
|
|
366
|
+
* bounds standing exposure and a finished run's rows are deleted at completion, but
|
|
367
|
+
* this backstop also clears any that outlived their TTL. The table always exists.
|
|
368
|
+
*/
|
|
369
|
+
function reclaimExpiredActivations(env, ctx, clock) {
|
|
370
|
+
const activations = new D1SubscriptionActivationRepository({ db: env.DB });
|
|
371
|
+
ctx.waitUntil(activations
|
|
372
|
+
.deleteExpired(clock.now())
|
|
373
|
+
.then((reclaimed) => {
|
|
374
|
+
if (reclaimed > 0)
|
|
375
|
+
logger.info({ cron: 'activation-sweeper', reclaimed }, 'reclaimed activations');
|
|
376
|
+
})
|
|
377
|
+
.catch((error) => logger.error({ cron: 'activation-sweeper', err: errInfo(error) }, 'activation sweep failed')));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Instance-level container reaper: kill any per-run container that outlived its
|
|
381
|
+
* legitimate maximum lifetime. This is the load-bearing backstop the run-record
|
|
382
|
+
* nets miss — a terminal run whose container survived, or a stuck-`running` run
|
|
383
|
+
* a live driver keeps warm (so its idle sleep clock never starts). Keys off the
|
|
384
|
+
* real live-container inventory, not the run record, and kills via the same
|
|
385
|
+
* EXEC_CONTAINER binding (no Cloudflare API token). With normal runs now self-
|
|
386
|
+
* reclaiming, a reaped container is a genuine leak — the registry logs each loudly.
|
|
387
|
+
*/
|
|
388
|
+
function reapStaleContainers(env, ctx, clock) {
|
|
389
|
+
if (env.EXEC_CONTAINER) {
|
|
390
|
+
const reaper = new ContainerInstanceRegistry(env.EXEC_CONTAINER, new D1LiveContainerRepository({ db: env.DB }), clock);
|
|
391
|
+
const maxAgeMs = loadConfig(env).execution.containerMaxAgeMs;
|
|
392
|
+
ctx.waitUntil(reaper
|
|
393
|
+
.reapStaleBefore(clock.now() - maxAgeMs)
|
|
394
|
+
.then(({ reaped }) => {
|
|
395
|
+
if (reaped > 0)
|
|
396
|
+
logger.warn({ cron: 'container-reaper', reaped }, 'reaped leaked containers');
|
|
397
|
+
})
|
|
398
|
+
.catch((error) => logger.error({ cron: 'container-reaper', err: errInfo(error) }, 'container reap failed')));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The remaining every-2-min backstops: notification escalation, recurring pipelines,
|
|
403
|
+
* the initiative loop, Kaizen gradings, GitHub reconcile, environment teardown, and the
|
|
404
|
+
* platform observability/health sweeps. Each is an independent, no-op-when-unwired sweep.
|
|
405
|
+
*/
|
|
406
|
+
function runPeriodicBackstops(env, ctx, clock) {
|
|
407
|
+
// Escalate long-waiting notifications yellow → red (every 2 min). Runs no longer
|
|
408
|
+
// time out waiting for a human, so the escalating notification — past each
|
|
409
|
+
// workspace's `waitingEscalationMinutes` threshold — is the overdue-human signal.
|
|
410
|
+
ctx.waitUntil(escalateStaleNotifications(buildContainer(env), clock.now())
|
|
411
|
+
.then((escalated) => {
|
|
412
|
+
if (escalated > 0)
|
|
413
|
+
logger.info({ cron: 'notification-escalation', escalated }, 'escalated notifications');
|
|
414
|
+
})
|
|
415
|
+
.catch((error) => logger.error({ cron: 'notification-escalation', err: errInfo(error) }, 'notification escalation failed')));
|
|
416
|
+
// Fire any due recurring pipelines (every 2 min; the actual cadence is hours).
|
|
417
|
+
// Each due schedule starts its pipeline against its reused block, skipping any
|
|
418
|
+
// whose block already has an active run. No-op when the feature isn't wired.
|
|
419
|
+
ctx.waitUntil(Promise.resolve(buildContainer(env).recurring?.service.runDue(clock.now()))
|
|
420
|
+
.then((result) => {
|
|
421
|
+
if (result && (result.fired > 0 || result.skipped > 0)) {
|
|
422
|
+
logger.info({ cron: 'recurring-pipelines', ...result }, 'fired recurring pipelines');
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
.catch((error) => logger.error({ cron: 'recurring-pipelines', err: errInfo(error) }, 'recurring-pipeline sweep failed')));
|
|
426
|
+
// Tick the initiative execution loop (every 2 min): reconcile each executing initiative's
|
|
427
|
+
// spawned tasks and spawn the next wave up to its concurrency cap. Terminal child runs poke
|
|
428
|
+
// the loop directly, so this is the backstop cadence. No-op when initiatives aren't wired.
|
|
429
|
+
ctx.waitUntil(Promise.resolve(buildContainer(env).initiatives?.loop.runDue(clock.now()))
|
|
430
|
+
.then((result) => {
|
|
431
|
+
if (result && (result.spawned > 0 || result.completed > 0)) {
|
|
432
|
+
logger.info({ cron: 'initiative-loop', ...result }, 'ticked initiative loop');
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
.catch((error) => logger.error({ cron: 'initiative-loop', err: errInfo(error) }, 'initiative-loop sweep failed')));
|
|
436
|
+
// Run any pending Kaizen gradings (every 2 min): the engine only inserts `scheduled`
|
|
437
|
+
// rows at run completion, so this background pass does the actual LLM grading (and
|
|
438
|
+
// re-drives `running` rows orphaned by a crashed sweep). Bounded per pass to stay
|
|
439
|
+
// within the cron budget; no-op when the Kaizen feature isn't wired. The grader's
|
|
440
|
+
// model is resolved per-workspace (Model Configuration), so this is workspace-wide.
|
|
441
|
+
if (!kaizenSweeping) {
|
|
442
|
+
kaizenSweeping = true;
|
|
443
|
+
ctx.waitUntil(Promise.resolve(buildContainer(env).kaizen?.service.runPending(clock.now() - KAIZEN_STALE_MS, KAIZEN_SWEEP_BATCH))
|
|
444
|
+
.then((processed) => {
|
|
445
|
+
if (processed && processed > 0)
|
|
446
|
+
logger.info({ cron: 'kaizen-sweeper', processed }, 'ran pending kaizen gradings');
|
|
447
|
+
})
|
|
448
|
+
.catch((error) => logger.error({ cron: 'kaizen-sweeper', err: errInfo(error) }, 'kaizen sweep failed'))
|
|
449
|
+
.finally(() => {
|
|
450
|
+
kaizenSweeping = false;
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
// Reconcile GitHub projections that may have missed a webhook (no-op unless
|
|
454
|
+
// the integration is configured).
|
|
455
|
+
ctx.waitUntil(reconcileStaleRepos(env, clock, GITHUB_RECONCILE_STALE_MS)
|
|
456
|
+
.then((scheduled) => {
|
|
457
|
+
if (scheduled > 0)
|
|
458
|
+
// `sweep:` (not `cron:`) so the summary shares a field with the pass's
|
|
459
|
+
// per-repo lines, which the shared reconcile core emits on both facades.
|
|
460
|
+
logger.info({ sweep: 'github-reconcile', scheduled }, 'scheduled repo resyncs');
|
|
461
|
+
})
|
|
462
|
+
.catch((error) => logger.error({ sweep: 'github-reconcile', err: errInfo(error) }, 'github reconcile failed')));
|
|
463
|
+
// Tear down ephemeral environments whose TTL has elapsed (no-op unless the
|
|
464
|
+
// environment integration is configured).
|
|
465
|
+
ctx.waitUntil(sweepExpiredEnvironments(env, clock).catch((error) => logger.error({ cron: 'env-sweeper', err: errInfo(error) }, 'environment sweep failed')));
|
|
466
|
+
// Push deployment-level (platform-operator) observability aggregates to the OTLP
|
|
467
|
+
// endpoint as OpenTelemetry gauge metrics, once per cron tick. Opt-in on top of the base
|
|
468
|
+
// OTel exporter (OTEL_PLATFORM_METRICS); a no-op otherwise. Per account, enumerated from
|
|
469
|
+
// the workspace projection — the same `listVisible(null)` shape the artifact sweep uses.
|
|
470
|
+
// The container (hence the platform-observability read) is built only when opted in.
|
|
471
|
+
{
|
|
472
|
+
const otel = loadConfig(env).otel;
|
|
473
|
+
const sweep = runPlatformMetricsSweep({
|
|
474
|
+
otel,
|
|
475
|
+
platformObservability: otel.platformMetrics.enabled
|
|
476
|
+
? buildContainer(env).platformObservability
|
|
477
|
+
: undefined,
|
|
478
|
+
workspaceRepository: new D1WorkspaceRepository({ db: env.DB }),
|
|
479
|
+
logger,
|
|
480
|
+
});
|
|
481
|
+
if (sweep)
|
|
482
|
+
ctx.waitUntil(sweep);
|
|
483
|
+
}
|
|
484
|
+
// Raise/clear `platform_health` notifications when the deployment's OWN run health crosses
|
|
485
|
+
// an operator threshold, per account (the push counterpart to the operator dashboard read).
|
|
486
|
+
// Opt-in (`PLATFORM_ALERTS`); the container (hence the platform-observability read) is built
|
|
487
|
+
// only when opted in so a deployment that hasn't opted in pays nothing.
|
|
488
|
+
if (loadConfig(env).platformAlerts.enabled) {
|
|
489
|
+
ctx.waitUntil(sweepPlatformHealth(buildContainer(env), logger)
|
|
490
|
+
.then(({ raised, cleared }) => {
|
|
491
|
+
if (raised > 0 || cleared > 0)
|
|
492
|
+
logger.info({ cron: 'platform-health', raised, cleared }, 'platform health sweep');
|
|
493
|
+
})
|
|
494
|
+
.catch((error) => logger.error({ cron: 'platform-health', err: errInfo(error) }, 'platform health sweep failed')));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
171
497
|
export default {
|
|
172
498
|
// Validate the registered extensions (gates / agent kinds) ONCE, on the first request —
|
|
173
499
|
// by which point every `register*` import side effect has run. A typo'd gate helperKind or
|
|
@@ -185,311 +511,17 @@ export default {
|
|
|
185
511
|
},
|
|
186
512
|
async scheduled(controller, env, ctx) {
|
|
187
513
|
const clock = new SystemClock();
|
|
188
|
-
// Daily pass: prune the unbounded ledgers/projections to their retention
|
|
189
|
-
// windows. The tables exist regardless of whether GitHub/agents are
|
|
190
|
-
// configured, so this runs unconditionally; an unused table reclaims nothing.
|
|
514
|
+
// Daily pass: prune the unbounded ledgers/projections to their retention windows.
|
|
191
515
|
if (controller.cron === RETENTION_CRON) {
|
|
192
|
-
|
|
193
|
-
// rides the daily cron. It seeds the fingerprint on first run and logs a definitive
|
|
194
|
-
// drift signal on a key change. Independent of (and cheaper than) the retention work.
|
|
195
|
-
if (env.ENCRYPTION_KEY) {
|
|
196
|
-
const encryptionKey = env.ENCRYPTION_KEY;
|
|
197
|
-
ctx.waitUntil(checkKeyFingerprint({
|
|
198
|
-
store: new D1KeyFingerprintStore({ db: env.DB }),
|
|
199
|
-
masterKeyBase64: encryptionKey,
|
|
200
|
-
logger: keyFingerprintLogger,
|
|
201
|
-
}).catch((error) => logger.error({ cron: 'key-fingerprint', err: errInfo(error) }, 'key fingerprint check failed')));
|
|
202
|
-
// ADR 0026 D6.2: the drift sweep — decrypt every sealed credential and raise/clear ONE
|
|
203
|
-
// `key_drift` card per affected workspace. Rides the same daily cron as the fingerprint.
|
|
204
|
-
ctx.waitUntil(sweepKeyDriftAndRaise(buildContainer(env), (info) => new WebCryptoSecretCipher({ masterKeyBase64: encryptionKey, info }), keyFingerprintLogger).catch((error) => logger.error({ cron: 'key-drift', err: errInfo(error) }, 'key drift sweep failed')));
|
|
205
|
-
}
|
|
206
|
-
// This branch never calls buildContainer (no request container is built for the
|
|
207
|
-
// sweep), so do the same fail-fast the build does: a clear error beats an opaque
|
|
208
|
-
// NPE deep in a telemetry repo when the binding is unbound.
|
|
209
|
-
const telemetryDb = requireTelemetryDb(env);
|
|
210
|
-
ctx.waitUntil(sweepRetention({
|
|
211
|
-
tokenUsageRepository: new D1TokenUsageRepository({ db: env.DB }),
|
|
212
|
-
rateLimitRepository: new D1RateLimitRepository({
|
|
213
|
-
db: env.DB,
|
|
214
|
-
idGenerator: new CryptoIdGenerator(),
|
|
215
|
-
}),
|
|
216
|
-
commitRepository: new D1CommitProjectionRepository({ db: env.DB }),
|
|
217
|
-
// Telemetry tables live in the dedicated TELEMETRY_DB database.
|
|
218
|
-
llmCallMetricRepository: new D1LlmCallMetricRepository({ db: telemetryDb }),
|
|
219
|
-
agentContextSnapshotRepository: new D1AgentContextSnapshotRepository({
|
|
220
|
-
db: telemetryDb,
|
|
221
|
-
}),
|
|
222
|
-
agentSearchQueryRepository: new D1AgentSearchQueryRepository({ db: telemetryDb }),
|
|
223
|
-
// Modeled subscription quota-cycle counters live in the main DB (migration 0047).
|
|
224
|
-
subscriptionQuotaCycleRepository: new D1SubscriptionQuotaCycleRepository({ db: env.DB }),
|
|
225
|
-
pipelineScheduleRepository: new D1PipelineScheduleRepository({ db: env.DB }),
|
|
226
|
-
passwordResetTokenRepository: new D1PasswordResetTokenRepository({ db: env.DB }),
|
|
227
|
-
notificationRepository: new D1NotificationRepository({ db: env.DB }),
|
|
228
|
-
// Prune the separate provisioning-log database when its binding is present.
|
|
229
|
-
...(env.PROVISIONING_DB
|
|
230
|
-
? {
|
|
231
|
-
provisioningLogRepository: new D1ProvisioningLogRepository({
|
|
232
|
-
db: env.PROVISIONING_DB,
|
|
233
|
-
}),
|
|
234
|
-
}
|
|
235
|
-
: {}),
|
|
236
|
-
clock,
|
|
237
|
-
policy: loadConfig(env).retention,
|
|
238
|
-
})
|
|
239
|
-
.then((result) => logger.info({ cron: 'retention', ...result }, 'retention sweep complete'))
|
|
240
|
-
.catch((error) => logger.error({ cron: 'retention', err: errInfo(error) }, 'retention sweep failed')));
|
|
241
|
-
// Binary-artifact retention (UI screenshots + reference designs) is per-workspace, and
|
|
242
|
-
// the blob backend is per-account (R2 or S3), so it resolves each workspace's store. Run
|
|
243
|
-
// whenever storage could be configured: the R2 default (ARTIFACT_BUCKET) OR a per-account
|
|
244
|
-
// S3 backend (which needs the encryption key to unseal its credentials).
|
|
245
|
-
if (env.ARTIFACT_BUCKET || env.ENCRYPTION_KEY) {
|
|
246
|
-
const settingsRepo = new D1WorkspaceSettingsRepository({ db: env.DB });
|
|
247
|
-
ctx.waitUntil(sweepBinaryArtifactRetention({
|
|
248
|
-
resolveStore: buildCloudflareArtifactStoreResolver(env, env.DB, clock, new CryptoIdGenerator()),
|
|
249
|
-
listWorkspaceIds: () => new D1WorkspaceRepository({ db: env.DB })
|
|
250
|
-
.listVisible(null)
|
|
251
|
-
.then((ws) => ws.map((w) => w.id)),
|
|
252
|
-
retentionDaysFor: (workspaceId) => settingsRepo
|
|
253
|
-
.get(workspaceId)
|
|
254
|
-
.then((s) => s?.artifactRetentionDays ?? DEFAULT_WORKSPACE_SETTINGS.artifactRetentionDays),
|
|
255
|
-
now: clock.now(),
|
|
256
|
-
})
|
|
257
|
-
.then((removed) => logger.info({ cron: 'retention', binaryArtifacts: removed }, 'artifact retention sweep complete'))
|
|
258
|
-
.catch((error) => logger.error({ cron: 'retention', err: errInfo(error) }, 'artifact retention sweep failed')));
|
|
259
|
-
}
|
|
516
|
+
runDailyRetentionSweeps(env, ctx, clock);
|
|
260
517
|
return;
|
|
261
518
|
}
|
|
262
519
|
// Frequent pass (every 2 min): time-sensitive backstops.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const repairLookup = env.ENV_CONFIG_REPAIR_WORKFLOW
|
|
269
|
-
? new WorkflowsLookup(env.ENV_CONFIG_REPAIR_WORKFLOW)
|
|
270
|
-
: null;
|
|
271
|
-
const execRunner = env.EXECUTION_WORKFLOW
|
|
272
|
-
? new WorkflowsWorkRunner({ workflow: env.EXECUTION_WORKFLOW, queue: env.EXECUTION_QUEUE })
|
|
273
|
-
: null;
|
|
274
|
-
const bootRunner = env.BOOTSTRAP_WORKFLOW
|
|
275
|
-
? new WorkflowsBootstrapRunner(env.BOOTSTRAP_WORKFLOW)
|
|
276
|
-
: null;
|
|
277
|
-
const repairRunner = env.ENV_CONFIG_REPAIR_WORKFLOW
|
|
278
|
-
? new WorkflowsEnvConfigRepairRunner(env.ENV_CONFIG_REPAIR_WORKFLOW)
|
|
279
|
-
: null;
|
|
280
|
-
ctx.waitUntil(sweepStuckRuns({
|
|
281
|
-
agentRunRepository: new D1AgentRunRepository({ db: env.DB }),
|
|
282
|
-
instanceState: (ref) => {
|
|
283
|
-
const lookup = ref.kind === 'bootstrap'
|
|
284
|
-
? bootLookup
|
|
285
|
-
: ref.kind === 'env-config-repair'
|
|
286
|
-
? repairLookup
|
|
287
|
-
: execLookup;
|
|
288
|
-
// No binding for this kind → can't classify, so treat as alive (skip).
|
|
289
|
-
return lookup ? lookup.instanceState(ref.id) : Promise.resolve('alive');
|
|
290
|
-
},
|
|
291
|
-
redrive: async (ref) => {
|
|
292
|
-
if (ref.kind === 'bootstrap')
|
|
293
|
-
await bootRunner?.startRun(ref.workspaceId, ref.id);
|
|
294
|
-
else if (ref.kind === 'env-config-repair')
|
|
295
|
-
await repairRunner?.startRun(ref.workspaceId, ref.id);
|
|
296
|
-
else
|
|
297
|
-
await execRunner?.startRun(ref.workspaceId, ref.id);
|
|
298
|
-
},
|
|
299
|
-
// The durable instance is terminal and can't be recreated → finalize the
|
|
300
|
-
// run as stopped so it stops showing `running` forever (also reclaims any
|
|
301
|
-
// leftover container). Reuses the same stop path the user-facing button hits.
|
|
302
|
-
finalizeOrphan: async (ref) => {
|
|
303
|
-
const container = buildContainer(env);
|
|
304
|
-
const reason = 'The run was stopped automatically: its durable driver ended without finalizing it.';
|
|
305
|
-
if (ref.kind === 'bootstrap') {
|
|
306
|
-
if (container.bootstrap) {
|
|
307
|
-
await container.bootstrap.service.stop(ref.workspaceId, ref.id, {
|
|
308
|
-
reason,
|
|
309
|
-
kind: 'unknown',
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else if (ref.kind === 'env-config-repair') {
|
|
314
|
-
if (container.envConfigRepair) {
|
|
315
|
-
await container.envConfigRepair.service.stop(ref.workspaceId, ref.id, {
|
|
316
|
-
reason,
|
|
317
|
-
kind: 'unknown',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
await container.executionService.stopRun(ref.workspaceId, ref.id, {
|
|
323
|
-
reason,
|
|
324
|
-
kind: 'unknown',
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
// An execution whose instance stays missing past this deadline is failed
|
|
329
|
-
// `stalled` rather than re-created forever (symmetric with the Node sweeper).
|
|
330
|
-
failStalled: async (ref) => {
|
|
331
|
-
const container = buildContainer(env);
|
|
332
|
-
await container.executionService.failRun(ref.workspaceId, ref.id, 'Run stalled: its durable driver was lost and automatic recovery could not resume it.', 'stalled', null);
|
|
333
|
-
},
|
|
334
|
-
clock,
|
|
335
|
-
leaseMs: SWEEP_LEASE_MS,
|
|
336
|
-
hardStallMs: SWEEP_HARD_STALL_MS,
|
|
337
|
-
orphanedSince: runSweepOrphanedSince,
|
|
338
|
-
})
|
|
339
|
-
// Surface what the sweep did — the key signal for "are runs getting stuck?"
|
|
340
|
-
// Only log when it actually acted.
|
|
341
|
-
.then(({ redriven, finalized, stalled }) => {
|
|
342
|
-
if (redriven > 0 || finalized > 0 || stalled > 0) {
|
|
343
|
-
logger.warn({ cron: 'run-sweeper', redriven, finalized, stalled }, 'swept stuck runs');
|
|
344
|
-
}
|
|
345
|
-
})
|
|
346
|
-
.catch((error) => logger.error({ cron: 'run-sweeper', err: errInfo(error) }, 'run sweep failed')));
|
|
347
|
-
}
|
|
348
|
-
// Env-test self-tests live in their own table (not agent_runs), so the unified run
|
|
349
|
-
// sweep above never sees them — this sibling sweep re-drives a run whose Workflows
|
|
350
|
-
// instance was lost and finalizes (cleanup + failed) one whose instance is terminal.
|
|
351
|
-
if (env.ENV_TEST_WORKFLOW) {
|
|
352
|
-
const envTestLookup = new WorkflowsLookup(env.ENV_TEST_WORKFLOW);
|
|
353
|
-
const envTestRunner = new WorkflowsEnvironmentTestRunner(env.ENV_TEST_WORKFLOW);
|
|
354
|
-
ctx.waitUntil(sweepStuckEnvTests({
|
|
355
|
-
repository: new D1EnvironmentTestRunRepository({ db: env.DB }),
|
|
356
|
-
instanceState: (runId) => envTestLookup.instanceState(runId),
|
|
357
|
-
redrive: (workspaceId, runId) => envTestRunner.startRun(workspaceId, runId),
|
|
358
|
-
finalizeOrphan: async (workspaceId, runId) => {
|
|
359
|
-
const container = buildContainer(env);
|
|
360
|
-
await container.environments?.environmentTest?.expire(workspaceId, runId, 'The environment test was stopped automatically: its durable driver ended without finalizing it.');
|
|
361
|
-
},
|
|
362
|
-
clock,
|
|
363
|
-
leaseMs: SWEEP_LEASE_MS,
|
|
364
|
-
})
|
|
365
|
-
.then(({ redriven, finalized }) => {
|
|
366
|
-
if (redriven > 0 || finalized > 0) {
|
|
367
|
-
logger.warn({ cron: 'env-test-sweeper', redriven, finalized }, 'swept stuck env-test runs');
|
|
368
|
-
}
|
|
369
|
-
})
|
|
370
|
-
.catch((error) => logger.error({ cron: 'env-test-sweeper', err: errInfo(error) }, 'env-test sweep failed')));
|
|
371
|
-
}
|
|
372
|
-
// Reclaim expired personal-credential activations (individual-usage subscriptions).
|
|
373
|
-
// Each is a short-lived, system-encrypted per-run copy of a user's token; the TTL
|
|
374
|
-
// bounds standing exposure and a finished run's rows are deleted at completion, but
|
|
375
|
-
// this backstop also clears any that outlived their TTL. The table always exists.
|
|
376
|
-
{
|
|
377
|
-
const activations = new D1SubscriptionActivationRepository({ db: env.DB });
|
|
378
|
-
ctx.waitUntil(activations
|
|
379
|
-
.deleteExpired(clock.now())
|
|
380
|
-
.then((reclaimed) => {
|
|
381
|
-
if (reclaimed > 0)
|
|
382
|
-
logger.info({ cron: 'activation-sweeper', reclaimed }, 'reclaimed activations');
|
|
383
|
-
})
|
|
384
|
-
.catch((error) => logger.error({ cron: 'activation-sweeper', err: errInfo(error) }, 'activation sweep failed')));
|
|
385
|
-
}
|
|
386
|
-
// Instance-level container reaper: kill any per-run container that outlived its
|
|
387
|
-
// legitimate maximum lifetime. This is the load-bearing backstop the run-record
|
|
388
|
-
// nets miss — a terminal run whose container survived, or a stuck-`running` run
|
|
389
|
-
// a live driver keeps warm (so its idle sleep clock never starts). Keys off the
|
|
390
|
-
// real live-container inventory, not the run record, and kills via the same
|
|
391
|
-
// EXEC_CONTAINER binding (no Cloudflare API token). With normal runs now self-
|
|
392
|
-
// reclaiming, a reaped container is a genuine leak — the registry logs each loudly.
|
|
393
|
-
if (env.EXEC_CONTAINER) {
|
|
394
|
-
const reaper = new ContainerInstanceRegistry(env.EXEC_CONTAINER, new D1LiveContainerRepository({ db: env.DB }), clock);
|
|
395
|
-
const maxAgeMs = loadConfig(env).execution.containerMaxAgeMs;
|
|
396
|
-
ctx.waitUntil(reaper
|
|
397
|
-
.reapStaleBefore(clock.now() - maxAgeMs)
|
|
398
|
-
.then(({ reaped }) => {
|
|
399
|
-
if (reaped > 0)
|
|
400
|
-
logger.warn({ cron: 'container-reaper', reaped }, 'reaped leaked containers');
|
|
401
|
-
})
|
|
402
|
-
.catch((error) => logger.error({ cron: 'container-reaper', err: errInfo(error) }, 'container reap failed')));
|
|
403
|
-
}
|
|
404
|
-
// Escalate long-waiting notifications yellow → red (every 2 min). Runs no longer
|
|
405
|
-
// time out waiting for a human, so the escalating notification — past each
|
|
406
|
-
// workspace's `waitingEscalationMinutes` threshold — is the overdue-human signal.
|
|
407
|
-
ctx.waitUntil(escalateStaleNotifications(buildContainer(env), clock.now())
|
|
408
|
-
.then((escalated) => {
|
|
409
|
-
if (escalated > 0)
|
|
410
|
-
logger.info({ cron: 'notification-escalation', escalated }, 'escalated notifications');
|
|
411
|
-
})
|
|
412
|
-
.catch((error) => logger.error({ cron: 'notification-escalation', err: errInfo(error) }, 'notification escalation failed')));
|
|
413
|
-
// Fire any due recurring pipelines (every 2 min; the actual cadence is hours).
|
|
414
|
-
// Each due schedule starts its pipeline against its reused block, skipping any
|
|
415
|
-
// whose block already has an active run. No-op when the feature isn't wired.
|
|
416
|
-
ctx.waitUntil(Promise.resolve(buildContainer(env).recurring?.service.runDue(clock.now()))
|
|
417
|
-
.then((result) => {
|
|
418
|
-
if (result && (result.fired > 0 || result.skipped > 0)) {
|
|
419
|
-
logger.info({ cron: 'recurring-pipelines', ...result }, 'fired recurring pipelines');
|
|
420
|
-
}
|
|
421
|
-
})
|
|
422
|
-
.catch((error) => logger.error({ cron: 'recurring-pipelines', err: errInfo(error) }, 'recurring-pipeline sweep failed')));
|
|
423
|
-
// Tick the initiative execution loop (every 2 min): reconcile each executing initiative's
|
|
424
|
-
// spawned tasks and spawn the next wave up to its concurrency cap. Terminal child runs poke
|
|
425
|
-
// the loop directly, so this is the backstop cadence. No-op when initiatives aren't wired.
|
|
426
|
-
ctx.waitUntil(Promise.resolve(buildContainer(env).initiatives?.loop.runDue(clock.now()))
|
|
427
|
-
.then((result) => {
|
|
428
|
-
if (result && (result.spawned > 0 || result.completed > 0)) {
|
|
429
|
-
logger.info({ cron: 'initiative-loop', ...result }, 'ticked initiative loop');
|
|
430
|
-
}
|
|
431
|
-
})
|
|
432
|
-
.catch((error) => logger.error({ cron: 'initiative-loop', err: errInfo(error) }, 'initiative-loop sweep failed')));
|
|
433
|
-
// Run any pending Kaizen gradings (every 2 min): the engine only inserts `scheduled`
|
|
434
|
-
// rows at run completion, so this background pass does the actual LLM grading (and
|
|
435
|
-
// re-drives `running` rows orphaned by a crashed sweep). Bounded per pass to stay
|
|
436
|
-
// within the cron budget; no-op when the Kaizen feature isn't wired. The grader's
|
|
437
|
-
// model is resolved per-workspace (Model Configuration), so this is workspace-wide.
|
|
438
|
-
if (!kaizenSweeping) {
|
|
439
|
-
kaizenSweeping = true;
|
|
440
|
-
ctx.waitUntil(Promise.resolve(buildContainer(env).kaizen?.service.runPending(clock.now() - KAIZEN_STALE_MS, KAIZEN_SWEEP_BATCH))
|
|
441
|
-
.then((processed) => {
|
|
442
|
-
if (processed && processed > 0)
|
|
443
|
-
logger.info({ cron: 'kaizen-sweeper', processed }, 'ran pending kaizen gradings');
|
|
444
|
-
})
|
|
445
|
-
.catch((error) => logger.error({ cron: 'kaizen-sweeper', err: errInfo(error) }, 'kaizen sweep failed'))
|
|
446
|
-
.finally(() => {
|
|
447
|
-
kaizenSweeping = false;
|
|
448
|
-
}));
|
|
449
|
-
}
|
|
450
|
-
// Reconcile GitHub projections that may have missed a webhook (no-op unless
|
|
451
|
-
// the integration is configured).
|
|
452
|
-
ctx.waitUntil(reconcileStaleRepos(env, clock, GITHUB_RECONCILE_STALE_MS)
|
|
453
|
-
.then((scheduled) => {
|
|
454
|
-
if (scheduled > 0)
|
|
455
|
-
// `sweep:` (not `cron:`) so the summary shares a field with the pass's
|
|
456
|
-
// per-repo lines, which the shared reconcile core emits on both facades.
|
|
457
|
-
logger.info({ sweep: 'github-reconcile', scheduled }, 'scheduled repo resyncs');
|
|
458
|
-
})
|
|
459
|
-
.catch((error) => logger.error({ sweep: 'github-reconcile', err: errInfo(error) }, 'github reconcile failed')));
|
|
460
|
-
// Tear down ephemeral environments whose TTL has elapsed (no-op unless the
|
|
461
|
-
// environment integration is configured).
|
|
462
|
-
ctx.waitUntil(sweepExpiredEnvironments(env, clock).catch((error) => logger.error({ cron: 'env-sweeper', err: errInfo(error) }, 'environment sweep failed')));
|
|
463
|
-
// Push deployment-level (platform-operator) observability aggregates to the OTLP
|
|
464
|
-
// endpoint as OpenTelemetry gauge metrics, once per cron tick. Opt-in on top of the base
|
|
465
|
-
// OTel exporter (OTEL_PLATFORM_METRICS); a no-op otherwise. Per account, enumerated from
|
|
466
|
-
// the workspace projection — the same `listVisible(null)` shape the artifact sweep uses.
|
|
467
|
-
// The container (hence the platform-observability read) is built only when opted in.
|
|
468
|
-
{
|
|
469
|
-
const otel = loadConfig(env).otel;
|
|
470
|
-
const sweep = runPlatformMetricsSweep({
|
|
471
|
-
otel,
|
|
472
|
-
platformObservability: otel.platformMetrics.enabled
|
|
473
|
-
? buildContainer(env).platformObservability
|
|
474
|
-
: undefined,
|
|
475
|
-
workspaceRepository: new D1WorkspaceRepository({ db: env.DB }),
|
|
476
|
-
logger,
|
|
477
|
-
});
|
|
478
|
-
if (sweep)
|
|
479
|
-
ctx.waitUntil(sweep);
|
|
480
|
-
}
|
|
481
|
-
// Raise/clear `platform_health` notifications when the deployment's OWN run health crosses
|
|
482
|
-
// an operator threshold, per account (the push counterpart to the operator dashboard read).
|
|
483
|
-
// Opt-in (`PLATFORM_ALERTS`); the container (hence the platform-observability read) is built
|
|
484
|
-
// only when opted in so a deployment that hasn't opted in pays nothing.
|
|
485
|
-
if (loadConfig(env).platformAlerts.enabled) {
|
|
486
|
-
ctx.waitUntil(sweepPlatformHealth(buildContainer(env), logger)
|
|
487
|
-
.then(({ raised, cleared }) => {
|
|
488
|
-
if (raised > 0 || cleared > 0)
|
|
489
|
-
logger.info({ cron: 'platform-health', raised, cleared }, 'platform health sweep');
|
|
490
|
-
})
|
|
491
|
-
.catch((error) => logger.error({ cron: 'platform-health', err: errInfo(error) }, 'platform health sweep failed')));
|
|
492
|
-
}
|
|
520
|
+
redriveStuckAgentRuns(env, ctx, clock);
|
|
521
|
+
redriveStuckEnvTests(env, ctx, clock);
|
|
522
|
+
reclaimExpiredActivations(env, ctx, clock);
|
|
523
|
+
reapStaleContainers(env, ctx, clock);
|
|
524
|
+
runPeriodicBackstops(env, ctx, clock);
|
|
493
525
|
},
|
|
494
526
|
async queue(batch, env) {
|
|
495
527
|
// Route by source queue — the single handler serves both queues.
|