muse-crew 0.7.4 → 0.7.6
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 +4 -2
- package/lib/crew-api.js +40 -2
- package/lib/schema.sql +11 -0
- package/package.json +1 -1
- package/seed/cron-body-template.md +6 -9
package/API.md
CHANGED
|
@@ -214,7 +214,7 @@ Update a dispatch reservation with the real platform workflow run ID. Called by
|
|
|
214
214
|
| `task_id` | string | Required. The task that was dispatched. |
|
|
215
215
|
| `run_id` | string | Required. The real platform run ID (e.g. `workflow-run-xxx`). Must not be `"pending-dispatch"`. |
|
|
216
216
|
|
|
217
|
-
Fails closed: throws if no reservation exists for the task (launching without a reservation is forbidden). Returns `{ ok: true, task_id, run_id }`.
|
|
217
|
+
Fails closed: throws if no reservation exists for the task (launching without a reservation is forbidden). Also records a durable platform run -> task mapping (`platform_run_tasks`) — the worker is the only party that ever knows both IDs at once, and the poll loop's platform-failure monitor resolves the task through this mapping, so a dead run is requeued on the next tick instead of waiting out the zombie-session sweep. Returns `{ ok: true, task_id, run_id }`.
|
|
218
218
|
|
|
219
219
|
### `clear-reservation`
|
|
220
220
|
|
|
@@ -295,7 +295,7 @@ The platform records workflow run status in `runtime.workflow_runs` — separate
|
|
|
295
295
|
|
|
296
296
|
### `record-platform-failure`
|
|
297
297
|
|
|
298
|
-
Record a platform workflow run failure. Correlates
|
|
298
|
+
Record a platform workflow run failure. Correlates the task in this order: (1) the durable `platform_run_tasks` mapping written by `acknowledge-dispatch-run`, (2) timestamp proximity (±60s) between `platform_created_at` and crew telemetry launch time (fallback for runs launched before the mapping existed), (3) an explicit `crew_run_id`/`task_id` when provided. A failure that correlates to no task is kept with `task_id: null` and reported as skipped by retry.
|
|
299
299
|
|
|
300
300
|
| Field | Type | Notes |
|
|
301
301
|
|-------|------|-------|
|
|
@@ -324,6 +324,8 @@ Returns `{ ok: true, failures: [...] }`.
|
|
|
324
324
|
|
|
325
325
|
Retry a task whose workflow died on a platform failure. Clears the stale reservation, re-queues the task to `todo`, and increments the retry count. If `retry_count` >= `max_retries` (default 3), parks the task instead.
|
|
326
326
|
|
|
327
|
+
**Supersede guard:** if a newer platform run has been linked for this task since this failure's run, the task is already owned by the successor — the retry is skipped instead of clobbering live work (a late detection of an old dead run never resets a redispatched task).
|
|
328
|
+
|
|
327
329
|
| Field | Type | Notes |
|
|
328
330
|
|-------|------|-------|
|
|
329
331
|
| `platform_run_id` | string | Required. |
|
package/lib/crew-api.js
CHANGED
|
@@ -628,6 +628,17 @@ commands["acknowledge-dispatch-run"] = (db, args) => {
|
|
|
628
628
|
if (info.changes === 0) {
|
|
629
629
|
throw usageError("no reservation exists for task_id — launch without reservation is forbidden.");
|
|
630
630
|
}
|
|
631
|
+
// Durable platform run -> task mapping (2026-09-13): the worker is the only
|
|
632
|
+
// party that ever knows both IDs at once. record-platform-failure resolves
|
|
633
|
+
// the task through this mapping instead of timestamp proximity, so a dead
|
|
634
|
+
// platform run is requeued on the next tick instead of waiting out the
|
|
635
|
+
// 1-hour zombie-session sweep.
|
|
636
|
+
db.prepare(
|
|
637
|
+
`INSERT INTO platform_run_tasks (platform_run_id, task_id)
|
|
638
|
+
VALUES (?, ?)
|
|
639
|
+
ON CONFLICT(platform_run_id) DO UPDATE SET
|
|
640
|
+
task_id = excluded.task_id, linked_at = datetime('now')`
|
|
641
|
+
).run(args.run_id, args.task_id);
|
|
631
642
|
return { ok: true, task_id: args.task_id, run_id: args.run_id };
|
|
632
643
|
};
|
|
633
644
|
|
|
@@ -735,11 +746,19 @@ commands["record-platform-failure"] = (db, args) => {
|
|
|
735
746
|
if (!args.platform_run_id) throw usageError("platform_run_id is required.");
|
|
736
747
|
if (!args.error_message) throw usageError("error_message is required.");
|
|
737
748
|
if (!args.platform_created_at) throw usageError("platform_created_at is required.");
|
|
738
|
-
//
|
|
739
|
-
//
|
|
749
|
+
// Correlate with a crew task. First the durable platform_run_tasks mapping
|
|
750
|
+
// (written by acknowledge-dispatch-run — the worker is the only party that
|
|
751
|
+
// ever knows both IDs at once). Timestamp proximity is the fallback for
|
|
752
|
+
// runs launched before the mapping existed.
|
|
740
753
|
let crewRunId = args.crew_run_id || null;
|
|
741
754
|
let taskId = args.task_id || null;
|
|
742
755
|
let workflow = args.workflow || null;
|
|
756
|
+
if (!taskId) {
|
|
757
|
+
const link = db.prepare(
|
|
758
|
+
"SELECT task_id FROM platform_run_tasks WHERE platform_run_id = ?"
|
|
759
|
+
).get(args.platform_run_id);
|
|
760
|
+
if (link) taskId = link.task_id;
|
|
761
|
+
}
|
|
743
762
|
if (!crewRunId) {
|
|
744
763
|
const match = db.prepare(
|
|
745
764
|
`SELECT run_id, task_id, workflow FROM workflow_runs
|
|
@@ -803,6 +822,25 @@ commands["retry-platform-failure"] = (db, args) => {
|
|
|
803
822
|
if (!failure.task_id) {
|
|
804
823
|
return { ok: true, action: "skipped", reason: "no task correlated" };
|
|
805
824
|
}
|
|
825
|
+
// Supersede guard: if a newer platform run was linked for this task after
|
|
826
|
+
// this failure's run, the task is already owned by the newer run —
|
|
827
|
+
// requeueing here would clobber live work. Skip instead of retrying.
|
|
828
|
+
// (Without this, a late Step-0 detection of an old dead run could reset a
|
|
829
|
+
// task its successor is already working.)
|
|
830
|
+
const myLink = db.prepare(
|
|
831
|
+
"SELECT linked_at, rowid AS rid FROM platform_run_tasks WHERE platform_run_id = ?"
|
|
832
|
+
).get(args.platform_run_id);
|
|
833
|
+
if (myLink) {
|
|
834
|
+
const newer = db.prepare(
|
|
835
|
+
`SELECT platform_run_id FROM platform_run_tasks
|
|
836
|
+
WHERE task_id = ? AND platform_run_id != ?
|
|
837
|
+
AND (linked_at > ? OR (linked_at = ? AND rowid > ?))
|
|
838
|
+
ORDER BY linked_at DESC, rowid DESC LIMIT 1`
|
|
839
|
+
).get(failure.task_id, args.platform_run_id, myLink.linked_at, myLink.linked_at, myLink.rid);
|
|
840
|
+
if (newer) {
|
|
841
|
+
return { ok: true, action: "skipped", reason: "superseded by newer run " + newer.platform_run_id };
|
|
842
|
+
}
|
|
843
|
+
}
|
|
806
844
|
if (failure.retry_count >= maxRetries) {
|
|
807
845
|
// Park the task — transient failures are not resolving.
|
|
808
846
|
const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(failure.task_id);
|
package/lib/schema.sql
CHANGED
|
@@ -161,3 +161,14 @@ CREATE TABLE IF NOT EXISTS platform_run_failures (
|
|
|
161
161
|
);
|
|
162
162
|
CREATE INDEX IF NOT EXISTS platform_run_failures_task_id_idx ON platform_run_failures(task_id);
|
|
163
163
|
CREATE INDEX IF NOT EXISTS platform_run_failures_detected_at_idx ON platform_run_failures(detected_at);
|
|
164
|
+
|
|
165
|
+
-- Durable platform run -> task mapping (2026-09-13). The poll worker is the
|
|
166
|
+
-- only party that ever knows both IDs at once, so acknowledge-dispatch-run
|
|
167
|
+
-- records the pairing here. record-platform-failure resolves the task through
|
|
168
|
+
-- this mapping; the old timestamp-proximity match is a fallback only.
|
|
169
|
+
CREATE TABLE IF NOT EXISTS platform_run_tasks (
|
|
170
|
+
platform_run_id TEXT PRIMARY KEY,
|
|
171
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
172
|
+
linked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS platform_run_tasks_task_id_idx ON platform_run_tasks(task_id);
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
13
13
|
WHERE w.created_at > now() - interval '15 minutes' AND w.status = 'failed'
|
|
14
14
|
ORDER BY w.created_at DESC
|
|
15
15
|
```
|
|
16
|
-
- For each failed run, record it in the crew DB (correlates
|
|
16
|
+
- For each failed run, record it in the crew DB (correlates via the durable platform run -> task mapping written at acknowledge time; timestamp proximity is the fallback for pre-mapping runs):
|
|
17
17
|
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-platform-failure --json '{"platform_run_id": "<run_id>", "error_message": "<error>", "platform_created_at": "<created_at>"}'`
|
|
18
18
|
- Then retry each failure (clears stale reservation, re-queues task, or parks after 3 attempts):
|
|
19
19
|
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} retry-platform-failure --json '{"platform_run_id": "<run_id>"}'`
|
|
@@ -36,24 +36,21 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
36
36
|
|
|
37
37
|
If the dispatcher returned no claims or the claims array is empty, report: NO_DISPATCH and exit.
|
|
38
38
|
|
|
39
|
-
5. **Monitor launched workflows
|
|
40
|
-
- For each launched run_id, poll its status every
|
|
39
|
+
5. **Monitor launched workflows within this tick (tick-bounded — 2026-09-13):** The platform ties async workflow subagent authorization to the launcher's lifetime: when THIS tick ends (120s timeout), any still-running workflow's next `agent()` call may fail with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". You cannot prevent that by staying alive — this tick WILL end at 120s, so a "stay alive until terminal" monitor is fiction. The design that survives launcher death is per-tick recovery: watch while you live, and let the next tick's Step 0 continue through the durable platform run -> task mapping.
|
|
40
|
+
- For each launched run_id, poll its status every ~20 seconds via muse.db (you have ~100s of monitoring budget — leave margin before the 120s kill):
|
|
41
41
|
```sql
|
|
42
42
|
SELECT status, error FROM runtime.workflow_runs WHERE run_id = '<run_id>'
|
|
43
43
|
```
|
|
44
44
|
- **If status is `completed`:** Done. Log success and stop monitoring this run.
|
|
45
45
|
- **If status is `failed`:** Check if it's a platform `agent()` error (error contains "subagent bootstrap", "reservation owner is terminal", "bootstrap was cancelled", or "workflow agent call failed"):
|
|
46
|
-
- **Platform error:** Record it and retry:
|
|
47
|
-
1. `node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-platform-failure --json '{"platform_run_id": "<run_id>", "error_message": "<error>"}'`
|
|
48
|
-
2. `node {crewHome}/lib/crew-api.js --crew-home {crewHome} retry-platform-failure --json '{"platform_run_id": "<run_id>"}'`
|
|
49
|
-
3. If the retry result says `requeued` and you have retries remaining (max 3 per task per tick):
|
|
46
|
+
- **Platform error:** Record it and retry immediately (same two commands as Step 0). If the retry says `requeued` AND you have at least ~40s left in this tick AND retries remain (max 3 per task per tick):
|
|
50
47
|
- Re-acquire the reservation: `node {crewHome}/lib/crew-api.js --crew-home {crewHome} reserve-dispatch --json '{"task_id": "<task_id>"}'`
|
|
51
48
|
- If `acquired` is true, re-launch via workflow_launch_async with the same scriptPath and args, acknowledge with the new run_id, and continue monitoring the NEW run_id.
|
|
52
49
|
- If `acquired` is false, stop — another dispatcher claimed it.
|
|
53
|
-
|
|
50
|
+
If the retry result says `parked` (3 attempts exhausted), stop monitoring this task.
|
|
54
51
|
- **Task-level error (not a platform error):** The workflow's own error handling applies. Stop monitoring this run.
|
|
55
52
|
- **If status is `running` or `paused`:** Continue polling.
|
|
56
|
-
- **
|
|
53
|
+
- **Tick end:** At ~100s elapsed, stop monitoring and exit. Do NOT try to outlive the 120s timeout — the monitor's job is not to prevent launcher death (impossible) but to recover from it fast. Anything that dies after you leave is caught by the next tick's Step 0 via the mapping, at most ~3 minutes later. That is the designed recovery path, not a fallback.
|
|
57
54
|
- When all launched workflows are terminal or retry-exhausted, exit silently.
|
|
58
55
|
|
|
59
56
|
6. Exit silently.
|