@sema-agent/core 2.10.0 → 2.12.0

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.
@@ -276,7 +276,7 @@ export function checkNoChange(oldString, newString) {
276
276
  return undefined;
277
277
  }
278
278
  export function checkStale(entry, currentHash) {
279
- if (entry.hash !== currentHash) {
279
+ if (entry === undefined || entry.hash !== currentHash) {
280
280
  return { code: "stale", message: "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it." };
281
281
  }
282
282
  return undefined;
@@ -83,16 +83,13 @@ function expandCronField(field, { min, max }) {
83
83
  return out.size > 0 ? [...out].sort((a, b) => a - b) : null;
84
84
  }
85
85
  export function cronScheduleError(expr) {
86
- const parts = expr.trim().split(/\s+/);
87
- let std = parts;
88
- if (parts.length === 6) {
89
- if (!expandCronField(parts[0] ?? "", { min: 0, max: 59 })) {
90
- return `invalid seconds field "${parts[0]}" — allowed 0-59 (syntax: N, N-M, */N, comma lists).`;
91
- }
92
- std = parts.slice(1);
86
+ const std = expr.trim().split(/\s+/);
87
+ if (std.length === 6) {
88
+ return (`its leading seconds field is not supported — the resident scheduler fires on 5-field cron, so one ` +
89
+ `minute is the finest granularity available. Pass "${std.slice(1).join(" ")}" if that cadence works.`);
93
90
  }
94
- else if (parts.length !== 5) {
95
- return `expected 5 fields (minute hour day-of-month month day-of-week) or 6 with leading seconds, got ${parts.length}.`;
91
+ if (std.length !== 5) {
92
+ return `expected 5 fields (minute hour day-of-month month day-of-week), got ${std.length}.`;
96
93
  }
97
94
  const expanded = [];
98
95
  for (let i = 0; i < 5; i++) {
@@ -117,9 +114,12 @@ export function cronScheduleError(expr) {
117
114
  return null;
118
115
  }
119
116
  const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
117
+ function cronExprFromSummary(when) {
118
+ const trimmed = when.trim();
119
+ return trimmed.startsWith("cron ") ? trimmed.slice("cron ".length).trim() : trimmed;
120
+ }
120
121
  export function cronToHuman(expr) {
121
- const parts = expr.trim().split(/\s+/);
122
- const std = parts.length === 6 ? parts.slice(1) : parts;
122
+ const std = cronExprFromSummary(expr).split(/\s+/);
123
123
  if (std.length !== 5)
124
124
  return expr;
125
125
  const [minute = "", hour = "", dom = "", month = "", dow = ""] = std;
@@ -191,7 +191,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
191
191
  parameters: Type.Object({
192
192
  prompt: Type.String({ description: "Self-contained instructions for the future task (it runs unattended)." }),
193
193
  schedule: Type.Optional(Type.Union([
194
- Type.Object({ kind: Type.Literal("cron"), expr: Type.String({ description: "5- or 6-field cron expression." }) }),
194
+ Type.Object({
195
+ kind: Type.Literal("cron"),
196
+ expr: Type.String({ description: "5-field cron expression (minute hour day-of-month month day-of-week)." }),
197
+ }),
195
198
  Type.Object({ kind: Type.Literal("at"), atMs: Type.Number({ description: "Absolute epoch ms to fire once." }) }),
196
199
  Type.Object({ kind: Type.Literal("delay"), delaySec: Type.Number({ description: "Seconds from now to fire once." }) }),
197
200
  ], { description: 'When to fire. Pass exactly one of `schedule` or `cron`. Defaults to `durable: true`.' })),
@@ -246,22 +249,32 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
246
249
  ...(!durable ? { lifetime: "session" } : {}),
247
250
  ...(a.recurring !== undefined ? { recurring: a.recurring } : {}),
248
251
  };
252
+ const before = await sched.list(schedCtx);
253
+ const knownIds = before.ok ? new Set(before.value.map((s) => s.id)) : undefined;
249
254
  const r = await sched.schedule(intent, schedCtx);
250
255
  if (!r.ok)
251
256
  return errorResult(`Error (CronCreate): ${r.error.message}`);
257
+ const replaced = knownIds?.has(r.value.id);
252
258
  const humanSchedule = when.kind === "cron"
253
259
  ? cronToHuman(when.expr)
254
260
  : when.kind === "delay"
255
261
  ? `once in ${when.delaySec}s`
256
262
  : `once at ${new Date(when.atMs).toISOString()}`;
263
+ const named = `${r.value.id}${a.label ? ` (${a.label})` : ""}`;
264
+ const content = replaced === true
265
+ ? `Updated scheduled task ${named} — it replaced an existing job with the same schedule and label, whose prompt is now gone.`
266
+ : replaced === false
267
+ ? `Scheduled task ${named}.`
268
+ : `Scheduled (or updated) task ${named} — the scheduler's listing was unavailable, so whether this replaced an existing job with the same schedule and label is unknown.`;
257
269
  return {
258
- content: `Scheduled task ${r.value.id}${a.label ? ` (${a.label})` : ""}.`,
270
+ content,
259
271
  details: {
260
272
  type: "cron-create",
261
273
  id: r.value.id,
262
274
  humanSchedule,
263
275
  recurring: when.kind === "cron" && a.recurring !== false,
264
276
  durable,
277
+ ...(replaced !== undefined ? { replaced } : {}),
265
278
  },
266
279
  };
267
280
  },
@@ -298,17 +311,18 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
298
311
  }
299
312
  const content = r.value
300
313
  .map((s) => {
314
+ const shown = cronExprFromSummary(s.when);
301
315
  const human = cronToHuman(s.when);
302
316
  const tier = s.lifetime === "session" ? " [session]" : "";
303
317
  const once = s.recurring === false ? " [once]" : "";
304
- return `- ${s.id}: ${s.when}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
318
+ return `- ${s.id}: ${shown}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
305
319
  })
306
320
  .join("\n");
307
321
  const jobs = r.value.map((s) => {
308
322
  const human = cronToHuman(s.when);
309
323
  return {
310
324
  id: s.id,
311
- cron: s.when,
325
+ cron: cronExprFromSummary(s.when),
312
326
  humanSchedule: human !== s.when ? human : s.when,
313
327
  ...(s.label !== undefined ? { label: s.label } : {}),
314
328
  ...(s.recurring !== undefined ? { recurring: s.recurring } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",