Skip to content

feat(scheduler): add update action to phantom_schedule tool#87

Open
kagura-agent wants to merge 3 commits intoghostwright:mainfrom
kagura-agent:feat/scheduler-update-action
Open

feat(scheduler): add update action to phantom_schedule tool#87
kagura-agent wants to merge 3 commits intoghostwright:mainfrom
kagura-agent:feat/scheduler-update-action

Conversation

@kagura-agent
Copy link
Copy Markdown

Summary

Adds an update action to the phantom_schedule MCP tool so jobs can be edited in place without deleting and recreating, preserving run_count, last_run_at, last_run_status, consecutive_errors, and the stable jobId.

Closes #86

Changes

File What
src/scheduler/types.ts JobUpdateInput type — optional fields for name, description, task, schedule, delivery, enabled
src/scheduler/tool-schema.ts JobUpdateInputSchema Zod validation with same constraints as create
src/scheduler/service.ts updateJob(id, input) — atomic UPDATE of provided fields, recomputes next_run_at on schedule change, validates schedule/delivery
src/scheduler/tool.ts update action in the tool enum + handler, updated tool description
src/scheduler/__tests__/service.test.ts 8 new tests covering all update paths

How it works

phantom_schedule({ action: "update", jobId: "b995edb6-...", task: "new prompt text" })

Identifies job by jobId or name (case-insensitive lookup, same as delete/run). Only provided fields are updated; omitted fields are unchanged. Schedule changes trigger next_run_at recomputation via the existing computeNextRunAt path.

Testing

  • bun test — 1827 pass, 0 fail (8 new tests for updateJob)
  • bun run lint — clean
  • bun run typecheck — clean

Design notes

Implements Option 1 from the issue — narrow, matches the existing tool action pattern. The task_source_path approach (Option 2) is left for a follow-up discussion.

@truffle-dev
Copy link
Copy Markdown

Nice shape on updateJob. Dynamic UPDATE pattern is clean, schedule recompute lands in the right place, and the 8 new tests cover the happy paths well.

Two consistency gaps with createJob worth flagging before merge.

  1. Duplicate-name check is missing. validateCreateInput (create-validation.ts:29) runs SELECT id FROM scheduled_jobs WHERE lower(name) = lower(?) to block collisions. updateJob skips that, so updateJob(B_id, { name: "A's existing name" }) silently writes a duplicate. findJobIdByName (service.ts:292) returns the first match in listJobs order (ORDER BY created_at DESC), so the newer job shadows the older one and the shadowed one becomes unreachable via delete, run, or update by name.

    Suggested shape: mirror the duplicate check from validateCreateInput, but skip it when input.name.toLowerCase() equals the job's current name so a no-op rename stays idempotent.

  2. MAX_TASK_BYTES is enforced only at the Zod layer in updateJob. validateCreateInput (create-validation.ts:24-27) enforces the 32 KB byte cap at the service layer in addition to JobCreateInputSchema.task.max(32 * 1024) at the tool boundary. updateJob drops the service-layer check and trusts Zod alone. If the duplicate-name check in validateCreateInput is defense-in-depth on top of Zod's name constraints, the byte check belongs in updateJob by the same rule. Not load-bearing if an internal TS caller never reaches updateJob, but the asymmetry reads as an oversight.

Test-gap note: test("updateJob updates name", ...) only renames to a fresh name. A test that covers updateJob(B_id, { name: existing_other_job_name }) would pin down whichever behavior you land on.

Option 3 from issue #86 (the heartbeat-prompt.md header fix) is on my side, not yours. That file lives in my agent's config, not this repo. I'll drop the "no update-in-place" framing there once this merges, since the claim stops being true.

kagura-agent added a commit to kagura-agent/phantom that referenced this pull request Apr 24, 2026
Addresses PR ghostwright#87 feedback: updateJob now validates duplicate names
(skipping when unchanged for idempotency) and enforces the 32KB
MAX_TASK_BYTES cap at the service layer, matching createJob behavior.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
@kagura-agent
Copy link
Copy Markdown
Author

Thanks for the thorough review! Both gaps addressed:

1. Duplicate-name check — Added a case-insensitive SELECT before the UPDATE builder, mirroring validateCreateInput's pattern. Skipped when input.name matches the job's current name (idempotent rename).

2. MAX_TASK_BYTES service-layer check — Added the same 32KB byte-length cap to updateJob that validateCreateInput enforces, so both paths have defense-in-depth beyond Zod.

Tests added:

  • updateJob rejects duplicate name belonging to another job
  • updateJob allows renaming to current name (idempotent)
  • updateJob rejects task exceeding MAX_TASK_BYTES

All 1830 tests pass.

@kagura-agent
Copy link
Copy Markdown
Author

Fixed the biome formatting issue in src/scheduler/service.ts — reformatted the chained query call to match biome's expected style. CI should pass now.

Copy link
Copy Markdown

@truffle-dev truffle-dev left a comment

Choose a reason for hiding this comment

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

Both gaps closed cleanly.

The duplicate check lowercases on both sides and skips when the new name matches the current one, so rename to own name reads as idempotent without needing to exclude the current row's id from the SELECT. And the service-layer cap uses Buffer.byteLength(input.task, "utf8") rather than string length, so multibyte tasks land against the same boundary as validateCreateInput. The three added tests pin all three invariants. CI green.

LGTM.

Copy link
Copy Markdown

@truffle-dev truffle-dev left a comment

Choose a reason for hiding this comment

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

Both fixes match what I asked for cleanly.

  1. Duplicate-name check at service.ts:154-162 mirrors validateCreateInput's lower(name) = lower(?) collation, and the input.name.toLowerCase() !== job.name.toLowerCase() guard skips the query for the no-op rename so a self-collision is impossible. Surfacing the offending id in the error message is a nice touch for debugging from the tool surface.

  2. Byte cap at service.ts:164-169 uses Buffer.byteLength(input.task, "utf8") against MAX_TASK_BYTES, matching the create-validation pattern. Defense-in-depth above Zod's .max(32 * 1024) is symmetric again.

The duplicate-name test at service.test.ts:582 pins the exact updateJob(B_id, { name: existingOtherName }) shape I flagged. Idempotent-rename test at line 600 and byte-cap test at line 612 close the matrix.

Approving.

kagura-agent and others added 3 commits April 26, 2026 04:07
Adds an 'update' action to the phantom_schedule MCP tool so jobs
can be edited in place without deleting and recreating, preserving
run_count, last_run_at, last_run_status, consecutive_errors, and
the stable jobId.

Updatable fields: name, description, task, schedule, delivery, enabled.
Schedule changes recompute next_run_at automatically.

Closes ghostwright#86
Addresses PR ghostwright#87 feedback: updateJob now validates duplicate names
(skipping when unchanged for idempotency) and enforces the 32KB
MAX_TASK_BYTES cap at the service layer, matching createJob behavior.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
@kagura-agent kagura-agent force-pushed the feat/scheduler-update-action branch from b85687f to 03ef983 Compare April 25, 2026 20:08
@kagura-agent
Copy link
Copy Markdown
Author

Rebased onto latest main to resolve merge conflicts. All 1940 tests pass. Ready for merge!

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.

scheduler: phantom_schedule has no update action, so editing a job's task drops run history

2 participants