@rubytech/taskmaster 1.9.0 → 1.9.1

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.9.0",
3
- "commit": "6ec3baf15f49752d39bae9589b10d00ba492cc2a",
4
- "builtAt": "2026-02-26T20:00:11.544Z"
2
+ "version": "1.9.1",
3
+ "commit": "0b919997a5bc45f672df09785fe6c44edd03fdc0",
4
+ "builtAt": "2026-02-27T06:34:25.149Z"
5
5
  }
@@ -23,6 +23,17 @@ export function stop(state) {
23
23
  stopTimer(state);
24
24
  }
25
25
  export async function status(state) {
26
+ // When the store is already loaded, read without the lock. This avoids
27
+ // deadlock when an agent running inside a cron job calls cron.status —
28
+ // the lock is held by executeJob for the entire agent turn.
29
+ if (state.store) {
30
+ return {
31
+ enabled: state.deps.cronEnabled,
32
+ storePath: state.deps.storePath,
33
+ jobs: state.store.jobs.length,
34
+ nextWakeAtMs: state.deps.cronEnabled === true ? (nextWakeAtMs(state) ?? null) : null,
35
+ };
36
+ }
26
37
  return await locked(state, async () => {
27
38
  await ensureLoaded(state);
28
39
  return {
@@ -34,6 +45,14 @@ export async function status(state) {
34
45
  });
35
46
  }
36
47
  export async function list(state, opts) {
48
+ // When the store is already loaded, read without the lock. This avoids
49
+ // deadlock when an agent running inside a cron job calls cron.list —
50
+ // the lock is held by executeJob for the entire agent turn.
51
+ if (state.store) {
52
+ const includeDisabled = opts?.includeDisabled === true;
53
+ const jobs = state.store.jobs.filter((j) => includeDisabled || j.enabled);
54
+ return jobs.sort((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
55
+ }
37
56
  return await locked(state, async () => {
38
57
  await ensureLoaded(state);
39
58
  const includeDisabled = opts?.includeDisabled === true;
@@ -99,19 +118,27 @@ export async function remove(state, id) {
99
118
  });
100
119
  }
101
120
  export async function run(state, id, mode) {
102
- return await locked(state, async () => {
121
+ // Resolve the job under the lock, then release before executing.
122
+ // executeJob runs a full agent turn whose tools may call back into the
123
+ // cron service. Holding the lock during execution causes a deadlock.
124
+ const resolved = await locked(state, async () => {
103
125
  warnIfDisabled(state, "run");
104
126
  await ensureLoaded(state);
105
127
  const job = findJobOrThrow(state, id);
106
128
  const now = state.deps.nowMs();
107
129
  const due = isJobDue(job, now, { forced: mode === "force" });
108
130
  if (!due)
109
- return { ok: true, ran: false, reason: "not-due" };
110
- await executeJob(state, job, now, { forced: mode === "force" });
131
+ return { job: null, now: 0, notDue: true };
132
+ return { job, now, notDue: false };
133
+ });
134
+ if (resolved.notDue)
135
+ return { ok: true, ran: false, reason: "not-due" };
136
+ await executeJob(state, resolved.job, resolved.now, { forced: mode === "force" });
137
+ await locked(state, async () => {
111
138
  await persist(state);
112
139
  armTimer(state);
113
- return { ok: true, ran: true };
114
140
  });
141
+ return { ok: true, ran: true };
115
142
  }
116
143
  export function wakeNow(state, opts) {
117
144
  return wake(state, opts);
@@ -26,9 +26,15 @@ export async function onTimer(state) {
26
26
  return;
27
27
  state.running = true;
28
28
  try {
29
+ // Load store under the lock, then release before executing jobs.
30
+ // executeJob runs a full agent turn whose tools may call back into the
31
+ // cron service (e.g. cron.list). Holding the lock during execution
32
+ // causes a deadlock because the inner cron call waits for the same lock.
29
33
  await locked(state, async () => {
30
34
  await ensureLoaded(state);
31
- await runDueJobs(state);
35
+ });
36
+ await runDueJobs(state);
37
+ await locked(state, async () => {
32
38
  await persist(state);
33
39
  armTimer(state);
34
40
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"