muse-crew 0.7.5 → 0.7.7
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 +11 -0
- package/lib/crew-api.js +45 -13
- package/package.json +1 -1
- package/seed/cron-body-template.md +7 -9
- package/seed/crons.json +1 -1
package/API.md
CHANGED
|
@@ -293,6 +293,11 @@ Returns `{ ok: true, runs: [...], events: [...] }`.
|
|
|
293
293
|
|
|
294
294
|
The platform records workflow run status in `runtime.workflow_runs` — separate from our telemetry. When a workflow dies on a fatal `agent()` failure (e.g. "subagent bootstrap is no longer authorized"), our telemetry shows a run with no events, but the platform knows why. This monitor correlates platform failures with our tasks.
|
|
295
295
|
|
|
296
|
+
Two layers, in order of preference:
|
|
297
|
+
|
|
298
|
+
1. **Prevention — the launcher stays alive.** Async workflow `agent()` authorization is tied to the launcher's lifetime: if the cron tick ends while a workflow is still running, the workflow's next `agent()` call fails. So the poll tick runs with a 90-minute execution timeout (`timeout_secs: 5400` in `seed/crons.json`) and its Step 5 monitor stays alive until every launched run reaches a terminal state. A launcher that outlives its runs never triggers the failure in the first place.
|
|
299
|
+
2. **Recovery — per-tick correlation through the durable mapping.** If the launcher dies early anyway (platform kill, cell recycle), the next tick's Step 0 finds the dead run, `record-platform-failure` correlates it via the `platform_run_tasks` mapping, and `retry-platform-failure` requeues the task — at most ~3 minutes later, with the ghost session settled so the dispatcher treats it as a retry candidate immediately.
|
|
300
|
+
|
|
296
301
|
### `record-platform-failure`
|
|
297
302
|
|
|
298
303
|
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.
|
|
@@ -324,6 +329,12 @@ Returns `{ ok: true, failures: [...] }`.
|
|
|
324
329
|
|
|
325
330
|
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
331
|
|
|
332
|
+
**Ghost-session settlement:** the dead workflow's `agent_sessions` row still says `running`, and the dispatcher skips any task whose latest session is running ("work in flight"). The retry settles it atomically with the state change, so the task is redispatchable on the very next tick instead of waiting out the 1-hour zombie-session sweep:
|
|
333
|
+
- on `requeued`: running sessions → `stalled` (the dispatcher already treats `stalled` as a retry candidate);
|
|
334
|
+
- on `parked`: running sessions → `failed` (what `recover-task` accepts), same as `park-task`.
|
|
335
|
+
|
|
336
|
+
Returns `{ ok: true, action: "requeued"|"parked"|"skipped", ..., settled_sessions: <n> }`.
|
|
337
|
+
|
|
327
338
|
**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
339
|
|
|
329
340
|
| Field | Type | Notes |
|
package/lib/crew-api.js
CHANGED
|
@@ -847,14 +847,32 @@ commands["retry-platform-failure"] = (db, args) => {
|
|
|
847
847
|
if (task && task.state !== "parked") {
|
|
848
848
|
const msg = "Platform workflow failed " + (failure.retry_count + 1) + "x: " +
|
|
849
849
|
failure.error_message.substring(0, 200);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
850
|
+
const ts = new Date().toISOString();
|
|
851
|
+
db.exec("BEGIN");
|
|
852
|
+
try {
|
|
853
|
+
db.prepare("UPDATE tasks SET state = 'parked', updated_at = datetime('now') WHERE id = ?")
|
|
854
|
+
.run(failure.task_id);
|
|
855
|
+
db.prepare("DELETE FROM dispatch_reservations WHERE task_id = ?").run(failure.task_id);
|
|
856
|
+
// Settle the ghost session: a parked task never keeps a running
|
|
857
|
+
// session, and 'failed' is what recover-task accepts.
|
|
858
|
+
const settled = db.prepare(
|
|
859
|
+
`UPDATE agent_sessions
|
|
860
|
+
SET status = 'failed', ended_at = ?,
|
|
861
|
+
notes = COALESCE(notes, '') || ' Parked: platform retry exhausted.'
|
|
862
|
+
WHERE task_id = ? AND status = 'running'`
|
|
863
|
+
).run(ts, failure.task_id);
|
|
864
|
+
db.prepare(
|
|
865
|
+
`INSERT INTO events (id, type, task_id, identity, message, timestamp)
|
|
866
|
+
VALUES (?, 'note', ?, NULL, ?, ?)`
|
|
867
|
+
).run(uuid(), failure.task_id, "Parked: " + msg, ts);
|
|
868
|
+
db.exec("COMMIT");
|
|
869
|
+
return { ok: true, action: "parked", reason: "max retries exceeded", retry_count: failure.retry_count, settled_sessions: Number(settled.changes) };
|
|
870
|
+
} catch (e) {
|
|
871
|
+
db.exec("ROLLBACK");
|
|
872
|
+
throw e;
|
|
873
|
+
}
|
|
856
874
|
}
|
|
857
|
-
return { ok: true, action: "parked", reason: "max retries exceeded", retry_count: failure.retry_count };
|
|
875
|
+
return { ok: true, action: "parked", reason: "max retries exceeded", retry_count: failure.retry_count, settled_sessions: 0 };
|
|
858
876
|
}
|
|
859
877
|
// Clear reservation and re-queue for retry.
|
|
860
878
|
db.exec("BEGIN");
|
|
@@ -863,22 +881,36 @@ commands["retry-platform-failure"] = (db, args) => {
|
|
|
863
881
|
db.prepare(
|
|
864
882
|
"UPDATE tasks SET state = 'todo', updated_at = datetime('now') WHERE id = ? AND state != 'done'"
|
|
865
883
|
).run(failure.task_id);
|
|
884
|
+
// Settle the ghost session atomically with the requeue: the platform
|
|
885
|
+
// workflow is dead, but its agent_sessions row still says 'running' —
|
|
886
|
+
// and the dispatcher skips any task whose latest session is running
|
|
887
|
+
// (crew-dispatch.js "work in flight"). Without this, the requeued task
|
|
888
|
+
// would sit until the 1-hour zombie-session sweep. 'stalled' is what
|
|
889
|
+
// the dispatcher already treats as a retry candidate.
|
|
890
|
+
const ts = new Date().toISOString();
|
|
891
|
+
const settled = db.prepare(
|
|
892
|
+
`UPDATE agent_sessions
|
|
893
|
+
SET status = 'stalled', ended_at = ?,
|
|
894
|
+
notes = COALESCE(notes, '') || ' Platform retry: launcher died; session settled for redispatch.'
|
|
895
|
+
WHERE task_id = ? AND status = 'running'`
|
|
896
|
+
).run(ts, failure.task_id);
|
|
866
897
|
db.prepare(
|
|
867
898
|
`UPDATE platform_run_failures
|
|
868
899
|
SET retry_count = retry_count + 1, last_retry_at = datetime('now')
|
|
869
900
|
WHERE platform_run_id = ?`
|
|
870
901
|
).run(args.platform_run_id);
|
|
871
902
|
db.exec("COMMIT");
|
|
903
|
+
return {
|
|
904
|
+
ok: true,
|
|
905
|
+
action: "requeued",
|
|
906
|
+
task_id: failure.task_id,
|
|
907
|
+
retry_count: failure.retry_count + 1,
|
|
908
|
+
settled_sessions: Number(settled.changes),
|
|
909
|
+
};
|
|
872
910
|
} catch (e) {
|
|
873
911
|
db.exec("ROLLBACK");
|
|
874
912
|
throw e;
|
|
875
913
|
}
|
|
876
|
-
return {
|
|
877
|
-
ok: true,
|
|
878
|
-
action: "requeued",
|
|
879
|
-
task_id: failure.task_id,
|
|
880
|
-
retry_count: failure.retry_count + 1,
|
|
881
|
-
};
|
|
882
914
|
};
|
|
883
915
|
|
|
884
916
|
commands["park-task"] = (db, args) => {
|
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,22 @@ 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 2 minutes via muse.db:
|
|
39
|
+
5. **Monitor launched workflows until terminal (stay-alive — 2026-09-13):** The platform ties async workflow `agent()` authorization to the launcher's lifetime: if THIS tick ends while a workflow is still running, the workflow's next `agent()` call fails with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". Prevention beats recovery here, so this tick is configured with a 90-minute execution timeout (`timeout_secs: 5400` in seed/crons.json) and you MUST stay alive until every launched run reaches a terminal state. Do not exit early while a launched run is still `running` — your death is what kills it.
|
|
40
|
+
- For each launched run_id, poll its status every ~2 minutes via muse.db:
|
|
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 retries remain (max 3 per task):
|
|
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` (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
|
+
- **Monitor ceiling:** 90 minutes from tick start. If a run is still not terminal then (pathological — the work phase caps each `agent()` call at 60 minutes), exit; the next tick's Step 0 continues recovery through the durable mapping.
|
|
54
|
+
- **Backstop (not the plan, the insurance):** If THIS launcher dies early for any reason (platform kill, cell recycle), the next tick's Step 0 detects the dead run through the durable platform run -> task mapping and retries it — at most ~3 minutes later. The mapping exists so a dead launcher never strands a task for an hour; the monitor exists so the launcher rarely dies mid-run in the first place.
|
|
57
55
|
- When all launched workflows are terminal or retry-exhausted, exit silently.
|
|
58
56
|
|
|
59
57
|
6. Exit silently.
|