loopengine 0.1.93 → 0.1.94
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/README.md +6 -1
- package/dist/core/scheduler.d.ts +17 -1
- package/dist/core/scheduler.js +100 -19
- package/dist/core/system-agents/templates.d.ts +3 -3
- package/dist/core/system-agents/templates.js +6 -14
- package/dist/core/system-tools/schedule.js +16 -6
- package/dist/core/time-zone.d.ts +40 -0
- package/dist/core/time-zone.js +138 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/web/agents-config-page.js +29 -9
- package/dist/web/global-config-page.js +65 -0
- package/dist/web/global-config.d.ts +24 -0
- package/dist/web/global-config.js +30 -0
- package/dist/web/templates-admin.d.ts +1 -0
- package/dist/web/templates-admin.js +3 -2
- package/dist/web/templates-page.js +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -253,7 +253,12 @@ The server runs one scheduler (`core/scheduler.ts`) that sends a message
|
|
|
253
253
|
to an agent later — once (`run_at`) or on an interval (`every: "1h"`),
|
|
254
254
|
optionally from a set first run (`start_at`), which every later run keeps:
|
|
255
255
|
`every: "1d"` from a 9:00 start runs daily at 9:00, `every: "7d"` from a
|
|
256
|
-
Monday runs on Mondays
|
|
256
|
+
Monday runs on Mondays. `at: "09:00"` (plus `weekday: "monday"`) picks
|
|
257
|
+
that start for you. Times of day are in the workspace's time zone, chosen
|
|
258
|
+
in the console's **Settings** (`LOOPENGINE_TIME_ZONE`; the server's own
|
|
259
|
+
when unset): a timestamp without an offset is read there, and daily and
|
|
260
|
+
weekly tasks keep their time of day when the clocks change. Schedule
|
|
261
|
+
from the console's **Schedules** tab, the `GET/POST
|
|
257
262
|
/agents/<name>/schedules` API, or the agent itself: every agent has
|
|
258
263
|
built-in `system_schedule_task`, `system_list_scheduled_tasks`,
|
|
259
264
|
`system_check_scheduled_task` and `system_cancel_scheduled_task` tools,
|
package/dist/core/scheduler.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/** `time_zone` (repeating in whole days only): the zone whose calendar
|
|
2
|
+
* days it steps through, so its time of day survives daylight saving.
|
|
3
|
+
* Tasks made before it existed step in exact 24-hour days. */
|
|
1
4
|
export type Schedule = {
|
|
2
5
|
run_at: string;
|
|
3
6
|
} | {
|
|
4
7
|
every: string;
|
|
5
8
|
anchor?: string;
|
|
9
|
+
time_zone?: string;
|
|
6
10
|
};
|
|
7
11
|
export type ScheduledTaskStatus = 'scheduled' | 'running' | 'awaiting_approval' | 'paused' | 'done' | 'cancelled';
|
|
8
12
|
export interface ScheduledTaskRun {
|
|
@@ -77,11 +81,16 @@ export interface CreateScheduledTaskInput {
|
|
|
77
81
|
* time of day / day of week every later run keeps, e.g. every "1d"
|
|
78
82
|
* from "2026-11-02T09:00:00+08:00" runs daily at 9:00. A start in the
|
|
79
83
|
* past is fine: the first run is its next slot after now. Without it,
|
|
80
|
-
* the first run is one interval from now.
|
|
84
|
+
* the first run is one interval from now. `at` ("09:00", with `every`
|
|
85
|
+
* only) instead starts it at the next 9:00 in the workspace's time
|
|
86
|
+
* zone, and `weekday` ("monday", or 0 for Sunday to 6) at the next one
|
|
87
|
+
* on that day. */
|
|
81
88
|
schedule: {
|
|
82
89
|
run_at?: unknown;
|
|
83
90
|
every?: unknown;
|
|
84
91
|
start_at?: unknown;
|
|
92
|
+
at?: unknown;
|
|
93
|
+
weekday?: unknown;
|
|
85
94
|
};
|
|
86
95
|
task_name?: string;
|
|
87
96
|
notify?: boolean;
|
|
@@ -101,6 +110,13 @@ export declare function resumeScheduledTask(taskId: string, nowMs?: number): Pro
|
|
|
101
110
|
* finished one-off task runs once more. */
|
|
102
111
|
export declare function runScheduledTaskNow(taskId: string, nowMs?: number): Promise<ScheduledTask>;
|
|
103
112
|
export declare function deleteScheduledTask(taskId: string): Promise<void>;
|
|
113
|
+
/** Moves every daily and weekly (whole-day) task to `toTimeZone`,
|
|
114
|
+
* keeping its time of day: a 9:00 task made while the workspace was on
|
|
115
|
+
* UTC runs at 9:00 in the new zone. A task from before tasks kept their
|
|
116
|
+
* zone is taken to be in `fromTimeZone`. One-off and shorter repeating
|
|
117
|
+
* tasks are left alone: they're a moment, not a time of day. Returns
|
|
118
|
+
* how many moved. */
|
|
119
|
+
export declare function moveScheduledTasksToTimeZone(fromTimeZone: string, toTimeZone: string, nowMs?: number): Promise<number>;
|
|
104
120
|
/** When the next task is due — for a host that stops idle servers and
|
|
105
121
|
* needs to wake this one in time (GET /internal/activity). */
|
|
106
122
|
export declare function nextScheduledWakeAt(): Promise<string | undefined>;
|
package/dist/core/scheduler.js
CHANGED
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
// `every` is anchored: an hourly task created at 10:07 keeps firing at
|
|
13
13
|
// :07, however long each run takes. `start_at` sets the anchor instead,
|
|
14
14
|
// for "daily at 9:00" ("1d" from a 9:00 start) or "Mondays at 9:00" ("7d"
|
|
15
|
-
// from a Monday)
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// from a Monday), or `at` ("09:00", plus a `weekday` for "7d") works the
|
|
16
|
+
// start out in the workspace's time zone (core/time-zone.ts). Intervals
|
|
17
|
+
// in days are calendar days in the time zone the task was made in, so a
|
|
18
|
+
// daily 9:00 task stays at 9:00 when the clocks change; shorter ones are
|
|
19
|
+
// exact durations. Timestamps without an offset are read in the
|
|
20
|
+
// workspace's time zone too. A run missed while the server was down fires
|
|
18
21
|
// once when it's back, then the schedule moves to the next future slot
|
|
19
22
|
// (no burst of catch-up runs).
|
|
20
23
|
import { randomUUID } from 'node:crypto';
|
|
21
24
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
25
|
import path from 'node:path';
|
|
23
26
|
import { KeyedMutex } from './session-store.js';
|
|
27
|
+
import { addCalendarDays, instantOf, nextTimeOfDay, parseTimeOfDay, parseTimestamp, wallTimeAt, workspaceTimeZone } from './time-zone.js';
|
|
24
28
|
const HISTORY_LIMIT = 20;
|
|
25
29
|
const EVERY_PATTERN = /^(\d+)\s*(s|m|h|d)$/;
|
|
26
30
|
const EVERY_UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
@@ -37,11 +41,38 @@ export function parseEvery(every) {
|
|
|
37
41
|
throw new ScheduleError(`"${every}" is too short — the shortest interval is 30s`);
|
|
38
42
|
return ms;
|
|
39
43
|
}
|
|
40
|
-
/**
|
|
41
|
-
function
|
|
44
|
+
/** Whole days in an interval ("2d" → 2), or undefined for one in s/m/h. */
|
|
45
|
+
function everyDays(every) {
|
|
46
|
+
const match = EVERY_PATTERN.exec(every.trim());
|
|
47
|
+
return match && match[2] === 'd' ? Number(match[1]) : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The first anchored slot strictly after `nowMs`: exact steps of
|
|
50
|
+
* `intervalMs`, or with `days` and `timeZone`, steps of that many
|
|
51
|
+
* calendar days there. */
|
|
52
|
+
function nextAnchoredSlot(anchorMs, intervalMs, nowMs, calendar) {
|
|
42
53
|
if (nowMs < anchorMs)
|
|
43
54
|
return anchorMs;
|
|
44
|
-
|
|
55
|
+
let k = Math.floor((nowMs - anchorMs) / intervalMs) + 1;
|
|
56
|
+
if (!calendar)
|
|
57
|
+
return anchorMs + k * intervalMs;
|
|
58
|
+
// A day is 23 to 25 hours, so the estimate can be one step off.
|
|
59
|
+
const slot = (n) => addCalendarDays(anchorMs, n * calendar.days, calendar.timeZone);
|
|
60
|
+
while (k > 1 && slot(k - 1) > nowMs)
|
|
61
|
+
k--;
|
|
62
|
+
while (slot(k) <= nowMs)
|
|
63
|
+
k++;
|
|
64
|
+
return slot(k);
|
|
65
|
+
}
|
|
66
|
+
const WEEKDAY_NAMES = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
67
|
+
function parseWeekday(value) {
|
|
68
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 6)
|
|
69
|
+
return value;
|
|
70
|
+
if (typeof value === 'string') {
|
|
71
|
+
const i = WEEKDAY_NAMES.findIndex((d) => d.startsWith(value.trim().toLowerCase()) && value.trim().length >= 3);
|
|
72
|
+
if (i >= 0)
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
throw new ScheduleError(`weekday "${String(value)}" isn't a day: use monday to sunday, or 0 (Sunday) to 6`);
|
|
45
76
|
}
|
|
46
77
|
/** Human-readable schedule, for lists and messages. */
|
|
47
78
|
export function describeSchedule(schedule) {
|
|
@@ -114,34 +145,53 @@ export async function createScheduledTask(input, nowMs = Date.now()) {
|
|
|
114
145
|
throw new ScheduleError('message is required');
|
|
115
146
|
let schedule;
|
|
116
147
|
let nextRunMs;
|
|
148
|
+
const timeZone = workspaceTimeZone();
|
|
117
149
|
if (typeof input.schedule?.run_at === 'string' && input.schedule.run_at) {
|
|
118
|
-
const runAtMs =
|
|
150
|
+
const runAtMs = parseTimestamp(input.schedule.run_at, timeZone);
|
|
119
151
|
if (Number.isNaN(runAtMs))
|
|
120
152
|
throw new ScheduleError(`"${input.schedule.run_at}" is not a valid ISO 8601 timestamp`);
|
|
121
153
|
schedule = { run_at: new Date(runAtMs).toISOString() };
|
|
122
154
|
nextRunMs = runAtMs;
|
|
123
155
|
}
|
|
124
156
|
else if (typeof input.schedule?.every === 'string' && input.schedule.every) {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
const every = input.schedule.every.trim();
|
|
158
|
+
const intervalMs = parseEvery(every);
|
|
159
|
+
const days = everyDays(every);
|
|
160
|
+
const calendar = days ? { days, timeZone } : undefined;
|
|
161
|
+
const { start_at: startAt, at, weekday } = input.schedule;
|
|
162
|
+
const given = (v) => v !== undefined && v !== null && v !== '';
|
|
163
|
+
let startMs;
|
|
164
|
+
if (given(at)) {
|
|
165
|
+
if (given(startAt))
|
|
166
|
+
throw new ScheduleError('give either start_at or at, not both');
|
|
167
|
+
const time = typeof at === 'string' ? parseTimeOfDay(at) : undefined;
|
|
168
|
+
if (!time)
|
|
169
|
+
throw new ScheduleError(`at "${String(at)}" isn't a time of day: use 24-hour "HH:MM", e.g. "09:00"`);
|
|
170
|
+
startMs = nextTimeOfDay(time, nowMs, timeZone, given(weekday) ? parseWeekday(weekday) : undefined);
|
|
171
|
+
}
|
|
172
|
+
else if (given(weekday)) {
|
|
173
|
+
throw new ScheduleError('weekday goes with at, e.g. { "every": "7d", "at": "09:00", "weekday": "monday" }');
|
|
174
|
+
}
|
|
175
|
+
else if (given(startAt)) {
|
|
176
|
+
startMs = typeof startAt === 'string' ? parseTimestamp(startAt, timeZone) : Number.NaN;
|
|
129
177
|
if (Number.isNaN(startMs))
|
|
130
178
|
throw new ScheduleError(`start_at "${String(startAt)}" is not a valid ISO 8601 timestamp`);
|
|
179
|
+
}
|
|
180
|
+
if (startMs !== undefined) {
|
|
131
181
|
// The anchor is the start itself, so every run lands on its time of
|
|
132
182
|
// day (or weekday, for "7d").
|
|
133
|
-
nextRunMs = nextAnchoredSlot(startMs, intervalMs, nowMs);
|
|
134
|
-
schedule = { every
|
|
183
|
+
nextRunMs = nextAnchoredSlot(startMs, intervalMs, nowMs, calendar);
|
|
184
|
+
schedule = { every, anchor: new Date(startMs).toISOString(), ...(calendar ? { time_zone: timeZone } : {}) };
|
|
135
185
|
}
|
|
136
186
|
else {
|
|
137
|
-
nextRunMs = nowMs + intervalMs;
|
|
138
|
-
schedule = { every
|
|
187
|
+
nextRunMs = calendar ? addCalendarDays(nowMs, calendar.days, timeZone) : nowMs + intervalMs;
|
|
188
|
+
schedule = { every, anchor: new Date(nextRunMs).toISOString(), ...(calendar ? { time_zone: timeZone } : {}) };
|
|
139
189
|
}
|
|
140
190
|
}
|
|
141
191
|
else {
|
|
142
|
-
if (input.schedule?.start_at)
|
|
143
|
-
throw new ScheduleError('start_at
|
|
144
|
-
throw new ScheduleError('schedule must be { "run_at": "<ISO timestamp>" } or { "every": "<interval, e.g. 1h>", "start_at"?: "<ISO timestamp>" }');
|
|
192
|
+
if (input.schedule?.start_at || input.schedule?.at)
|
|
193
|
+
throw new ScheduleError('start_at and at go with "every" (a repeating task); a one-off task uses run_at');
|
|
194
|
+
throw new ScheduleError('schedule must be { "run_at": "<ISO timestamp>" } or { "every": "<interval, e.g. 1h>", "start_at"?: "<ISO timestamp>" } or { "every": "1d", "at": "09:00" }');
|
|
145
195
|
}
|
|
146
196
|
const task = {
|
|
147
197
|
task_id: randomUUID(),
|
|
@@ -197,6 +247,36 @@ export async function runScheduledTaskNow(taskId, nowMs = Date.now()) {
|
|
|
197
247
|
export async function deleteScheduledTask(taskId) {
|
|
198
248
|
await getSchedulerStore().remove(taskId);
|
|
199
249
|
}
|
|
250
|
+
/** Moves every daily and weekly (whole-day) task to `toTimeZone`,
|
|
251
|
+
* keeping its time of day: a 9:00 task made while the workspace was on
|
|
252
|
+
* UTC runs at 9:00 in the new zone. A task from before tasks kept their
|
|
253
|
+
* zone is taken to be in `fromTimeZone`. One-off and shorter repeating
|
|
254
|
+
* tasks are left alone: they're a moment, not a time of day. Returns
|
|
255
|
+
* how many moved. */
|
|
256
|
+
export async function moveScheduledTasksToTimeZone(fromTimeZone, toTimeZone, nowMs = Date.now()) {
|
|
257
|
+
let moved = 0;
|
|
258
|
+
for (const listed of await getSchedulerStore().list()) {
|
|
259
|
+
if (listed.status === 'done' || listed.status === 'cancelled' || !('every' in listed.schedule) || !everyDays(listed.schedule.every))
|
|
260
|
+
continue;
|
|
261
|
+
await getSchedulerStore().update(listed.task_id, (t) => {
|
|
262
|
+
if (!t || !('every' in t.schedule) || !t.schedule.anchor)
|
|
263
|
+
return t;
|
|
264
|
+
const days = everyDays(t.schedule.every);
|
|
265
|
+
const from = t.schedule.time_zone ?? fromTimeZone;
|
|
266
|
+
if (!days || from === toTimeZone)
|
|
267
|
+
return t;
|
|
268
|
+
const anchorMs = instantOf(wallTimeAt(Date.parse(t.schedule.anchor), from), toTimeZone);
|
|
269
|
+
const schedule = { ...t.schedule, anchor: new Date(anchorMs).toISOString(), time_zone: toTimeZone };
|
|
270
|
+
moved++;
|
|
271
|
+
// A run that's due or under way keeps its slot; the next one moves.
|
|
272
|
+
if (t.status !== 'scheduled' || !t.next_run_at || Date.parse(t.next_run_at) <= nowMs)
|
|
273
|
+
return { ...t, schedule };
|
|
274
|
+
const next = nextAnchoredSlot(anchorMs, parseEvery(t.schedule.every), nowMs, { days, timeZone: toTimeZone });
|
|
275
|
+
return { ...t, schedule, next_run_at: new Date(next).toISOString() };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return moved;
|
|
279
|
+
}
|
|
200
280
|
/** When the next task is due — for a host that stops idle servers and
|
|
201
281
|
* needs to wake this one in time (GET /internal/activity). */
|
|
202
282
|
export async function nextScheduledWakeAt() {
|
|
@@ -208,7 +288,8 @@ function nextRunAfter(task, nowMs) {
|
|
|
208
288
|
return undefined;
|
|
209
289
|
const intervalMs = parseEvery(task.schedule.every);
|
|
210
290
|
const anchorMs = task.schedule.anchor ? Date.parse(task.schedule.anchor) : Date.parse(task.created_at) + intervalMs;
|
|
211
|
-
|
|
291
|
+
const days = everyDays(task.schedule.every);
|
|
292
|
+
return nextAnchoredSlot(anchorMs, intervalMs, nowMs, days && task.schedule.time_zone ? { days, timeZone: task.schedule.time_zone } : undefined);
|
|
212
293
|
}
|
|
213
294
|
/** Moves the old lp-task-scheduler's task files (`<dir>/.tasks/*.json`)
|
|
214
295
|
* into this store once, renaming each to `.json.migrated`. Returns how
|
|
@@ -13,7 +13,7 @@ export interface TemplateSkill {
|
|
|
13
13
|
export interface TemplateSchedule {
|
|
14
14
|
/** How often, in the scheduler's terms: "10m", "1h", "7d". */
|
|
15
15
|
every: string;
|
|
16
|
-
/** Optional first run, as a time of day in the
|
|
16
|
+
/** Optional first run, as a time of day in the workspace's time zone
|
|
17
17
|
* ("09:00"), and for weekly schedules a weekday (1 = Monday). Without
|
|
18
18
|
* it, the first run is one interval after the schedule is turned on. */
|
|
19
19
|
at?: string;
|
|
@@ -49,5 +49,5 @@ export interface StarterTemplate {
|
|
|
49
49
|
export declare const STARTER_TEMPLATES: readonly StarterTemplate[];
|
|
50
50
|
export declare function starterTemplate(id: string): StarterTemplate | undefined;
|
|
51
51
|
/** The first run of a template's schedule: its time of day (and weekday)
|
|
52
|
-
* next, in the
|
|
53
|
-
export declare function templateScheduleStart(schedule: TemplateSchedule, nowMs?: number): string | undefined;
|
|
52
|
+
* next, in the workspace's time zone; undefined when it has none. */
|
|
53
|
+
export declare function templateScheduleStart(schedule: TemplateSchedule, nowMs?: number, timeZone?: string): string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nextTimeOfDay, parseTimeOfDay, workspaceTimeZone } from '../time-zone.js';
|
|
1
2
|
const GMAIL_KEYS = ['GMAIL_CLIENT_ID', 'GMAIL_CLIENT_SECRET', 'GMAIL_REFRESH_TOKEN'];
|
|
2
3
|
const SHOPIFY_KEYS = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_ADMIN_TOKEN'];
|
|
3
4
|
const GITHUB_KEYS = ['GITHUB_TOKEN', 'GITHUB_DEFAULT_REPO'];
|
|
@@ -378,19 +379,10 @@ export function starterTemplate(id) {
|
|
|
378
379
|
return STARTER_TEMPLATES.find((t) => t.id === id);
|
|
379
380
|
}
|
|
380
381
|
/** The first run of a template's schedule: its time of day (and weekday)
|
|
381
|
-
* next, in the
|
|
382
|
-
export function templateScheduleStart(schedule, nowMs = Date.now()) {
|
|
383
|
-
|
|
382
|
+
* next, in the workspace's time zone; undefined when it has none. */
|
|
383
|
+
export function templateScheduleStart(schedule, nowMs = Date.now(), timeZone = workspaceTimeZone()) {
|
|
384
|
+
const at = schedule.at ? parseTimeOfDay(schedule.at) : undefined;
|
|
385
|
+
if (!at)
|
|
384
386
|
return undefined;
|
|
385
|
-
|
|
386
|
-
const start = new Date(nowMs);
|
|
387
|
-
start.setHours(hours, minutes, 0, 0);
|
|
388
|
-
if (schedule.weekday !== undefined) {
|
|
389
|
-
const ahead = (schedule.weekday - start.getDay() + 7) % 7;
|
|
390
|
-
start.setDate(start.getDate() + ahead);
|
|
391
|
-
}
|
|
392
|
-
const step = schedule.weekday !== undefined ? 7 : 1;
|
|
393
|
-
while (start.getTime() <= nowMs)
|
|
394
|
-
start.setDate(start.getDate() + step);
|
|
395
|
-
return start.toISOString();
|
|
387
|
+
return new Date(nextTimeOfDay(at, nowMs, timeZone, schedule.weekday)).toISOString();
|
|
396
388
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cancelScheduledTask, createScheduledTask, getScheduledTask, listScheduledTasks, } from '../scheduler.js';
|
|
2
|
+
import { formatWallTime, workspaceTimeZone } from '../time-zone.js';
|
|
2
3
|
/** How many active (not done or cancelled) tasks one agent may create
|
|
3
4
|
* through these tools — each run is a model call, so a runaway loop of
|
|
4
5
|
* "schedule another one" stops here. The console and API aren't capped. */
|
|
@@ -6,7 +7,12 @@ function maxActivePerAgent() {
|
|
|
6
7
|
const n = Number(process.env.LOOPENGINE_SCHEDULER_MAX_PER_AGENT);
|
|
7
8
|
return Number.isInteger(n) && n > 0 ? n : 25;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
/** next_run_at as the workspace's wall-clock time, so the model can
|
|
11
|
+
* confirm "tomorrow at 9:00" without converting from UTC. */
|
|
12
|
+
function nextRunLocal(t, timeZone) {
|
|
13
|
+
return t.next_run_at ? `${formatWallTime(Date.parse(t.next_run_at), timeZone)} (${timeZone})` : undefined;
|
|
14
|
+
}
|
|
15
|
+
function summary(t, timeZone) {
|
|
10
16
|
return {
|
|
11
17
|
task_id: t.task_id,
|
|
12
18
|
task_name: t.task_name,
|
|
@@ -14,11 +20,15 @@ function summary(t) {
|
|
|
14
20
|
schedule: t.schedule,
|
|
15
21
|
status: t.status,
|
|
16
22
|
next_run_at: t.next_run_at,
|
|
23
|
+
next_run_local: nextRunLocal(t, timeZone),
|
|
17
24
|
run_count: t.run_count,
|
|
18
25
|
last_status: t.last_status,
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
export function createScheduleTools(agentName) {
|
|
29
|
+
// Read once per run: the time zone is in the tool description, which
|
|
30
|
+
// should stay the same from call to call.
|
|
31
|
+
const timeZone = workspaceTimeZone();
|
|
22
32
|
async function ownTask(taskId, tool) {
|
|
23
33
|
const task = await getScheduledTask(taskId);
|
|
24
34
|
// Another agent's task reads exactly like a missing one.
|
|
@@ -28,7 +38,7 @@ export function createScheduleTools(agentName) {
|
|
|
28
38
|
}
|
|
29
39
|
const scheduleTask = {
|
|
30
40
|
name: 'system_schedule_task',
|
|
31
|
-
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job. When it fires you receive the message as a new conversation (none of the current one carries over, so make the message self-contained) and handle it with your tools as usual, with no person live. A tool call that needs approval goes wherever your approval requests are sent and waits; if nowhere is set, it is denied. Use only when the user asks for something to happen later or regularly, never on your own initiative.
|
|
41
|
+
description: 'Schedule a message to yourself for later — once at a set time, or repeatedly on an interval — like setting a reminder or a cron job. When it fires you receive the message as a new conversation (none of the current one carries over, so make the message self-contained) and handle it with your tools as usual, with no person live. A tool call that needs approval goes wherever your approval requests are sent and waits; if nowhere is set, it is denied. Use only when the user asks for something to happen later or regularly, never on your own initiative. Times are in this workspace\'s time zone, ' + timeZone + ', unless you give an offset, so when the user means another time zone, give its offset. Prefer the longest interval that serves them since each run is a model call, and confirm the schedule back in plain words. Returns task_id, next_run_at and next_run_local (the workspace\'s time).',
|
|
32
42
|
input_schema: {
|
|
33
43
|
type: 'object',
|
|
34
44
|
properties: {
|
|
@@ -38,8 +48,8 @@ export function createScheduleTools(agentName) {
|
|
|
38
48
|
},
|
|
39
49
|
schedule: {
|
|
40
50
|
type: 'object',
|
|
41
|
-
description: 'Either { "run_at": "<ISO 8601 timestamp
|
|
42
|
-
properties: { run_at: { type: 'string' }, every: { type: 'string' }, start_at: { type: 'string' } },
|
|
51
|
+
description: 'Either { "run_at": "<ISO 8601 timestamp>" } for once, or { "every": "<interval>", ... } to repeat until cancelled — a number plus s/m/h/d, e.g. "30m", "1h", "1d"; 30s at the shortest. A timestamp without an offset, like "2026-11-02T09:00", is in the workspace\'s time zone (' + timeZone + '). A repeating task keeps its first slot: started at 10:07 with "1h", it fires at 11:07, 12:07, ... For a time of day or day of week, add "at" (24-hour "HH:MM") and for weekly "weekday": "every day at 9am" is { "every": "1d", "at": "09:00" }; "every Monday at 9am" is { "every": "7d", "at": "09:00", "weekday": "monday" }. Daily and weekly tasks keep their time of day when the clocks change. Or give "start_at", a timestamp, for the first run.',
|
|
52
|
+
properties: { run_at: { type: 'string' }, every: { type: 'string' }, at: { type: 'string' }, weekday: { type: 'string' }, start_at: { type: 'string' } },
|
|
43
53
|
},
|
|
44
54
|
task_name: { type: 'string', description: 'Optional short label, shown when listing tasks and in the console.' },
|
|
45
55
|
notify: { type: 'boolean', description: 'Optional — also send each run\'s result through your notifier (where your approval requests go), so a person sees it.' },
|
|
@@ -58,7 +68,7 @@ export function createScheduleTools(agentName) {
|
|
|
58
68
|
task_name: typeof input.task_name === 'string' ? input.task_name : undefined,
|
|
59
69
|
notify: input.notify === true,
|
|
60
70
|
});
|
|
61
|
-
return JSON.stringify({ task_id: task.task_id, next_run_at: task.next_run_at, schedule: task.schedule });
|
|
71
|
+
return JSON.stringify({ task_id: task.task_id, next_run_at: task.next_run_at, next_run_local: nextRunLocal(task, timeZone), schedule: task.schedule });
|
|
62
72
|
},
|
|
63
73
|
};
|
|
64
74
|
const listTasks = {
|
|
@@ -76,7 +86,7 @@ export function createScheduleTools(agentName) {
|
|
|
76
86
|
},
|
|
77
87
|
execute: async (input) => {
|
|
78
88
|
const status = typeof input.status === 'string' ? input.status : undefined;
|
|
79
|
-
return JSON.stringify((await listScheduledTasks({ agent: agentName, status })).map(summary));
|
|
89
|
+
return JSON.stringify((await listScheduledTasks({ agent: agentName, status })).map((t) => summary(t, timeZone)));
|
|
80
90
|
},
|
|
81
91
|
safe: true,
|
|
82
92
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const TIME_ZONE_ENV = "LOOPENGINE_TIME_ZONE";
|
|
2
|
+
export interface WallTime {
|
|
3
|
+
year: number;
|
|
4
|
+
/** 1–12 */
|
|
5
|
+
month: number;
|
|
6
|
+
day: number;
|
|
7
|
+
hour: number;
|
|
8
|
+
minute: number;
|
|
9
|
+
second: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function isValidTimeZone(timeZone: unknown): timeZone is string;
|
|
12
|
+
/** The server's own time zone. */
|
|
13
|
+
export declare function serverTimeZone(): string;
|
|
14
|
+
/** The workspace's time zone: LOOPENGINE_TIME_ZONE when it's a real one,
|
|
15
|
+
* else the server's. */
|
|
16
|
+
export declare function workspaceTimeZone(): string;
|
|
17
|
+
/** Every time zone this server knows, for the console's picker. */
|
|
18
|
+
export declare function listTimeZones(): string[];
|
|
19
|
+
/** The wall-clock time (and weekday, 0 = Sunday) at `ms` in `timeZone`. */
|
|
20
|
+
export declare function wallTimeAt(ms: number, timeZone: string): WallTime & {
|
|
21
|
+
weekday: number;
|
|
22
|
+
};
|
|
23
|
+
/** The instant a wall-clock time in `timeZone` happens. Fields may
|
|
24
|
+
* overflow (day 32, hour 25), as with Date.UTC. */
|
|
25
|
+
export declare function instantOf(t: WallTime, timeZone: string): number;
|
|
26
|
+
/** An ISO 8601 timestamp as ms. One with a "Z" or an offset is exact;
|
|
27
|
+
* one without ("2026-11-02T09:00") is a wall-clock time in `timeZone`.
|
|
28
|
+
* NaN when it isn't a timestamp. */
|
|
29
|
+
export declare function parseTimestamp(value: string, timeZone: string): number;
|
|
30
|
+
/** "09:00" as [hours, minutes], or undefined when it isn't a time. */
|
|
31
|
+
export declare function parseTimeOfDay(value: string): [number, number] | undefined;
|
|
32
|
+
/** The next time it's `at` ("09:00") in `timeZone`, strictly after
|
|
33
|
+
* `nowMs`, on `weekday` (0 = Sunday) when given. */
|
|
34
|
+
export declare function nextTimeOfDay(at: [number, number], nowMs: number, timeZone: string, weekday?: number): number;
|
|
35
|
+
/** `ms` moved by whole calendar days in `timeZone`, keeping its time of
|
|
36
|
+
* day across a daylight-saving change. */
|
|
37
|
+
export declare function addCalendarDays(ms: number, days: number, timeZone: string): number;
|
|
38
|
+
/** `ms` as "2026-11-02 09:00" in `timeZone`, for tool results the model
|
|
39
|
+
* reads back to a person. */
|
|
40
|
+
export declare function formatWallTime(ms: number, timeZone: string): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The workspace's time zone: what "9:00" means in a schedule, a
|
|
2
|
+
// template's start time and the console's date fields. Set from the
|
|
3
|
+
// console's Settings (LOOPENGINE_TIME_ZONE, an IANA name such as
|
|
4
|
+
// "America/New_York"); unset, it's the server's own, which on a hosted
|
|
5
|
+
// workspace is UTC.
|
|
6
|
+
//
|
|
7
|
+
// Times are worked out with Intl alone, no time zone data of our own:
|
|
8
|
+
// a wall-clock time is turned into an instant by guessing its offset
|
|
9
|
+
// and correcting it twice, which settles on the right answer either side
|
|
10
|
+
// of a daylight-saving change. A time the clocks skip (02:30 on the
|
|
11
|
+
// morning they spring forward) lands an hour later; a time that happens
|
|
12
|
+
// twice (the hour they fall back) is the first of the two.
|
|
13
|
+
export const TIME_ZONE_ENV = 'LOOPENGINE_TIME_ZONE';
|
|
14
|
+
const formatters = new Map();
|
|
15
|
+
function formatter(timeZone) {
|
|
16
|
+
let f = formatters.get(timeZone);
|
|
17
|
+
if (!f) {
|
|
18
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
19
|
+
timeZone,
|
|
20
|
+
hourCycle: 'h23',
|
|
21
|
+
year: 'numeric',
|
|
22
|
+
month: 'numeric',
|
|
23
|
+
day: 'numeric',
|
|
24
|
+
hour: 'numeric',
|
|
25
|
+
minute: 'numeric',
|
|
26
|
+
second: 'numeric',
|
|
27
|
+
weekday: 'short',
|
|
28
|
+
});
|
|
29
|
+
formatters.set(timeZone, f);
|
|
30
|
+
}
|
|
31
|
+
return f;
|
|
32
|
+
}
|
|
33
|
+
export function isValidTimeZone(timeZone) {
|
|
34
|
+
if (typeof timeZone !== 'string' || !timeZone.trim())
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
new Intl.DateTimeFormat('en-US', { timeZone });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The server's own time zone. */
|
|
45
|
+
export function serverTimeZone() {
|
|
46
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
47
|
+
}
|
|
48
|
+
/** The workspace's time zone: LOOPENGINE_TIME_ZONE when it's a real one,
|
|
49
|
+
* else the server's. */
|
|
50
|
+
export function workspaceTimeZone() {
|
|
51
|
+
const configured = process.env[TIME_ZONE_ENV]?.trim();
|
|
52
|
+
return configured && isValidTimeZone(configured) ? configured : serverTimeZone();
|
|
53
|
+
}
|
|
54
|
+
/** Every time zone this server knows, for the console's picker. */
|
|
55
|
+
export function listTimeZones() {
|
|
56
|
+
const supported = Intl.supportedValuesOf?.('timeZone') ?? [];
|
|
57
|
+
return supported.includes('UTC') ? supported : ['UTC', ...supported];
|
|
58
|
+
}
|
|
59
|
+
const WEEKDAY_INDEX = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
60
|
+
/** The wall-clock time (and weekday, 0 = Sunday) at `ms` in `timeZone`. */
|
|
61
|
+
export function wallTimeAt(ms, timeZone) {
|
|
62
|
+
const parts = {};
|
|
63
|
+
for (const p of formatter(timeZone).formatToParts(new Date(ms)))
|
|
64
|
+
parts[p.type] = p.value;
|
|
65
|
+
return {
|
|
66
|
+
year: Number(parts.year),
|
|
67
|
+
month: Number(parts.month),
|
|
68
|
+
day: Number(parts.day),
|
|
69
|
+
hour: Number(parts.hour),
|
|
70
|
+
minute: Number(parts.minute),
|
|
71
|
+
second: Number(parts.second),
|
|
72
|
+
weekday: WEEKDAY_INDEX[parts.weekday] ?? 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function asUtcMs(t) {
|
|
76
|
+
return Date.UTC(t.year, t.month - 1, t.day, t.hour, t.minute, t.second);
|
|
77
|
+
}
|
|
78
|
+
/** How far ahead of UTC `timeZone` is at `ms`, in ms. */
|
|
79
|
+
function offsetAt(ms, timeZone) {
|
|
80
|
+
return asUtcMs(wallTimeAt(Math.floor(ms / 1000) * 1000, timeZone)) - Math.floor(ms / 1000) * 1000;
|
|
81
|
+
}
|
|
82
|
+
/** The instant a wall-clock time in `timeZone` happens. Fields may
|
|
83
|
+
* overflow (day 32, hour 25), as with Date.UTC. */
|
|
84
|
+
export function instantOf(t, timeZone) {
|
|
85
|
+
const naive = asUtcMs(t);
|
|
86
|
+
const first = naive - offsetAt(naive, timeZone);
|
|
87
|
+
const second = naive - offsetAt(first, timeZone);
|
|
88
|
+
// Either side of a change the two guesses differ; the earlier one is
|
|
89
|
+
// right for a repeated hour, the later one for a skipped hour.
|
|
90
|
+
if (first === second)
|
|
91
|
+
return first;
|
|
92
|
+
return offsetAt(Math.min(first, second), timeZone) === naive - Math.min(first, second) ? Math.min(first, second) : Math.max(first, second);
|
|
93
|
+
}
|
|
94
|
+
const LOCAL_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?$/;
|
|
95
|
+
/** An ISO 8601 timestamp as ms. One with a "Z" or an offset is exact;
|
|
96
|
+
* one without ("2026-11-02T09:00") is a wall-clock time in `timeZone`.
|
|
97
|
+
* NaN when it isn't a timestamp. */
|
|
98
|
+
export function parseTimestamp(value, timeZone) {
|
|
99
|
+
const local = LOCAL_TIMESTAMP.exec(value.trim());
|
|
100
|
+
if (!local)
|
|
101
|
+
return Date.parse(value);
|
|
102
|
+
const [, y, mo, d, h, mi, s] = local;
|
|
103
|
+
const t = { year: Number(y), month: Number(mo), day: Number(d), hour: Number(h), minute: Number(mi), second: Number(s ?? 0) };
|
|
104
|
+
if (t.month < 1 || t.month > 12 || t.day < 1 || t.day > 31 || t.hour > 23 || t.minute > 59 || t.second > 59)
|
|
105
|
+
return Number.NaN;
|
|
106
|
+
return instantOf(t, timeZone);
|
|
107
|
+
}
|
|
108
|
+
const TIME_OF_DAY = /^([01]?\d|2[0-3]):([0-5]\d)$/;
|
|
109
|
+
/** "09:00" as [hours, minutes], or undefined when it isn't a time. */
|
|
110
|
+
export function parseTimeOfDay(value) {
|
|
111
|
+
const match = TIME_OF_DAY.exec(value.trim());
|
|
112
|
+
return match ? [Number(match[1]), Number(match[2])] : undefined;
|
|
113
|
+
}
|
|
114
|
+
/** The next time it's `at` ("09:00") in `timeZone`, strictly after
|
|
115
|
+
* `nowMs`, on `weekday` (0 = Sunday) when given. */
|
|
116
|
+
export function nextTimeOfDay(at, nowMs, timeZone, weekday) {
|
|
117
|
+
const today = wallTimeAt(nowMs, timeZone);
|
|
118
|
+
const ahead = weekday === undefined ? 0 : (weekday - today.weekday + 7) % 7;
|
|
119
|
+
const step = weekday === undefined ? 1 : 7;
|
|
120
|
+
for (let days = ahead;; days += step) {
|
|
121
|
+
const ms = instantOf({ year: today.year, month: today.month, day: today.day + days, hour: at[0], minute: at[1], second: 0 }, timeZone);
|
|
122
|
+
if (ms > nowMs)
|
|
123
|
+
return ms;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** `ms` moved by whole calendar days in `timeZone`, keeping its time of
|
|
127
|
+
* day across a daylight-saving change. */
|
|
128
|
+
export function addCalendarDays(ms, days, timeZone) {
|
|
129
|
+
const t = wallTimeAt(ms, timeZone);
|
|
130
|
+
return instantOf({ ...t, day: t.day + days }, timeZone);
|
|
131
|
+
}
|
|
132
|
+
/** `ms` as "2026-11-02 09:00" in `timeZone`, for tool results the model
|
|
133
|
+
* reads back to a person. */
|
|
134
|
+
export function formatWallTime(ms, timeZone) {
|
|
135
|
+
const t = wallTimeAt(ms, timeZone);
|
|
136
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
137
|
+
return `${t.year}-${pad(t.month)}-${pad(t.day)} ${pad(t.hour)}:${pad(t.minute)}`;
|
|
138
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { setUsageSink, recordUsage, type UsageEvent, type UsageSink } from '#cor
|
|
|
10
10
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS, type NotifierFile, type MessagingApprover } from '#core/notifier-file.js';
|
|
11
11
|
export { listDeliveries, recordDelivery, type NotifierDelivery } from '#core/notifier-deliveries.js';
|
|
12
12
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
13
|
-
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
13
|
+
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule, type ScheduledTask, type ScheduledTaskRun, type ScheduledTaskStatus, type Schedule, type SchedulerStore, type FireTask, type CreateScheduledTaskInput } from '#core/scheduler.js';
|
|
14
14
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
15
15
|
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval, type MessagingDeps, type MessagingDecision, type DecisionOutcome } from '#core/messaging/engine.js';
|
|
16
16
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
@@ -88,10 +88,11 @@ export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities,
|
|
|
88
88
|
export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult } from './bin/ability-manager.js';
|
|
89
89
|
export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
|
|
90
90
|
export type { PublicAbilitySearchResult } from './web/abilities-admin.js';
|
|
91
|
-
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError } from './web/global-config.js';
|
|
91
|
+
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError, describeTimeZone, setWorkspaceTimeZone, TimeZoneError } from './web/global-config.js';
|
|
92
92
|
export { loopengineVersion } from './core/version.js';
|
|
93
93
|
export { describeUpgrade, runUpgrade, UpgradeError } from './web/upgrade-admin.js';
|
|
94
|
-
export type { ModelProviderInfo, AgentModelUsage, ModelsView, GatewayProviderInfo, GatewaysView } from './web/global-config.js';
|
|
94
|
+
export type { ModelProviderInfo, AgentModelUsage, ModelsView, GatewayProviderInfo, GatewaysView, TimeZoneView } from './web/global-config.js';
|
|
95
|
+
export { workspaceTimeZone, serverTimeZone, isValidTimeZone, TIME_ZONE_ENV } from '#core/time-zone.js';
|
|
95
96
|
export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
|
|
96
97
|
export { scaffoldAgent, scaffoldSubagent, AgentNameError, AgentExistsError, AgentNotFoundError, AgentModelError } from './bin/cli.js';
|
|
97
98
|
export type { AgentTemplateOptions } from './bin/cli.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { setUsageSink, recordUsage } from '#core/usage.js';
|
|
|
12
12
|
export { readNotifierFile, writeNotifierFile, removeNotifierFile, validateNotifierFile, notifierConfigFromFile, NotifierFileError, NOTIFIER_SECRET_VARS } from '#core/notifier-file.js';
|
|
13
13
|
export { listDeliveries, recordDelivery } from '#core/notifier-deliveries.js';
|
|
14
14
|
export { describeNotifier } from '#core/http-notifier.js';
|
|
15
|
-
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
15
|
+
export { Scheduler, ScheduleError, FileSchedulerStore, getSchedulerStore, setSchedulerStore, createScheduledTask, listScheduledTasks, getScheduledTask, cancelScheduledTask, pauseScheduledTask, resumeScheduledTask, runScheduledTaskNow, deleteScheduledTask, nextScheduledWakeAt, moveScheduledTasksToTimeZone, importLegacyScheduledTasks, parseEvery, describeSchedule } from '#core/scheduler.js';
|
|
16
16
|
export { NOTIFIER_SLACK_SIGNING_SECRET_VAR } from '#core/notifier-file.js';
|
|
17
17
|
export { firstSighting, handleInboundMessage, handleMessagingAction, settleThreadChoice, isPlainApproval } from '#core/messaging/engine.js';
|
|
18
18
|
export { formatArgs, formatChange, labelFor } from '#core/messaging/format-args.js';
|
|
@@ -78,8 +78,9 @@ export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, setAgentEnvVar, unsetAgent
|
|
|
78
78
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
|
79
79
|
export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
|
|
80
80
|
export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
|
|
81
|
-
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError } from './web/global-config.js';
|
|
81
|
+
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError, describeTimeZone, setWorkspaceTimeZone, TimeZoneError } from './web/global-config.js';
|
|
82
82
|
export { loopengineVersion } from './core/version.js';
|
|
83
83
|
export { describeUpgrade, runUpgrade, UpgradeError } from './web/upgrade-admin.js';
|
|
84
|
+
export { workspaceTimeZone, serverTimeZone, isValidTimeZone, TIME_ZONE_ENV } from '#core/time-zone.js';
|
|
84
85
|
export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
|
|
85
86
|
export { scaffoldAgent, scaffoldSubagent, AgentNameError, AgentExistsError, AgentNotFoundError, AgentModelError } from './bin/cli.js';
|
|
@@ -1484,10 +1484,22 @@ ${consoleShellScript}
|
|
|
1484
1484
|
scheduled: 'scheduled', running: 'running', awaiting_approval: 'waiting for approval', paused: 'paused', done: 'done', cancelled: 'cancelled'
|
|
1485
1485
|
};
|
|
1486
1486
|
|
|
1487
|
+
// The workspace's time zone, from GET /agents/:name/schedules (an
|
|
1488
|
+
// older project adapter doesn't send it: then the browser's own).
|
|
1489
|
+
var schedulesTimeZone = null;
|
|
1490
|
+
|
|
1491
|
+
function fmtScheduleTime(iso) {
|
|
1492
|
+
var d = new Date(iso);
|
|
1493
|
+
if (schedulesTimeZone) {
|
|
1494
|
+
try { return d.toLocaleString(undefined, { timeZone: schedulesTimeZone, timeZoneName: 'short' }); } catch (e) {}
|
|
1495
|
+
}
|
|
1496
|
+
return d.toLocaleString();
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1487
1499
|
function scheduleText(s) {
|
|
1488
1500
|
// A repeating task's anchor is its first run, so it says when the
|
|
1489
1501
|
// later ones fall (e.g. every 1d from 9:00).
|
|
1490
|
-
return s.run_at ? 'Once, ' +
|
|
1502
|
+
return s.run_at ? 'Once, ' + escapeHtml(fmtScheduleTime(s.run_at)) : 'Every ' + escapeHtml(s.every) + (s.anchor ? '<div class="hint">from ' + escapeHtml(fmtScheduleTime(s.anchor)) + '</div>' : '');
|
|
1491
1503
|
}
|
|
1492
1504
|
|
|
1493
1505
|
function renderScheduleRow(t) {
|
|
@@ -1499,17 +1511,17 @@ ${consoleShellScript}
|
|
|
1499
1511
|
if (t.status === 'cancelled' || t.status === 'done') actions.push(['delete', 'Delete']);
|
|
1500
1512
|
var last = t.last_run_at
|
|
1501
1513
|
? '<span class="le-pill ' + (t.last_status === 'ok' ? 'good' : 'bad') + '">' + escapeHtml(t.last_status) + '</span> ' +
|
|
1502
|
-
'<span class="hint">' + escapeHtml(
|
|
1514
|
+
'<span class="hint">' + escapeHtml(fmtScheduleTime(t.last_run_at)) + '</span>' +
|
|
1503
1515
|
'<div class="schedule-result">' + escapeHtml(t.last_status === 'ok' ? (t.last_result || '') : (t.last_error || '')) + '</div>'
|
|
1504
1516
|
: '<span class="hint">not run yet</span>';
|
|
1505
1517
|
var history = (t.history || []).slice().reverse().map(function (h) {
|
|
1506
|
-
return '<li><span class="le-pill ' + (h.status === 'ok' ? 'good' : 'bad') + '">' + escapeHtml(h.status) + '</span> <span class="hint">' + escapeHtml(
|
|
1518
|
+
return '<li><span class="le-pill ' + (h.status === 'ok' ? 'good' : 'bad') + '">' + escapeHtml(h.status) + '</span> <span class="hint">' + escapeHtml(fmtScheduleTime(h.ran_at)) + '</span>' +
|
|
1507
1519
|
'<div class="schedule-result">' + escapeHtml(h.status === 'ok' ? (h.result || '') : (h.error || '')) + '</div></li>';
|
|
1508
1520
|
}).join('');
|
|
1509
1521
|
return '<tr>' +
|
|
1510
1522
|
'<td style="min-width:180px"><strong>' + escapeHtml(t.task_name || 'Untitled') + '</strong>' + (t.notify ? ' <span class="le-pill accent" title="Each result goes through this agent's notifier">notifies</span>' : '') +
|
|
1511
1523
|
'<div class="schedule-result hint">' + escapeHtml(t.message) + '</div></td>' +
|
|
1512
|
-
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(
|
|
1524
|
+
'<td>' + scheduleText(t.schedule) + (t.next_run_at ? '<div class="hint">next ' + escapeHtml(fmtScheduleTime(t.next_run_at)) + '</div>' : '') + '</td>' +
|
|
1513
1525
|
'<td><span class="le-pill ' + (SCHEDULE_PILLS[t.status] || 'neutral') + '">' + escapeHtml(SCHEDULE_LABELS[t.status] || t.status) + '</span>' +
|
|
1514
1526
|
(t.status === 'awaiting_approval' ? '<div class="hint"><a href="#" class="schedule-to-approvals">Decide in Messaging</a></div>' : '') + '</td>' +
|
|
1515
1527
|
'<td style="min-width:200px">' + last + (history ? '<details><summary class="hint">History (' + (t.history || []).length + ')</summary><ul class="schedule-history">' + history + '</ul></details>' : '') + '</td>' +
|
|
@@ -1533,7 +1545,8 @@ ${consoleShellScript}
|
|
|
1533
1545
|
'<input type="number" name="everyN" value="1" min="1" aria-label="How many">' +
|
|
1534
1546
|
'<select name="everyUnit" aria-label="Unit"><option value="m">minutes</option><option value="h" selected>hours</option><option value="d">days</option><option value="w">weeks</option></select>' +
|
|
1535
1547
|
'<label for="everyStart">starting</label><input type="datetime-local" name="everyStart" id="everyStart" aria-label="First run"></div>' +
|
|
1536
|
-
'<p class="hint" style="margin:0 0 4px 22px">Optional. The first run, which sets the time of day (and the weekday, for weeks) every later run keeps, e.g. every 1 day starting tomorrow 09:00. Without it, the first run is one interval from now
|
|
1548
|
+
'<p class="hint" style="margin:0 0 4px 22px">Optional. The first run, which sets the time of day (and the weekday, for weeks) every later run keeps, e.g. every 1 day starting tomorrow 09:00. Without it, the first run is one interval from now.' +
|
|
1549
|
+
(schedulesTimeZone ? ' Times are in ' + escapeHtml(schedulesTimeZone.replace(/_/g, ' ')) + ' (<a href="/config?section=settings">change</a>).' : '') + '</p>' +
|
|
1537
1550
|
'<div class="schedule-when"><input type="radio" name="when" value="once" id="whenOnce"><label for="whenOnce">Once at</label>' +
|
|
1538
1551
|
'<input type="datetime-local" name="runAt" aria-label="Date and time"></div>' +
|
|
1539
1552
|
'</div>' +
|
|
@@ -1544,6 +1557,13 @@ ${consoleShellScript}
|
|
|
1544
1557
|
'</form></section>';
|
|
1545
1558
|
}
|
|
1546
1559
|
|
|
1560
|
+
// A date field's value ("2026-11-02T09:00") goes as it is, for the
|
|
1561
|
+
// server to read in the workspace's time zone; with no workspace time
|
|
1562
|
+
// zone known, it's the browser's.
|
|
1563
|
+
function scheduleFormTime(value) {
|
|
1564
|
+
return schedulesTimeZone ? value : new Date(value).toISOString();
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1547
1567
|
function wireSchedulesHandlers(name) {
|
|
1548
1568
|
var content = schedulesContentEl();
|
|
1549
1569
|
if (!content) return;
|
|
@@ -1576,14 +1596,13 @@ ${consoleShellScript}
|
|
|
1576
1596
|
var schedule;
|
|
1577
1597
|
if (data.get('when') === 'once') {
|
|
1578
1598
|
if (!data.get('runAt')) { result.textContent = 'Pick a date and time.'; return; }
|
|
1579
|
-
schedule = { run_at:
|
|
1599
|
+
schedule = { run_at: scheduleFormTime(data.get('runAt')) };
|
|
1580
1600
|
} else {
|
|
1581
|
-
// Weeks go to the scheduler as days
|
|
1582
|
-
// browser's own time zone.
|
|
1601
|
+
// Weeks go to the scheduler as days.
|
|
1583
1602
|
var unit = data.get('everyUnit');
|
|
1584
1603
|
var n = Number(data.get('everyN')) || 1;
|
|
1585
1604
|
schedule = { every: unit === 'w' ? String(n * 7) + 'd' : String(n) + unit };
|
|
1586
|
-
if (data.get('everyStart')) schedule.start_at =
|
|
1605
|
+
if (data.get('everyStart')) schedule.start_at = scheduleFormTime(data.get('everyStart'));
|
|
1587
1606
|
}
|
|
1588
1607
|
result.textContent = 'Scheduling' + String.fromCharCode(8230);
|
|
1589
1608
|
fetchJsonRetryOnce('/agents/' + encodeURIComponent(name) + '/schedules', {
|
|
@@ -1604,6 +1623,7 @@ ${consoleShellScript}
|
|
|
1604
1623
|
if (currentName !== name) return;
|
|
1605
1624
|
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
1606
1625
|
var tasks = r.body.tasks || [];
|
|
1626
|
+
schedulesTimeZone = r.body.time_zone || null;
|
|
1607
1627
|
content.innerHTML = renderSchedulesHtml(tasks);
|
|
1608
1628
|
wireSchedulesHandlers(name);
|
|
1609
1629
|
leShell.setCount('schedules', tasks.filter(function (t) { return t.status !== 'cancelled' && t.status !== 'done'; }).length);
|
|
@@ -298,14 +298,78 @@ ${consoleShellScript}
|
|
|
298
298
|
['Environment name', '<code>' + escapeHtml(d.environment) + '</code> <span class="hint">(LOOPENGINE_ENV)</span>'],
|
|
299
299
|
['Public address', d.publicUrl ? '<code>' + escapeHtml(d.publicUrl) + '</code>' : '<span class="hint">Not set (LOOPENGINE_PUBLIC_URL); links in notifications use the address saved with each notifier</span>'],
|
|
300
300
|
['Scheduled tasks', d.scheduler === 'off' ? 'Off on this server (LOOPENGINE_SCHEDULER=off)' : 'Run by this server'],
|
|
301
|
+
['Time zone', d.timeZone ? '<code>' + escapeHtml(d.timeZone) + '</code>' : '<span class="hint">The server' + String.fromCharCode(8217) + 's own. Update the template below to choose one here.</span>'],
|
|
301
302
|
['Theme', 'Follows your system'],
|
|
302
303
|
];
|
|
303
304
|
return '<section><table><thead><tr><th>Setting</th><th>Value</th></tr></thead><tbody>' +
|
|
304
305
|
rows.map(function (r) { return '<tr><td>' + r[0] + '</td><td>' + r[1] + '</td></tr>'; }).join('') +
|
|
305
306
|
'</tbody></table></section>' +
|
|
307
|
+
(d.timeZone ? '<section><h3>Time zone</h3><div class="source" id="timeZoneBox"><p class="hint">Loading' + String.fromCharCode(8230) + '</p></div></section>' : '') +
|
|
306
308
|
'<section><h3>Updates</h3><div class="source" id="upgradeBox"><p class="hint">Checking for a newer LoopEngine' + String.fromCharCode(8230) + '</p></div></section>';
|
|
307
309
|
}
|
|
308
310
|
|
|
311
|
+
// ---- Time zone ----
|
|
312
|
+
|
|
313
|
+
function renderTimeZone(t) {
|
|
314
|
+
var box = document.getElementById('timeZoneBox');
|
|
315
|
+
if (!box) return;
|
|
316
|
+
var intro = '<p>Schedules and templates use this for times of day: “daily at 09:00” runs at 9:00 here, and daily and weekly schedules keep that time when the clocks change. ' +
|
|
317
|
+
(t.chosen ? '' : 'Not chosen yet, so it' + String.fromCharCode(8217) + 's the server' + String.fromCharCode(8217) + 's (<code>' + escapeHtml(t.server) + '</code>).') + '</p>';
|
|
318
|
+
if (!t.canSave) {
|
|
319
|
+
box.innerHTML = intro + '<p>In use: <code>' + escapeHtml(t.timeZone) + '</code></p><p class="hint">Set LOOPENGINE_ADMIN_AUTH to change it here, or put <code>LOOPENGINE_TIME_ZONE</code> in .env.</p>';
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
var browserZone = '';
|
|
323
|
+
try { browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) {}
|
|
324
|
+
var options = t.zones.map(function (z) {
|
|
325
|
+
return '<option value="' + escapeHtml(z) + '"' + (z === t.timeZone ? ' selected' : '') + '>' + escapeHtml(z.replace(/_/g, ' ')) + '</option>';
|
|
326
|
+
}).join('');
|
|
327
|
+
box.innerHTML = intro +
|
|
328
|
+
'<form id="timeZoneForm" style="display:flex;flex-direction:column;gap:10px;max-width:480px">' +
|
|
329
|
+
'<label for="timeZoneSelect">Time zone</label>' +
|
|
330
|
+
'<select id="timeZoneSelect" name="timeZone">' + options + '</select>' +
|
|
331
|
+
(browserZone && browserZone !== t.timeZone && t.zones.indexOf(browserZone) >= 0
|
|
332
|
+
? '<p class="hint" style="margin:0"><a href="#" id="timeZoneUseBrowser">Use this browser' + String.fromCharCode(8217) + 's, ' + escapeHtml(browserZone) + '</a></p>' : '') +
|
|
333
|
+
'<label style="display:flex;gap:8px;align-items:center"><input type="checkbox" name="moveSchedules" checked> Move existing daily and weekly schedules too, keeping their time of day</label>' +
|
|
334
|
+
'<div><button type="submit" class="primary">Save</button></div>' +
|
|
335
|
+
'<div id="timeZoneMsg" class="hint"></div>' +
|
|
336
|
+
'</form>';
|
|
337
|
+
var form = document.getElementById('timeZoneForm');
|
|
338
|
+
var useBrowser = document.getElementById('timeZoneUseBrowser');
|
|
339
|
+
if (useBrowser) useBrowser.addEventListener('click', function (ev) { ev.preventDefault(); form.timeZone.value = browserZone; });
|
|
340
|
+
form.addEventListener('submit', function (ev) {
|
|
341
|
+
ev.preventDefault();
|
|
342
|
+
var msg = document.getElementById('timeZoneMsg');
|
|
343
|
+
var btn = form.querySelector('button[type="submit"]');
|
|
344
|
+
btn.disabled = true;
|
|
345
|
+
msg.textContent = 'Saving' + String.fromCharCode(8230);
|
|
346
|
+
fetch('/config/time-zone', {
|
|
347
|
+
method: 'PUT',
|
|
348
|
+
headers: { 'content-type': 'application/json' },
|
|
349
|
+
body: JSON.stringify({ timeZone: form.timeZone.value, moveSchedules: form.moveSchedules.checked }),
|
|
350
|
+
})
|
|
351
|
+
.then(function (r) { return r.json().then(function (b) { return { ok: r.ok, body: b }; }); })
|
|
352
|
+
.then(function (r) {
|
|
353
|
+
btn.disabled = false;
|
|
354
|
+
if (!r.ok) { msg.innerHTML = '<span class="error">' + escapeHtml(r.body.error || 'Could not save') + '</span>'; return; }
|
|
355
|
+
msg.textContent = 'Saved: ' + r.body.timeZone.replace(/_/g, ' ') + '.' +
|
|
356
|
+
(r.body.moved ? ' Moved ' + r.body.moved + ' schedule' + (r.body.moved === 1 ? '' : 's') + '.' : '');
|
|
357
|
+
})
|
|
358
|
+
.catch(function (err) { btn.disabled = false; msg.innerHTML = '<span class="error">' + escapeHtml(err.message) + '</span>'; });
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function loadTimeZone() {
|
|
363
|
+
if (!document.getElementById('timeZoneBox')) return;
|
|
364
|
+
fetch('/config/time-zone', { cache: 'no-store' })
|
|
365
|
+
.then(function (r) { return r.json(); })
|
|
366
|
+
.then(renderTimeZone)
|
|
367
|
+
.catch(function (err) {
|
|
368
|
+
var box = document.getElementById('timeZoneBox');
|
|
369
|
+
if (box) box.innerHTML = '<p class="error">Could not load the time zone: ' + escapeHtml(err.message) + '</p>';
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
309
373
|
// ---- Upgrade ----
|
|
310
374
|
|
|
311
375
|
var UPGRADE_NOTE = 'Updates this server' + String.fromCharCode(8217) + 's LoopEngine code and restarts it, which takes a minute or two. ' +
|
|
@@ -436,6 +500,7 @@ ${consoleShellScript}
|
|
|
436
500
|
.then(function (d) {
|
|
437
501
|
content.innerHTML = renderSettings(d);
|
|
438
502
|
loadedSections.settings = true;
|
|
503
|
+
loadTimeZone();
|
|
439
504
|
loadUpgrade();
|
|
440
505
|
})
|
|
441
506
|
.catch(function (err) {
|
|
@@ -69,3 +69,27 @@ export declare function modelKeyEnvVar(provider: string): string | undefined;
|
|
|
69
69
|
*/
|
|
70
70
|
export declare function setModelProviderKey(provider: string, key: unknown): Promise<void>;
|
|
71
71
|
export declare function clearModelProviderKey(provider: string): Promise<void>;
|
|
72
|
+
export declare class TimeZoneError extends Error {
|
|
73
|
+
}
|
|
74
|
+
export interface TimeZoneView {
|
|
75
|
+
/** The one in use. */
|
|
76
|
+
timeZone: string;
|
|
77
|
+
/** Whether it was chosen (LOOPENGINE_TIME_ZONE), not the server's. */
|
|
78
|
+
chosen: boolean;
|
|
79
|
+
server: string;
|
|
80
|
+
zones: string[];
|
|
81
|
+
}
|
|
82
|
+
export declare function describeTimeZone(): TimeZoneView;
|
|
83
|
+
/**
|
|
84
|
+
* The console's Settings → Time zone: saved like a model key (the
|
|
85
|
+
* project's .env by default) and applied to this process at once. New
|
|
86
|
+
* schedules and template start times use it from then on; with
|
|
87
|
+
* `moveSchedules`, existing daily and weekly schedules move to it too,
|
|
88
|
+
* keeping their time of day. An empty value goes back to the server's.
|
|
89
|
+
*/
|
|
90
|
+
export declare function setWorkspaceTimeZone(value: unknown, options?: {
|
|
91
|
+
moveSchedules?: boolean;
|
|
92
|
+
}): Promise<{
|
|
93
|
+
timeZone: string;
|
|
94
|
+
moved: number;
|
|
95
|
+
}>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getComposioAuthStatus } from '../core/gateway-tools.js';
|
|
2
|
+
import { moveScheduledTasksToTimeZone } from '../core/scheduler.js';
|
|
3
|
+
import { TIME_ZONE_ENV, isValidTimeZone, listTimeZones, serverTimeZone, workspaceTimeZone } from '../core/time-zone.js';
|
|
2
4
|
import { setEnvVar, unsetEnvVar } from './env-admin.js';
|
|
3
5
|
export class ModelKeyError extends Error {
|
|
4
6
|
}
|
|
@@ -102,3 +104,31 @@ export async function clearModelProviderKey(provider) {
|
|
|
102
104
|
throw new ModelKeyError(`unknown provider '${provider}'`);
|
|
103
105
|
await unsetEnvVar(envVar);
|
|
104
106
|
}
|
|
107
|
+
export class TimeZoneError extends Error {
|
|
108
|
+
}
|
|
109
|
+
export function describeTimeZone() {
|
|
110
|
+
const timeZone = workspaceTimeZone();
|
|
111
|
+
const configured = process.env[TIME_ZONE_ENV]?.trim();
|
|
112
|
+
return { timeZone, chosen: Boolean(configured && isValidTimeZone(configured)), server: serverTimeZone(), zones: listTimeZones() };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The console's Settings → Time zone: saved like a model key (the
|
|
116
|
+
* project's .env by default) and applied to this process at once. New
|
|
117
|
+
* schedules and template start times use it from then on; with
|
|
118
|
+
* `moveSchedules`, existing daily and weekly schedules move to it too,
|
|
119
|
+
* keeping their time of day. An empty value goes back to the server's.
|
|
120
|
+
*/
|
|
121
|
+
export async function setWorkspaceTimeZone(value, options = {}) {
|
|
122
|
+
const requested = typeof value === 'string' ? value.trim() : '';
|
|
123
|
+
if (requested && !isValidTimeZone(requested))
|
|
124
|
+
throw new TimeZoneError(`"${requested}" isn't a time zone this server knows, such as America/New_York or Europe/London`);
|
|
125
|
+
const before = workspaceTimeZone();
|
|
126
|
+
// Saved by its canonical name ("america/new_york" → "America/New_York").
|
|
127
|
+
if (requested)
|
|
128
|
+
await setEnvVar(TIME_ZONE_ENV, new Intl.DateTimeFormat('en-US', { timeZone: requested }).resolvedOptions().timeZone);
|
|
129
|
+
else
|
|
130
|
+
await unsetEnvVar(TIME_ZONE_ENV);
|
|
131
|
+
const timeZone = workspaceTimeZone();
|
|
132
|
+
const moved = options.moveSchedules && timeZone !== before ? await moveScheduledTasksToTimeZone(before, timeZone) : 0;
|
|
133
|
+
return { timeZone, moved };
|
|
134
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// rule. Its template's rules are written before its abilities' own, so
|
|
10
10
|
// they take precedence where both name the same tool.
|
|
11
11
|
import { createScheduledTask, pauseScheduledTask } from '../core/scheduler.js';
|
|
12
|
+
import { workspaceTimeZone } from '../core/time-zone.js';
|
|
12
13
|
import { STARTER_TEMPLATES, starterTemplate, templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
14
|
import { ActauthRuleExistsError, addActauthRule, setDefaultDecision } from './actauth-admin.js';
|
|
14
15
|
import { setAgentEnvVar } from './env-admin.js';
|
|
@@ -168,7 +169,7 @@ export async function createFromTemplate(id, deps) {
|
|
|
168
169
|
task_name: setup.schedule.name,
|
|
169
170
|
});
|
|
170
171
|
await pauseScheduledTask(task.task_id);
|
|
171
|
-
schedule = { description: describeTemplateSchedule(setup.schedule), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
172
|
+
schedule = { description: describeTemplateSchedule(setup.schedule), timeZone: workspaceTimeZone(), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
172
173
|
}
|
|
173
174
|
catch (err) {
|
|
174
175
|
scheduleError = err instanceof Error ? err.message : String(err);
|
|
@@ -210,7 +211,7 @@ async function createFromTemplateLegacy(id, deps) {
|
|
|
210
211
|
const startAt = templateScheduleStart(setup.schedule);
|
|
211
212
|
const task = await createScheduledTask({ agent: name, message: setup.schedule.message, schedule: { every: setup.schedule.every, ...(startAt ? { start_at: startAt } : {}) }, task_name: setup.schedule.name });
|
|
212
213
|
await pauseScheduledTask(task.task_id);
|
|
213
|
-
schedule = { description: describeTemplateSchedule(setup.schedule), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
214
|
+
schedule = { description: describeTemplateSchedule(setup.schedule), timeZone: workspaceTimeZone(), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
214
215
|
}
|
|
215
216
|
catch (err) {
|
|
216
217
|
scheduleError = err instanceof Error ? err.message : String(err);
|
|
@@ -136,7 +136,7 @@ ${consoleShellScript}
|
|
|
136
136
|
slot.innerHTML = '<div class="tp-preview">' +
|
|
137
137
|
'<div><strong>Created <code>' + esc(c.name) + '</code>.</strong> Anything its permissions don' + String.fromCharCode(8217) + 't cover asks you first.</div>' +
|
|
138
138
|
(installed ? '<div>' + CHECK + ' Installed ' + installed + '</div>' : '') +
|
|
139
|
-
(c.schedule ? '<div>' + CHECK + ' Schedule created, paused: ' + esc(c.schedule.description) + (c.schedule.next_run_at ? ' (first run ' + esc(new Date(c.schedule.next_run_at).toLocaleString()) + ' once turned on)' : '') + '</div>' : '') +
|
|
139
|
+
(c.schedule ? '<div>' + CHECK + ' Schedule created, paused: ' + esc(c.schedule.description) + (c.schedule.next_run_at ? ' (first run ' + esc(new Date(c.schedule.next_run_at).toLocaleString(undefined, c.schedule.timeZone ? { timeZone: c.schedule.timeZone, timeZoneName: 'short' } : undefined)) + ' once turned on)' : '') + '</div>' : '') +
|
|
140
140
|
(c.loadError ? '<div class="error">It was written but couldn' + String.fromCharCode(8217) + 't be loaded: ' + esc(c.loadError) + '</div>' : '') +
|
|
141
141
|
(c.scheduleError ? '<div class="error">Its schedule wasn' + String.fromCharCode(8217) + 't created: ' + esc(c.scheduleError) + '. Add one in its Schedules tab.</div>' : '') +
|
|
142
142
|
'<div><strong>Finish setup:</strong>' + checklistHtml(c.checklist) + '</div>' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"dist/core/messaging",
|
|
58
58
|
"dist/core/scheduler.js",
|
|
59
59
|
"dist/core/scheduler.d.ts",
|
|
60
|
+
"dist/core/time-zone.js",
|
|
61
|
+
"dist/core/time-zone.d.ts",
|
|
60
62
|
"dist/core/session-store.js",
|
|
61
63
|
"dist/core/session-store.d.ts",
|
|
62
64
|
"dist/core/vector-index.js",
|