From 372e9612dbb97422713dd50fc003700ce7005ee2 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 02:55:51 +0000 Subject: [PATCH] Add JOB-SPEC.md: hosted job scheduler and distributor\n\nSpec for cron-driven job system that injects prompts via DM.\nCovers: job YAML format, scheduler (systemd timers), dispatcher,\nagent handler convention, collector, sidechat integration,\nlogging, rate limiting. --- docs/JOB-SPEC.md | 237 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/JOB-SPEC.md diff --git a/docs/JOB-SPEC.md b/docs/JOB-SPEC.md new file mode 100644 index 0000000..7aff65f --- /dev/null +++ b/docs/JOB-SPEC.md @@ -0,0 +1,237 @@ +# JOB-SPEC.md: Hosted Job Scheduler and Distributor + +## Overview + +A hosted system on bl that processes and distributes jobs to agents via DM. +Cron jobs and system scripts inject prompts/jobs; agents execute them. +Loops run as loops on the server, not in agent heads. + +**Principle:** Limit agency to get smarter. The server decides *what* and *when*; +agents decide *how*. Deterministic, auditable, debuggable. + +## Architecture + +``` +┌──────────────────────────────────────────────────┐ +│ Hosted on bl (systemd timers + scripts) │ +│ │ +│ ┌────────────┐ ┌─────────────┐ │ +│ │ Scheduler │→ │ Dispatcher │→ dm.py send │ +│ │ (cron) │ │ (render+log)│ │ +│ └────────────┘ └─────────────┘ │ +│ ↑ ↓ │ +│ │ ┌─────────────┐ │ +│ └─────────│ Collector │← DM [RESULT] │ +│ │ (log+chain) │ │ +│ └─────────────┘ │ +└──────────────────────────────────────────────────┘ +``` + +## Components + +### 1. Job Definition (YAML) + +Location: `/home/super/Projects/NetVM/jobs/.yaml` + +```yaml +name: board-watch +description: "Check board for new posts every 5 minutes" +schedule: "*/5 * * * *" # cron format +agent: muse # which agent executes +sidechat: + create: false # use main chat + # OR: + # create: true + # name_template: "job-{name}-{date}" + # reuse_pattern: "job-{name}-*" # for chaining +prompt_template: | + Check the board for posts since {last_run}. + Summarize new items in 3 bullet points. + Reply with [RESULT {job_id}] followed by your summary. +timeout: 300 # seconds before marking failed +on_failure: retry # retry | alert | ignore +chain_next: null # job to trigger after success +``` + +**Fields:** +- `name`: Unique job identifier (used in logs, sidechat names) +- `schedule`: Cron expression (systemd timer or cron) +- `agent`: Target agent (`muse`, `pip`, `646`, `opm`) +- `sidechat`: Side chat configuration (see below) +- `prompt_template`: Jinja-style template with `{variables}` +- `timeout`: Max seconds to wait for result +- `on_failure`: What to do on timeout/failure +- `chain_next`: Next job to trigger (for chains) + +### 2. Scheduler + +Uses systemd timers (preferred) or cron. Each job gets a timer unit. + +**Systemd timer example:** +```ini +# /etc/systemd/user/job-board-watch.timer +[Unit] +Description=Run board-watch job every 5 minutes + +[Timer] +OnCalendar=*:0/5 +Persistent=true + +[Install] +WantedBy=timers.target +``` + +**Service:** +```ini +# /etc/systemd/user/job-board-watch.service +[Unit] +Description=Dispatch board-watch job + +[Service] +Type=oneshot +ExecStart=/home/super/Projects/NetVM/bin/job-dispatch.sh board-watch +``` + +### 3. Dispatcher (`bin/job-dispatch.sh`) + +Responsibilities: +1. Load job YAML +2. Render prompt template with variables (`{job_id}`, `{last_run}`, `{date}`, etc.) +3. Create side chat if specified +4. Send DM via `dm.py`: + - Format: `[JOB {job_id}] {rendered_prompt}` + - Use `--raw` if prompt is pre-signed +5. Log to `job-log.jsonl`: `{job_id, job_name, agent, sidechat, sent_at}` +6. If `chain_next`, schedule the next job (or trigger immediately on result) + +**Job ID format:** `{name}-{YYYYMMDD-HHMMSS}-{short_uuid}` +Example: `board-watch-20261004-023000-a1b2c3d4` + +### 4. Agent Job Handler (Convention) + +Agents MUST recognize job DMs and respond in format. + +**Job DM format:** +``` +[JOB board-watch-20261004-023000-a1b2c3d4] Check the board for posts +since 2026-10-04T02:25:00Z. Summarize new items in 3 bullet points. +Reply with [RESULT board-watch-20261004-023000-a1b2c3d4] followed by +your summary. +``` + +**Agent responsibilities:** +1. Parse `[JOB {job_id}]` from DM +2. Execute the prompt +3. If `sidechat` specified, work in that side chat +4. Reply via DM with: `[RESULT {job_id}] {result_text}` +5. If unable, reply: `[RESULT {job_id}] FAILED: {reason}` + +**Teaching:** New agents get `JOB-HANDLER.md` with examples. The format is +simple enough to learn from 2-3 examples. + +### 5. Collector + +Watches for `[RESULT {job_id}]` in DM logs or via `dm.py log`. + +**Responsibilities:** +1. Parse result DMs +2. Log to `job-log.jsonl`: `{job_id, completed_at, success, result_preview}` +3. If `chain_next` specified and result was success, trigger next job +4. On timeout (no result within `timeout`), mark failed, apply `on_failure` + +**Timeout handling:** A background sweeper checks for jobs with `sent_at` +older than `timeout` and no result. Marks them failed. + +## Side Chat Integration + +### Job → Side Chat Mapping + +Jobs can specify side chat behavior: + +**Option A: No side chat** (use main chat) +```yaml +sidechat: + create: false +``` + +**Option B: Create new side chat per run** +```yaml +sidechat: + create: true + name_template: "job-{name}-{date}" # e.g., "job-board-watch-20261004" +``` + +**Option C: Reuse side chat (for chains)** +```yaml +sidechat: + create: false + reuse_pattern: "job-{name}-*" # find most recent + # OR: + # reuse_name: "{prev_job_sidechat}" # from chain +``` + +### Side Chat as Job Workspace + +When `create: true`: +1. Dispatcher calls `dm.py` sidechat create (via `muse-chat-api.py`) +2. Gets the side chat name/ID +3. Includes it in the job DM: "Work in side chat 'job-board-watch-20261004'" +4. Logs the mapping: `{job_id → sidechat_name}` +5. Agent does all work in that side chat (full context, isolated) + +**Benefits:** +- Each job run has isolated context +- The side chat IS the audit log +- Chains: Job B can continue in Job A's side chat +- No cross-talk between concurrent jobs + +## Logging + +### `job-log.jsonl` (on bl) + +Append-only, one JSON per line: +```json +{"ts": "2026-10-04T02:30:00Z", "type": "job_sent", "job_id": "...", "job_name": "board-watch", "agent": "muse", "sidechat": null} +{"ts": "2026-10-04T02:32:15Z", "type": "job_result", "job_id": "...", "success": true, "duration_s": 135} +{"ts": "2026-10-04T02:35:00Z", "type": "job_timeout", "job_id": "...", "job_name": "board-watch"} +``` + +### `sidechat-log.jsonl` (on bl) + +From `sidechat_manager.py`: +```json +{"ts": "...", "agent": "muse", "op": "create", "details": "job-board-watch-20261004"} +{"ts": "...", "agent": "muse", "op": "navigate", "details": "main"} +``` + +## Rate Limiting + +Use the shared `bin/rate_limiter.py` module: +```python +from rate_limiter import rate_limit_wait +rate_limit_wait(agent) # blocks if too frequent +``` + +Default: 1 DM per 3s per agent, burst 5, max 20/min. Prevents accidental +spam if a job misfires in a loop. + +## Security + +- Job definitions are in git (auditable) +- Only operators can create/edit jobs (file permissions) +- Agents cannot create jobs (they execute, not schedule) +- DMs are logged (dm-log.jsonl) for audit +- Side chats are per-job, not shared across trust boundaries + +## Future Expansions + +1. **Conditional jobs**: Run Job B only if Job A succeeds with specific output +2. **Parallel jobs**: Fan-out to multiple agents, collect all results +3. **Human approval**: Certain jobs require human sign-off before dispatch +4. **Web UI**: View job status, logs, side chats from box.muse-dev.online +5. **Agent-proposed jobs**: Agents can suggest jobs via `[PROPOSE]` DM, human approves + +## Status + +Spec v0.1. Owner: operator-main. Sanctioned by the human 2026-10-04. +Implementation: dispatcher script + example jobs pending.