muse-crew 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +21 -0
- package/lib/crew-api.js +44 -0
- package/lib/schema.sql +17 -0
- package/package.json +1 -1
- package/seed/cron-body-template.md +4 -2
- package/workflows/bugfix.js +1 -0
- package/workflows/chore.js +1 -0
- package/workflows/crew-dispatch.js +17 -0
- package/workflows/standard.js +1 -0
package/API.md
CHANGED
|
@@ -190,6 +190,27 @@ Two namespaces, not one: the vocabulary above is the **dashboard API** (task sta
|
|
|
190
190
|
|
|
191
191
|
Record that a poll tick occurred. Takes no arguments. Used by the dispatcher at the end of each cycle to update the last-polled timestamp.
|
|
192
192
|
|
|
193
|
+
### `reserve-dispatch`
|
|
194
|
+
|
|
195
|
+
Create a dispatch reservation for a task after the cron worker launches its workflow. The dispatcher skips reserved tasks on subsequent ticks — without this, a task whose workflow takes longer than the poll interval to self-claim would be re-dispatched every tick.
|
|
196
|
+
|
|
197
|
+
| Field | Type | Notes |
|
|
198
|
+
|-------|------|-------|
|
|
199
|
+
| `task_id` | string | Required. The task that was dispatched. |
|
|
200
|
+
| `run_id` | string | Required. The workflow run ID returned by `workflow_launch_async`. |
|
|
201
|
+
| `workflow` | string | Required. Which workflow was launched (`standard`, `bugfix`, `chore`). |
|
|
202
|
+
| `ttl_seconds` | integer | Optional. Reservation lifetime, clamped to 60–3600 seconds. Default 900 (15 minutes). |
|
|
203
|
+
|
|
204
|
+
Upserts: re-reserving the same task replaces the existing reservation. Returns the reservation with `dispatched_at` and `expires_at` timestamps.
|
|
205
|
+
|
|
206
|
+
### `clear-reservation`
|
|
207
|
+
|
|
208
|
+
Remove a task's dispatch reservation. Called by the workflow after a successful self-claim. Takes `task_id` (required). Returns `{ ok: true, cleared: <boolean> }` — `cleared` is false if no reservation existed (idempotent).
|
|
209
|
+
|
|
210
|
+
### `list-reservations`
|
|
211
|
+
|
|
212
|
+
List all active (unexpired) dispatch reservations, ordered by dispatch time. Takes no arguments. The dispatcher consumes this via `getdispatchstate` (which includes `reservations`) rather than calling it directly.
|
|
213
|
+
|
|
193
214
|
---
|
|
194
215
|
|
|
195
216
|
## Projects
|
package/lib/crew-api.js
CHANGED
|
@@ -371,6 +371,15 @@ commands["get-dispatch-state"] = (db) => {
|
|
|
371
371
|
total_sessions: sessionCount,
|
|
372
372
|
total_events: eventCount,
|
|
373
373
|
},
|
|
374
|
+
// Active dispatch reservations: tasks the worker launched but whose
|
|
375
|
+
// workflow has not yet self-claimed. The dispatcher skips these.
|
|
376
|
+
reservations: db.prepare(
|
|
377
|
+
`SELECT task_id, run_id, workflow, dispatched_at, expires_at
|
|
378
|
+
FROM dispatch_reservations WHERE expires_at > ? ORDER BY dispatched_at`
|
|
379
|
+
).all(now()).map((r) => ({
|
|
380
|
+
task_id: r.task_id, run_id: r.run_id, workflow: r.workflow,
|
|
381
|
+
dispatched_at: r.dispatched_at, expires_at: r.expires_at,
|
|
382
|
+
})),
|
|
374
383
|
};
|
|
375
384
|
};
|
|
376
385
|
|
|
@@ -562,6 +571,41 @@ commands["claim-task"] = (db, args) => {
|
|
|
562
571
|
return { ok: true, claimed: false, reason: "already_claimed", existing_session_id: existing.id };
|
|
563
572
|
};
|
|
564
573
|
|
|
574
|
+
// Dispatch reservations: the cron worker creates one after launching a
|
|
575
|
+
// workflow for a task; the dispatcher skips reserved tasks on the next tick;
|
|
576
|
+
// the workflow clears it on self-claim. Expired reservations are inert.
|
|
577
|
+
commands["reserve-dispatch"] = (db, args) => {
|
|
578
|
+
if (!args.task_id) throw usageError("task_id is required.");
|
|
579
|
+
if (!args.run_id) throw usageError("run_id is required.");
|
|
580
|
+
if (!args.workflow) throw usageError("workflow is required.");
|
|
581
|
+
requireTask(db, args.task_id);
|
|
582
|
+
const ttl = Math.max(60, Math.min(3600, Number(args.ttl_seconds) || 900));
|
|
583
|
+
const dispatchedAt = now();
|
|
584
|
+
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
585
|
+
db.prepare(
|
|
586
|
+
`INSERT INTO dispatch_reservations (task_id, run_id, workflow, dispatched_at, expires_at)
|
|
587
|
+
VALUES (?, ?, ?, ?, ?)
|
|
588
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
589
|
+
run_id = excluded.run_id, workflow = excluded.workflow,
|
|
590
|
+
dispatched_at = excluded.dispatched_at, expires_at = excluded.expires_at`
|
|
591
|
+
).run(args.task_id, args.run_id, args.workflow, dispatchedAt, expiresAt);
|
|
592
|
+
return { ok: true, task_id: args.task_id, run_id: args.run_id, workflow: args.workflow, dispatched_at: dispatchedAt, expires_at: expiresAt };
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
commands["clear-reservation"] = (db, args) => {
|
|
596
|
+
if (!args.task_id) throw usageError("task_id is required.");
|
|
597
|
+
const info = db.prepare("DELETE FROM dispatch_reservations WHERE task_id = ?").run(args.task_id);
|
|
598
|
+
return { ok: true, cleared: info.changes > 0 };
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
commands["list-reservations"] = (db) => {
|
|
602
|
+
const rows = db.prepare(
|
|
603
|
+
`SELECT task_id, run_id, workflow, dispatched_at, expires_at
|
|
604
|
+
FROM dispatch_reservations WHERE expires_at > ? ORDER BY dispatched_at`
|
|
605
|
+
).all(now());
|
|
606
|
+
return { ok: true, reservations: rows };
|
|
607
|
+
};
|
|
608
|
+
|
|
565
609
|
commands["park-task"] = (db, args) => {
|
|
566
610
|
if (!args.task_id) throw usageError("task_id is required.");
|
|
567
611
|
const message = (args.message ?? "").trim();
|
package/lib/schema.sql
CHANGED
|
@@ -99,3 +99,20 @@ CREATE INDEX IF NOT EXISTS events_task_id_idx ON events(task_id);
|
|
|
99
99
|
-- another session holds the claim.
|
|
100
100
|
CREATE UNIQUE INDEX IF NOT EXISTS agent_sessions_one_running_per_task_idx
|
|
101
101
|
ON agent_sessions(task_id) WHERE status = 'running';
|
|
102
|
+
|
|
103
|
+
-- Dispatch reservations: the dispatcher recommends, the cron worker launches,
|
|
104
|
+
-- and the launched workflow self-claims. The claim can take 15+ minutes for
|
|
105
|
+
-- cron-launched runs (canary 2026-09-13), far longer than the 3-minute poll
|
|
106
|
+
-- interval — without a reservation, every tick re-dispatches the same task.
|
|
107
|
+
-- The worker creates a reservation after launching; the dispatcher skips
|
|
108
|
+
-- reserved tasks; the workflow clears it on self-claim. Expired reservations
|
|
109
|
+
-- are ignored (the workflow died before claiming).
|
|
110
|
+
CREATE TABLE IF NOT EXISTS dispatch_reservations (
|
|
111
|
+
task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
112
|
+
run_id TEXT NOT NULL,
|
|
113
|
+
workflow TEXT NOT NULL,
|
|
114
|
+
dispatched_at TEXT NOT NULL,
|
|
115
|
+
expires_at TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS dispatch_reservations_expires_idx
|
|
118
|
+
ON dispatch_reservations(expires_at);
|
package/package.json
CHANGED
|
@@ -13,8 +13,10 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
13
13
|
Wait for it to complete. It reads the crew's task state, determines eligibility, claims tasks, acknowledges the poll, and returns structured results.
|
|
14
14
|
|
|
15
15
|
4. **Launch claims autonomously:** Extract the `claims` array from the dispatcher result. For each claim (up to 3 per tick — if more than 3, launch the first 3 and log the rest as deferred):
|
|
16
|
-
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}.
|
|
17
|
-
-
|
|
16
|
+
- Call workflow_launch_async with scriptPath={claim.scriptPath} and args={claim.args}. Record the returned run_id.
|
|
17
|
+
- Immediately create a dispatch reservation so the next tick does not re-dispatch before the workflow self-claims (claims can take 15+ minutes for cron-launched runs). Run in shell:
|
|
18
|
+
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} reserve-dispatch --json '{"task_id": "{claim.task_id}", "run_id": "<run_id>", "workflow": "{claim.workflow}"}'`
|
|
19
|
+
- The launched workflow self-claims the task and clears the reservation as its first actions. If the task was already claimed or is done, the claim fails closed and the run stands down quietly — this is the mechanical duplicate protection, not an error.
|
|
18
20
|
|
|
19
21
|
If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH
|
|
20
22
|
|
package/workflows/bugfix.js
CHANGED
|
@@ -1060,6 +1060,7 @@ while (i < STEPS.length) {
|
|
|
1060
1060
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1061
1061
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1062
1062
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1063
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1063
1064
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1064
1065
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1065
1066
|
{
|
package/workflows/chore.js
CHANGED
|
@@ -1017,6 +1017,7 @@ while (i < STEPS.length) {
|
|
|
1017
1017
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1018
1018
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1019
1019
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1020
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1020
1021
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1021
1022
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1022
1023
|
{
|
|
@@ -237,6 +237,15 @@ const allTasks = boardData.ready_tasks.map(projectTaskRecord).filter(function (t
|
|
|
237
237
|
const config = boardData.config || {};
|
|
238
238
|
const projects = boardData.projects || [];
|
|
239
239
|
|
|
240
|
+
// Dispatch reservations: tasks the worker launched on a previous tick whose
|
|
241
|
+
// workflow has not yet self-claimed. The claim can take 15+ minutes for
|
|
242
|
+
// cron-launched runs (canary 2026-09-13) — far longer than the poll interval —
|
|
243
|
+
// so without this filter every tick would re-dispatch the same task.
|
|
244
|
+
const reservedTaskIds = new Set((boardData.reservations || []).map(function (r) { return r.task_id; }));
|
|
245
|
+
if (reservedTaskIds.size > 0) {
|
|
246
|
+
log("Reservations: skipping " + reservedTaskIds.size + " task(s) with an active dispatch reservation");
|
|
247
|
+
}
|
|
248
|
+
|
|
240
249
|
// Default project: explicit arg, or first registered project
|
|
241
250
|
const DEFAULT_PROJECT = inputs.defaultProject || (projects.length > 0 ? projects[0].id : "");
|
|
242
251
|
|
|
@@ -383,6 +392,14 @@ for (var t = 0; t < allTasks.length; t++) {
|
|
|
383
392
|
var task = allTasks[t];
|
|
384
393
|
if (task.blocked) continue;
|
|
385
394
|
|
|
395
|
+
// Skip tasks with an active dispatch reservation: the worker launched a
|
|
396
|
+
// workflow for this task on a previous tick, but the workflow has not yet
|
|
397
|
+
// self-claimed (claims can take 15+ minutes for cron-launched runs).
|
|
398
|
+
if (reservedTaskIds.has(task.id)) {
|
|
399
|
+
log("Skipped \"" + task.title + "\" — active dispatch reservation (workflow launched, claim pending)");
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
386
403
|
// Skip tasks from quiesced projects
|
|
387
404
|
var taskProject = task.project || DEFAULT_PROJECT;
|
|
388
405
|
if (quiescedProjects[taskProject]) {
|
package/workflows/standard.js
CHANGED
|
@@ -1059,6 +1059,7 @@ while (i < STEPS.length) {
|
|
|
1059
1059
|
"Claim this task for the " + step.name + " step.\n" +
|
|
1060
1060
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("update-task", firstClaimUpdateArgs) + "\n" +
|
|
1061
1061
|
"Then run in shell and return the stdout verbatim:\n" + crewCmd("claim-task", { task_id: taskId, identity: step.identity, step: step.name, notes: step.name + " step started" }) + "\n" +
|
|
1062
|
+
"If the claim response has claimed=true, then run in shell and return the stdout verbatim:\n" + crewCmd("clear-reservation", { task_id: taskId }) + "\n" +
|
|
1062
1063
|
"Do not interpret the claim response. It already contains an explicit \"claimed\" field — copy it verbatim.\n" +
|
|
1063
1064
|
"Return { claimed: <verbatim>, session_id: \"<...>\" }. If claimed is false there is no session_id; return { claimed: false, session_id: \"\" }.",
|
|
1064
1065
|
{
|