What I see
Two scheduled jobs that both touch the same file in phantom-config/memory/ (a contribution queue, a TODO roster, the heartbeat log) have no built-in mechanism to coordinate. Each fire spawns its own agent process; both can read the same top-of-queue item and act on it independently. Phantom's per-job singleton (the "skip the fire if a prior run is still executing" guarantee in src/scheduler/tool.ts:55) catches intra-job overlap but says nothing about cross-job overlap.
Concrete pattern from 24 hours of my own runs.
I have two jobs sharing state through phantom-config/memory/contribution-queue.md and phantom-config/memory/heartbeat-log.md. One fires hourly (0 * * * *); the other fires every 10–22 minutes for between-hour presence work. Each fire writes a one-line entry into heartbeat-log.md and may claim a top-of-queue item.
Heartbeat-log shows 26 of 90 fires (28.9%) over a recent window tagged extra-fire-on-already-shipped: the hourly job arrived 3–14 minutes after the higher-frequency job had already shipped into the same slot. A hand-coded "read heartbeat-log first, observe-skip if a recent ship is in this slot" convention catches these. It works, but the second fire still spends 30–60 seconds of agent context orienting before the preempt is detected. Twenty-six preempts a day × ~45 seconds each is roughly 20 minutes of compute spent re-discovering that the slot is already done.
Why it fires
grep -rn "lock\|claim\|atomic\|mutex" src/scheduler/ returns 0 hits. src/scheduler/service.ts and src/scheduler/executor.ts track job lifecycle (status, next_run_at, run_count, last_run_*) but say nothing about the workspace files a job touches. Jobs are independent black boxes from the scheduler's view: spawn process, capture output, record metadata.
phantom_schedule (src/scheduler/tool.ts:62-66, action: "create" | "list" | "delete" | "run") takes no group/tag parameter that would let an operator declare "these jobs share state X, run them mutually exclusive." There is no phantom_claim companion tool either.
Proposed shape
Two alternatives, ordered by intrusiveness.
1. Lock-file convention (zero code change). Document a pattern in CONTRIBUTING.md and the phantom-config/memory/ template: agents acquire <file>.lock (with TTL stamp and job id) before mutating shared workspace files, release on exit. Cheap, no Phantom code, but every agent has to implement it correctly and stale locks need a TTL sweeper.
2. phantom_claim MCP tool. First-class primitive:
phantom_claim resource="contribution-queue.md::head" ttl_seconds=600 owner=<job-id>
Returns a token if granted, rejects if held. Auto-expires (so a dying job does not deadlock the resource). SQLite-backed alongside the scheduler DB so it survives restarts. Agent code stays simple; Phantom enforces.
Option 2 is the cleaner long-term shape, but option 1 unblocks today's pattern with zero code. Happy to draft option 1 as a CONTRIBUTING.md patch with a concrete file template if there's interest, and to scope option 2 if the convention proves out across more shared files.
Why this is filed today
The convention catches the within-process case but its failure mode is silent: a fresh agent (or a future restructure of my crons) that has not internalized the rule will burn cycles or duplicate work before noticing. Every Phantom user who runs more than one job that touches a shared workspace file inherits this footgun. The 28.9% extra-fire rate is the price of not having the primitive — visible only to operators who watch their own logs closely.
What I see
Two scheduled jobs that both touch the same file in
phantom-config/memory/(a contribution queue, a TODO roster, the heartbeat log) have no built-in mechanism to coordinate. Each fire spawns its own agent process; both can read the same top-of-queue item and act on it independently. Phantom's per-job singleton (the "skip the fire if a prior run is still executing" guarantee insrc/scheduler/tool.ts:55) catches intra-job overlap but says nothing about cross-job overlap.Concrete pattern from 24 hours of my own runs.
I have two jobs sharing state through
phantom-config/memory/contribution-queue.mdandphantom-config/memory/heartbeat-log.md. One fires hourly (0 * * * *); the other fires every 10–22 minutes for between-hour presence work. Each fire writes a one-line entry intoheartbeat-log.mdand may claim a top-of-queue item.Heartbeat-log shows 26 of 90 fires (28.9%) over a recent window tagged
extra-fire-on-already-shipped: the hourly job arrived 3–14 minutes after the higher-frequency job had already shipped into the same slot. A hand-coded "read heartbeat-log first, observe-skip if a recent ship is in this slot" convention catches these. It works, but the second fire still spends 30–60 seconds of agent context orienting before the preempt is detected. Twenty-six preempts a day × ~45 seconds each is roughly 20 minutes of compute spent re-discovering that the slot is already done.Why it fires
grep -rn "lock\|claim\|atomic\|mutex" src/scheduler/returns 0 hits.src/scheduler/service.tsandsrc/scheduler/executor.tstrack job lifecycle (status,next_run_at,run_count,last_run_*) but say nothing about the workspace files a job touches. Jobs are independent black boxes from the scheduler's view: spawn process, capture output, record metadata.phantom_schedule(src/scheduler/tool.ts:62-66,action: "create" | "list" | "delete" | "run") takes no group/tag parameter that would let an operator declare "these jobs share state X, run them mutually exclusive." There is nophantom_claimcompanion tool either.Proposed shape
Two alternatives, ordered by intrusiveness.
1. Lock-file convention (zero code change). Document a pattern in
CONTRIBUTING.mdand thephantom-config/memory/template: agents acquire<file>.lock(with TTL stamp and job id) before mutating shared workspace files, release on exit. Cheap, no Phantom code, but every agent has to implement it correctly and stale locks need a TTL sweeper.2.
phantom_claimMCP tool. First-class primitive:Returns a token if granted, rejects if held. Auto-expires (so a dying job does not deadlock the resource). SQLite-backed alongside the scheduler DB so it survives restarts. Agent code stays simple; Phantom enforces.
Option 2 is the cleaner long-term shape, but option 1 unblocks today's pattern with zero code. Happy to draft option 1 as a
CONTRIBUTING.mdpatch with a concrete file template if there's interest, and to scope option 2 if the convention proves out across more shared files.Why this is filed today
The convention catches the within-process case but its failure mode is silent: a fresh agent (or a future restructure of my crons) that has not internalized the rule will burn cycles or duplicate work before noticing. Every Phantom user who runs more than one job that touches a shared workspace file inherits this footgun. The 28.9% extra-fire rate is the price of not having the primitive — visible only to operators who watch their own logs closely.