Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c435a16
fix: prioritize above-fold Roomote logo loading for LCP (#218)
roomote-roomote[bot] Jul 12, 2026
0a5daf5
fix(preview-proxy): secure local preview auth cookies (#219)
mrubens Jul 12, 2026
87fe850
Retain Docker and Blaxel standby tasks with bounded cleanup (#220)
mrubens Jul 12, 2026
7357213
fix: normalize typed compute runtime values (#221)
mrubens Jul 12, 2026
4f78fc2
improve: add status footer to PR review summary comments (#222)
roomote-roomote[bot] Jul 12, 2026
212ee85
Fix stale local Docker workers and tunneled live sessions (#223)
mrubens Jul 12, 2026
f379e0e
[Improve] Simplify PR review status footer to phase and SHA (#226)
roomote-roomote[bot] Jul 12, 2026
3164344
fix: expire signed artifact raw URLs after 30 days (#225)
roomote-roomote[bot] Jul 12, 2026
754ad80
[Fix] Resume retained environments before the first agent session (#227)
mrubens Jul 12, 2026
31bf049
[Improve] Drop hash prefix from PR review status footer SHA (#228)
roomote-roomote[bot] Jul 12, 2026
2522d7d
[Feat] Add Sleep option to task overflow menu (#229)
roomote-roomote[bot] Jul 12, 2026
71791b7
[Fix] Worker common env file is world-readable to other sandbox users…
roomote-roomote[bot] Jul 12, 2026
b17bf44
[Improve] Highlight failing CI and merge conflicts in PR review updat…
roomote-roomote[bot] Jul 12, 2026
7a2d2c3
Fix manual sleep for Docker tasks (#231)
mrubens Jul 12, 2026
524d9f9
chore: replace remaining customer identifiers in test fixtures with f…
mrubens Jul 12, 2026
fa245c5
Harden Blaxel sandbox lifecycle (#233)
mrubens Jul 12, 2026
0efdd5e
[Chore] Prep minor release notes for 0.3.0 (#234)
roomote-roomote[bot] Jul 12, 2026
40940bc
Release Roomote 0.3.0 (#235)
mrubens Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .docker/caddy/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
}
}

# Route the sandbox-server transport through the same public origin as the
# web app. A browser opened through ngrok cannot connect to the worker's
# loopback-only published port directly because 127.0.0.1 would refer to the
# browser device. The preview proxy resolves the task's current worker port
# after Caddy translates this path route into its normal virtual host.
@local_sandbox path_regexp local_sandbox ^/_roomote-sandbox/([a-z0-9]+)(/.*)$

handle @local_sandbox {
rewrite * {re.local_sandbox.2}
reverse_proxy host.docker.internal:18081 {
header_up Host {re.local_sandbox.1}-sandbox-server.roomotepreview.localhost:18081
lb_try_duration 10s
lb_try_interval 250ms
}
}

handle {
reverse_proxy host.docker.internal:13000 {
lb_try_duration 10s
Expand Down
2 changes: 2 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ DEFAULT_COMPUTE_PROVIDER=docker
# DOCKER_WORKER_LOG_MAX_SIZE=10m
# DOCKER_WORKER_LOG_MAX_FILES=3
# DOCKER_WORKER_EGRESS_POLICY=internet
# DOCKER_STANDBY_MAX_COUNT=10
# DOCKER_STANDBY_MAX_AGE_HOURS=24
# MODAL_TOKEN_ID=
# MODAL_TOKEN_SECRET=
# Leave blank: the installer and deployer keep it in sync with the matching
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

This file tracks product releases for Roomote (single monorepo version). Automated release entries are prepended by `pnpm run version`.

## 0.3.0 (2026-07-12)

### Minor changes

- Local Docker tasks now retain idle containers and resume them in place with bounded cleanup (default 10 retained containers, 24-hour max age; max count `0` disables retention). Blaxel standby retention is likewise bounded (defaults 25 / 168 hours), with env knobs `DOCKER_STANDBY_MAX_*` and `BLAXEL_STANDBY_MAX_*` documented for operators.
- Tasks can go to sleep early from the task page overflow menu (Sleep above Delete). The action is available for snapshot-capable runs and for resumable Docker/Blaxel standby, so operators can release an awake environment without waiting for the keepalive timer.

### Patch changes

- Signed public artifact raw URLs (allowlisted images and videos used for visual proofs and PR embeds) expire 30 days after they are signed, and cache headers stay within the remaining TTL, so a leaked screenshot link cannot be fetched indefinitely.
- Blaxel sandbox lifecycle is more resilient: deterministic external IDs with idempotent create, bounded retries on readiness-sensitive calls, reuse of preview resources across standby/resume instead of delete-and-recreate, and immediate failure on non-retryable 4xx errors.
- Local Docker development rebuilds worker images that lack current networking tools before launching tasks, routes sandbox HTTP/WebSocket traffic through the public app edge so tunneled clients get a usable live session, and marks preview auth cookies Secure when using SameSite=None and Partitioned so iframe previews authenticate reliably.
- PR review notification updates treat failing CI checks and live merge conflicts as high-signal blockers: triage copy names the problem and offers a fix or conflict resolution instead of burying it after a soft "looked good" wrap-up.
- Main GitHub PR review summary comments now show a compact status footer with the review phase and short commit SHA (`Reviewing abc1234` / `Reviewed abc1234`), using a linked SHA when a commit URL can be built.
- Docker and Blaxel standby environments can resume even when they were suspended before the first agent harness session, so early-sleep retains come back to Idle without forcing a new session create path.
- The worker common env file (`~/.roomote/env.sh`, which holds deployment secrets such as cloud tokens) is written owner-only (`0o600`) with `~/.roomote` locked to `0o700`, so other sandbox users cannot read those secrets.

## 0.2.0 (2026-07-12)

### Minor changes
Expand Down
10 changes: 10 additions & 0 deletions apps/bullmq/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { teamsSuggestedTasksOnboardingFollowupJob } from './jobs/teams-suggested
import { startSnapshotQueue } from './snapshot-queue';
import { startSlackPrInactivityQueue } from './slack-pr-inactivity-queue';
import { startPrReviewNotificationQueue } from './pr-review-notification-queue';
import { startTaskSleepQueue } from './task-sleep-queue';

// Resolve auto-generated auth keypairs before any queue worker starts so
// scheduled jobs that sign tokens observe the resolved keys.
Expand Down Expand Up @@ -75,6 +76,11 @@ const {

const { snapshotQueue, snapshotWorker, snapshotQueueEvents } =
startSnapshotQueue();
const {
queue: taskSleepQueue,
worker: taskSleepWorker,
queueEvents: taskSleepQueueEvents,
} = startTaskSleepQueue();
const {
slackAccountLinkEducationQueue,
slackAccountLinkEducationWorker,
Expand Down Expand Up @@ -124,6 +130,7 @@ createBullBoard({
new BullMQAdapter(schedulerQueue, { readOnlyMode: false }),
new BullMQAdapter(sandboxOidcRefreshQueue, { readOnlyMode: false }),
new BullMQAdapter(snapshotQueue, { readOnlyMode: false }),
new BullMQAdapter(taskSleepQueue, { readOnlyMode: false }),
new BullMQAdapter(slackAccountLinkEducationQueue, { readOnlyMode: false }),
new BullMQAdapter(slackSuggestedTasksOnboardingFollowupQueue, {
readOnlyMode: false,
Expand Down Expand Up @@ -268,6 +275,9 @@ async function gracefulShutdown() {
await snapshotWorker.close();
await snapshotQueueEvents.close();
await snapshotQueue.close();
await taskSleepWorker.close();
await taskSleepQueueEvents.close();
await taskSleepQueue.close();
await slackAccountLinkEducationWorker.close();
await slackAccountLinkEducationQueueEvents.close();
await slackAccountLinkEducationQueue.close();
Expand Down
123 changes: 122 additions & 1 deletion apps/bullmq/src/scheduled-jobs/__tests__/sleep-check.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/bullmq/src/scheduled-jobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { refreshSnapshotsJob } from './refresh-snapshots';
export { pullRequestAnalyticsSyncJob } from './pull-request-analytics-sync';
export { instancePingJob } from './instance-ping';
export { webhookCleanupJob } from './webhook-cleanup';
export { standbyRetentionJob } from './standby-retention';
83 changes: 82 additions & 1 deletion apps/bullmq/src/scheduled-jobs/sleep-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const SLEEP_CHECK_PROVIDERS = sleepCheckManagedComputeProviders;

type SleepCheckPath =
| 'due_sleep'
| 'manual_sleep'
| 'stale_worker'
| 'hard_limit'
| 'booting_no_heartbeat';
Expand Down Expand Up @@ -157,6 +158,8 @@ async function createSleepCheckClient(provider: ComputeProvider) {
provider: 'blaxel',
envFallback: await resolveComputeProviderEnvValues('blaxel'),
});
case 'docker':
return createComputeProviderClient({ provider: 'docker' });
default:
throw new Error(
`Sleep check has no compute client for provider "${provider}"`,
Expand Down Expand Up @@ -688,7 +691,7 @@ async function claimAndEnterStandby(
await recordSleepCheckEvent(
job,
'completed',
`Retained Blaxel sandbox ${job.machineId} on standby for task run #${job.id}.`,
`Retained ${job.vendor} sandbox ${job.machineId} on standby for task run #${job.id}.`,
{
path,
decision: 'standby_completed',
Expand Down Expand Up @@ -733,6 +736,82 @@ async function claimResumableSleep(
return (await claimAndSnapshot(job, path)) === 'enqueued';
}

/**
* Process a user-requested sleep immediately. Unlike the scheduled due-sleep
* path, this intentionally does not extend the deadline for an active phase.
*/
export async function sleepTaskRunNow(runId: number): Promise<void> {
const job = await db.query.taskRuns.findFirst({
where: eq(taskRuns.id, runId),
columns: {
id: true,
payloadKind: true,
status: true,
taskPhase: true,
machineId: true,
vendor: true,
taskId: true,
snapshotRequestedAt: true,
sandboxCmdId: true,
sleepAt: true,
sleepRequestedAt: true,
startedAt: true,
workerHeartbeatAt: true,
snapshotId: true,
},
});

if (!job) {
throw new Error(`Task run #${runId} was not found`);
}

if (!ACTIVE_SLEEP_CHECK_STATUSES.includes(job.status)) {
throw new Error(`Task run #${runId} is not active`);
}

if (!job.machineId || !job.vendor) {
throw new Error(`Task run #${runId} has no active machine`);
}

if (
!isResumableTaskPayloadKind(job.payloadKind) ||
!isTaskResumeCapableComputeProvider(job.vendor)
) {
throw new Error(`Task run #${runId} does not support resumable sleep`);
}

if (job.snapshotId || job.snapshotRequestedAt || job.sleepRequestedAt) {
return;
}

const client = await createSleepCheckClient(job.vendor);
const { status } = await client.getInstanceStatus({
instanceId: job.machineId,
});

if (status !== 'running') {
throw new Error(
`Cannot put task run #${runId} to sleep because its instance is ${status}`,
);
}

if (isStandbyResumeCapableComputeProvider(job.vendor)) {
const result = await claimAndEnterStandby(job, client, 'manual_sleep');

if (result === 'error') {
throw new Error(`Failed to put task run #${runId} on standby`);
}

return;
}

const result = await claimAndSnapshot(job, 'manual_sleep');

if (result === 'error') {
throw new Error(`Failed to snapshot task run #${runId}`);
}
}

/**
* Merge candidate jobs by machine ID while keeping the newest row per category.
*/
Expand Down Expand Up @@ -1336,6 +1415,8 @@ function describeSleepCheckPath(path: SleepCheckPath): string {
switch (path) {
case 'due_sleep':
return 'Due sleep handling';
case 'manual_sleep':
return 'Manual sleep handling';
case 'hard_limit':
return 'Provider-timeout backstop';
case 'stale_worker':
Expand Down
Loading
Loading