Skip to content

feat(server): tunable per-watcher execution_config for device-worker runs#1058

Merged
buremba merged 4 commits into
mainfrom
feat/watcher-execution-config
May 26, 2026
Merged

feat(server): tunable per-watcher execution_config for device-worker runs#1058
buremba merged 4 commits into
mainfrom
feat/watcher-execution-config

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 25, 2026

Why

Device-worker watcher runs (Owletto Mac app's `WatcherDispatcher`, which spawns the local `claude`/`codex` CLI) were capped at a hardcoded 60s timeout, so every real agent loop got SIGTERM-killed — the "claude exited via SIGTERM after 60s timeout" failures on the menubar Recent Activity. Make per-run execution tunable per watcher.

What

New nullable `watchers.execution_config` jsonb (mirrors `model_config`; one column, no migration for future knobs):

```jsonc
{ "timeout_seconds": 1800, "max_budget_usd": 2, "model": "opus",
"permission_mode": "acceptEdits", "effort": "high" }
```

  • Migration `20260525120000_watcher_execution_config.sql` — `ADD COLUMN IF NOT EXISTS execution_config jsonb` (idempotent). NULL = defaults.
  • `manage_watchers` — TypeBox field + `create` / `update` / `create_from_version` (clone copies it) + `handleList` projection; sandbox SDK `WatcherCreateInput`/`WatcherUpdateInput` + new `WatcherExecutionConfig` type.
  • `worker-api` — emits `payload.watcher.execution_config` in the `/api/workers/poll` envelope (consumed by the Swift dispatcher).

Pairs with lobu-ai/owletto#222 (dispatcher: default 60s→600s, config-driven claude flags, UI). This PR bumps `packages/owletto` to that commit.

Validation (red→green, run locally)

  • Migration applies cleanly on the full baseline schema — on Homebrew PG17 and ephemeral embedded PG18+pgvector (the `lobu run` engine).
  • CRUD round-trip — new `watchers-crud` tests: `execution_config` persists → reads back via `list` → updates → null when unset.
  • No regression — all 50 watcher integration tests pass; `make review` green (typecheck/unit/integration all 0; pi: bug_free 76, simplicity 82, slop 0, 0 blockers).

Notes

  • No runtime validation of watcher args (consistent with `model_config` = `Type.Any()`); the dispatcher defends against bad values (ignores non-positive numerics + empty strings).
  • `payload.watcher.execution_config` is covered indirectly (the modified poll query runs in the passing device-path tests); a direct poll-HTTP assertion was skipped (full device-auth/pin setup, high flakiness for low marginal gain).
  • Merge order: merge owletto#222 first; if it squash-merges, re-bump this PR's `packages/owletto` pointer to owletto `main`'s new tip before merging (reachable-SHA requirement).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Per-watcher execution settings (timeout, budget, model, permission mode, effort) — persist, can be cloned, updated, or cleared (null) to revert to defaults; visible in watcher listings.
  • Integration
    • Worker job payloads now include watcher execution settings so workers receive per-watcher overrides.
  • Tests
    • Added integration and unit tests for CRUD behavior, validation, null-clearing, and permission gating.

Review Change Stack

…runs

Adds watchers.execution_config jsonb { timeout_seconds, max_budget_usd, model,
permission_mode, effort } so device-worker watcher runs (local claude/codex CLI)
are no longer capped at a hardcoded 60s. NULL = defaults.

- Migration: ADD COLUMN execution_config jsonb (idempotent).
- manage_watchers: TypeBox field + create/update/create_from_version + list
  projection; sandbox SDK WatcherCreateInput/WatcherUpdateInput types.
- worker-api: emit payload.watcher.execution_config in the poll envelope.
- Bumps packages/owletto pointer to the paired dispatcher + UI change.

Round-trip + migration validated (watchers-crud test, full integration suite,
embedded PG18).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6ecaf7f-a45e-47cd-9efd-24bb1dbc6a4d

📥 Commits

Reviewing files that changed from the base of the PR and between 2489fa1 and 03467f2.

📒 Files selected for processing (1)
  • packages/owletto
✅ Files skipped from review due to trivial changes (1)
  • packages/owletto

📝 Walkthrough

Walkthrough

Adds a nullable JSONB execution_config column to watchers, TypeScript types and a runtime validator for the config, extends admin create/update/clone/list handlers to validate/persist/project it, includes it in worker poll payloads, and adds unit and integration tests plus a submodule bump.

Changes

Watcher Execution Configuration Support

Layer / File(s) Summary
Database migration and TypeScript types
db/migrations/20260525120000_watcher_execution_config.sql, packages/server/src/sandbox/namespaces/watchers.ts, packages/server/src/utils/table-schema.ts
Adds nullable execution_config JSONB column to public.watchers, exposes it in QUERYABLE_SCHEMA, and introduces exported WatcherExecutionConfig plus create/update input fields (null clears).
Execution config schema and validator
packages/server/src/tools/admin/watcher-execution-config.ts
Adds TypeBox WatcherExecutionConfigSchema, compiles a runtime validator, defines elevated permission modes, ExecutionConfigCaller interface, and assertValidExecutionConfig which validates shape and enforces elevated-mode authorization.
Admin API schema and CRUD persistence
packages/server/src/tools/admin/manage_watchers.ts
Extends ManageWatchersSchema to accept nullable execution_config; validates on create/update, persists on create and clone, conditionally updates watchers.execution_config on updates, and projects i.execution_config in list responses.
Worker API integration
packages/server/src/worker-api.ts
pollWorkerJob selects watchers.execution_config (aliased watcher_execution_config), extends the typed row, and exposes payload.watcher.execution_config (or null) in worker claim responses.
Tests (unit & integration)
packages/server/src/__tests__/unit/watcher-execution-config.test.ts, packages/server/src/__tests__/integration/watchers/*, packages/server/src/__tests__/integration/watchers/manual-trigger.test.ts
Unit tests cover schema validation and permission gating for assertValidExecutionConfig. Integration tests verify create/list/update/clear round-trip, omission -> null, validation rejects invalid payloads, and worker poll includes the configured execution_config.

Dependency Update

Layer / File(s) Summary
Owletto submodule update
packages/owletto
Updates the submodule commit reference used by the repo.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lobu-ai/lobu#814: Modifies pollWorkerJob watcher-run SQL and payload shaping on the same code path (adds device-pinning fields).
  • lobu-ai/lobu#811: Changes manage_watchers schema and persistence logic for watcher fields; overlaps at the same handler and update/clone code paths.
  • lobu-ai/lobu#824: Related to manual-trigger/poll flow tests that exercise watcher payloads used by workers.

Poem

🐰 I found a JSON burrow neat and small,

timeout, budget, model — stored for all.
It hops to DB, then back to the list,
null when absent, exact when kissed.
Hooray — a watcher config for the call!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding tunable per-watcher execution configuration for device-worker runs, which is the core feature across all modified files.
Description check ✅ Passed The description comprehensively covers the change rationale, implementation details, validation approach, and merge considerations; all essential context is provided for reviewers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/watcher-execution-config

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server/src/tools/admin/manage_watchers.ts`:
- Around line 409-452: The schema for execution_config currently only accepts an
object so clients cannot clear it; update the Type.Optional wrapper around
Type.Object for execution_config to allow explicit null as a valid value (e.g.,
make it a Type.Optional(Type.Union([Type.Null(), Type.Object(...)])) so callers
can send null to clear saved config), keep the same inner object shape,
descriptions and additionalProperties settings, and ensure any downstream
validators that check execution_config handle null correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf1e8a9b-0891-46be-a92f-8f7fba7aa38e

📥 Commits

Reviewing files that changed from the base of the PR and between 26efe00 and bc10fc4.

📒 Files selected for processing (6)
  • db/migrations/20260525120000_watcher_execution_config.sql
  • packages/owletto
  • packages/server/src/__tests__/integration/watchers/watchers-crud.test.ts
  • packages/server/src/sandbox/namespaces/watchers.ts
  • packages/server/src/tools/admin/manage_watchers.ts
  • packages/server/src/worker-api.ts

Comment thread packages/server/src/tools/admin/manage_watchers.ts Outdated
buremba added 2 commits May 26, 2026 00:36
Addresses CodeRabbit/pi review: the nullable column was write-once because the
schema only accepted an object. Widen the TypeBox + SDK update type to a
null-union so update can reset execution_config to NULL/defaults (omitted =
unchanged, null = clear, object = replace). The UPDATE SQL already mapped null
→ SQL NULL via toJsonParam. Test extended to cover the null-clear path.
Addresses multi-aspect review of the watcher execution_config PR:

- Runtime validation (watcher-execution-config.ts): manage_watchers args are
  not schema-checked at the boundary, so a type-wrong value (e.g.
  timeout_seconds as a string) would persist and then fail the device-worker's
  strict payload decode — silently disabling every run of that watcher. Compile
  the TypeBox schema and reject invalid types/enums/range on create + update.
- Security: gate elevated permission modes (bypassPermissions, dontAsk) behind
  owner/admin. device_worker_id is not ownership-checked, so a member-write
  actor could otherwise pin a watcher to another user's device and run an agent
  there with permissions bypassed. System/internal callers are exempt.
- Bound timeout_seconds with a 24h maximum so an unbounded value can't wedge a
  device's serialized watcher queue.
- query_sql parity: add execution_config to QUERYABLE_SCHEMA (watchers), fixing
  the table-schema drift test (DATABASE_URL mode) and enabling admin SELECTs.
- Tests: unit coverage for validation + the owner gate; integration coverage
  for the /api/workers/poll payload carrying execution_config end-to-end.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server/src/tools/admin/manage_watchers.ts (1)

1187-1189: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Re-validate inherited execution_config before cloning.

create_from_version copies w.execution_config straight into the new watcher without going through assertValidExecutionConfig. That bypasses the owner/admin gate for bypassPermissions / dontAsk, and it can also replicate any legacy invalid config already stored in the source row.

Suggested fix
   if (!version.agent_id) {
     // Source watcher has no agent — cloning would silently inherit null and
     // produce active zombies the scheduler skips. Same invariant as handleCreate.
     throw new ToolUserError(
       `Source watcher version ${args.version_id} has no agent_id; assign an agent on the source before cloning.`
     );
   }
+  const inheritedExecutionConfig = version.execution_config ?? null;
+  assertValidExecutionConfig(inheritedExecutionConfig, ctx);

   // Fetch entity names for name pattern substitution
   const entityRows = await sql`
@@
         ${`{${entityId}}`}::bigint[],
         ${version.schedule ?? null}, ${version.schedule ? nextRunAt(version.schedule as string) : null},
         ${version.agent_id ?? null}, ${version.scheduler_client_id ?? null},
-        ${toJsonParam(sql, version.model_config)}, ${toJsonParam(sql, version.execution_config)}, ${toJsonParam(sql, sources)},
+        ${toJsonParam(sql, version.model_config)}, ${toJsonParam(sql, inheritedExecutionConfig)}, ${toJsonParam(sql, sources)},
         ${(version.version as number) ?? 1}, ${sharedVersionId}, ${toTextArrayParam((version.tags as string[]) || [])}::text[],
         'active', ${createdBy}, NOW(), NOW(),

Also applies to: 1243-1255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/tools/admin/manage_watchers.ts` around lines 1187 - 1189,
The code path that clones watchers in create_from_version currently copies
w.execution_config directly and bypasses validation; update create_from_version
to run the source execution_config through assertValidExecutionConfig (the same
validation used for new watchers) before assigning it to the cloned watcher,
ensuring owner/admin-only flags like bypassPermissions/dontAsk are enforced and
legacy invalid configs are rejected or normalized; apply the same change to the
related cloning code around the other block mentioned (lines ~1243-1255) so both
cloning locations call assertValidExecutionConfig and use its
validated/normalized result instead of w.execution_config raw.
🧹 Nitpick comments (1)
packages/server/src/tools/admin/manage_watchers.ts (1)

731-732: ⚡ Quick win

Remove the unused env parameter from handleUpdate.

handleUpdate does not use env, so this should be deleted rather than kept as _env.

Suggested fix
-    update: () => handleUpdate(args, env, ctx),
+    update: () => handleUpdate(args, ctx),
@@
 async function handleUpdate(
   args: ManageWatchersArgs,
-  _env: Env,
   ctx: ToolContext
 ): Promise<{ action: 'update'; watcher_id: string; updated_fields: string[] }> {
As per coding guidelines, "Fix unused parameters by deleting them, not by prefixing with `_`".

Also applies to: 1278-1281

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/tools/admin/manage_watchers.ts` around lines 731 - 732,
handleUpdate currently declares an unused env parameter; remove that parameter
from the handleUpdate function signature and update every call site to stop
passing env. Specifically, change the handler registration that reads update: ()
=> handleUpdate(args, env, ctx) (and any similar references near the other
mapping around handleCreateVersion) to call handleUpdate with only the required
arguments (e.g., handleUpdate(args, ctx) or handleUpdate(args, ctx, ...) as
appropriate), and remove the unused env parameter from the function definition
of handleUpdate itself so no callers or signatures include env.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/server/src/tools/admin/manage_watchers.ts`:
- Around line 1187-1189: The code path that clones watchers in
create_from_version currently copies w.execution_config directly and bypasses
validation; update create_from_version to run the source execution_config
through assertValidExecutionConfig (the same validation used for new watchers)
before assigning it to the cloned watcher, ensuring owner/admin-only flags like
bypassPermissions/dontAsk are enforced and legacy invalid configs are rejected
or normalized; apply the same change to the related cloning code around the
other block mentioned (lines ~1243-1255) so both cloning locations call
assertValidExecutionConfig and use its validated/normalized result instead of
w.execution_config raw.

---

Nitpick comments:
In `@packages/server/src/tools/admin/manage_watchers.ts`:
- Around line 731-732: handleUpdate currently declares an unused env parameter;
remove that parameter from the handleUpdate function signature and update every
call site to stop passing env. Specifically, change the handler registration
that reads update: () => handleUpdate(args, env, ctx) (and any similar
references near the other mapping around handleCreateVersion) to call
handleUpdate with only the required arguments (e.g., handleUpdate(args, ctx) or
handleUpdate(args, ctx, ...) as appropriate), and remove the unused env
parameter from the function definition of handleUpdate itself so no callers or
signatures include env.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 601dff65-757e-4532-85c5-2e79b4f2781e

📥 Commits

Reviewing files that changed from the base of the PR and between f4403c9 and 2489fa1.

📒 Files selected for processing (6)
  • packages/server/src/__tests__/integration/watchers/manual-trigger.test.ts
  • packages/server/src/__tests__/integration/watchers/watchers-crud.test.ts
  • packages/server/src/__tests__/unit/watcher-execution-config.test.ts
  • packages/server/src/tools/admin/manage_watchers.ts
  • packages/server/src/tools/admin/watcher-execution-config.ts
  • packages/server/src/utils/table-schema.ts

@buremba
Copy link
Copy Markdown
Member Author

buremba commented May 26, 2026

bug_free 88, simplicity 86, slop 5, bugs 0, 0 blockers

Read local diff and suite logs: typecheck/unit/integration all passed. git diff --check clean. Inspected owletto submodule diff and local claude CLI flag support. Skipped server boot because DATABASE_URL was not exported in this review shell.

Full verdict JSON
{
  "bug_free_confidence": 88,
  "bugs": 0,
  "slop": 5,
  "simplicity": 86,
  "blockers": [],
  "change_type": "feat",
  "behavior_change_risk": "medium",
  "tests_adequate": true,
  "suggested_fixes": [],
  "notes": "Read local diff and suite logs: typecheck/unit/integration all passed. git diff --check clean. Inspected owletto submodule diff and local claude CLI flag support. Skipped server boot because DATABASE_URL was not exported in this review shell.",
  "categories": {
    "src": 165,
    "tests": 247,
    "docs": 0,
    "config": 0,
    "deps": 2,
    "migrations": 18,
    "ci": 0,
    "generated": 0
  }
}

Local review gate — branch protection can require the pi-review commit status. See docs/REVIEW_SCHEMA.md.

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.

2 participants