openmausbot 0.1.68 → 0.1.70
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/dist/assets/index-B2F__IRS.css +1 -0
- package/dist/assets/index-CLj3t5Ba.js +280 -0
- package/dist/assets/{index-HqooKFTS.js → index-D1RGhLUK.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +40 -2
- package/dist-server/drivers/agents-proxy.js +188 -5
- package/dist-server/index.js +2431 -984
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/openmausbot.js +923 -334
- package/dist-server/pair-cli.js +923 -334
- package/dist-server/prepare-cloudflared.js +20 -3
- package/dist-server/server/avatar-image.js +86 -34
- package/dist-server/server/bot-package.js +60 -14
- package/dist-server/server/config.js +23 -2
- package/dist-server/server/delegations.js +132 -74
- package/dist-server/server/drivers/acp/qwen.js +142 -5
- package/dist-server/server/drivers/agents-proxy.js +224 -4
- package/dist-server/server/drivers/claude-login-auth.js +102 -9
- package/dist-server/server/drivers/claude.js +11 -5
- package/dist-server/server/drivers/codex-device-auth.js +102 -3
- package/dist-server/server/drivers/codex-identity.js +115 -0
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/engine-install.js +137 -0
- package/dist-server/server/env-path.js +12 -0
- package/dist-server/server/harness/registry.js +33 -6
- package/dist-server/server/index.js +365 -26
- package/dist-server/server/package-export.js +7 -0
- package/dist-server/server/peer-provenance.js +6 -2
- package/dist-server/server/provider-auth-sessions.js +24 -0
- package/dist-server/server/routine-requests.js +270 -25
- package/dist-server/server/routines.js +297 -69
- package/dist-server/server/steer-queue.js +48 -1
- package/dist-server/server/store.js +22 -4
- package/dist-server/server/system-prompt.js +1 -0
- package/dist-server/server/team-backup.js +18 -4
- package/dist-server/shared/image-generation.js +28 -0
- package/dist-server/shared/team-backup.js +1 -0
- package/dist-server/vps-container-mcp.js +40 -2
- package/package.json +1 -1
- package/dist/assets/index-CbaejjQA.js +0 -278
- package/dist/assets/index-aJ5EWZOQ.css +0 -1
|
@@ -12,6 +12,9 @@ function routineRequestOwnerKey(owner) {
|
|
|
12
12
|
const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
13
13
|
const CATCH_UP_MS = 12 * 60 * 60_000;
|
|
14
14
|
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
15
|
+
const LOCAL_DAY_MS = 24 * 60 * 60_000;
|
|
16
|
+
const INTERVAL_RESTRICTION_SEARCH_MS = 9 * LOCAL_DAY_MS;
|
|
17
|
+
const CLOCK_TIME = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
15
18
|
const MAX_RUNS = 2_000;
|
|
16
19
|
const MAX_ATTACHMENTS = 50;
|
|
17
20
|
const attachmentSchema = z.object({
|
|
@@ -86,7 +89,14 @@ function cloneSchedule(schedule) {
|
|
|
86
89
|
if (schedule.type === "once")
|
|
87
90
|
return { type: "once", at: schedule.at };
|
|
88
91
|
if (schedule.type === "interval") {
|
|
89
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
type: "interval",
|
|
94
|
+
everyMinutes: schedule.everyMinutes,
|
|
95
|
+
anchorAt: schedule.anchorAt,
|
|
96
|
+
...(schedule.weekdays ? { weekdays: [...schedule.weekdays] } : {}),
|
|
97
|
+
...(schedule.window ? { window: { ...schedule.window } } : {}),
|
|
98
|
+
...(schedule.endsAt === undefined ? {} : { endsAt: schedule.endsAt }),
|
|
99
|
+
};
|
|
90
100
|
}
|
|
91
101
|
return { type: "daily", time: schedule.time, weekdays: [...schedule.weekdays] };
|
|
92
102
|
}
|
|
@@ -164,6 +174,57 @@ function composeExecutionPrompt(prompt, attachments, carry) {
|
|
|
164
174
|
}
|
|
165
175
|
return parts.filter(Boolean).join("\n\n");
|
|
166
176
|
}
|
|
177
|
+
function cleanIntervalWeekdays(value) {
|
|
178
|
+
if (value == null)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > ALL_DAYS.length) {
|
|
181
|
+
throw new Error("Choose at least one valid interval day");
|
|
182
|
+
}
|
|
183
|
+
if (value.some((day) => typeof day !== "number" || !Number.isInteger(day) || day < 0 || day > 6)) {
|
|
184
|
+
throw new Error("Interval days must be whole numbers from 0 to 6");
|
|
185
|
+
}
|
|
186
|
+
if (new Set(value).size !== value.length)
|
|
187
|
+
throw new Error("Choose each interval day only once");
|
|
188
|
+
const weekdays = [...value].sort((a, b) => a - b);
|
|
189
|
+
return weekdays.length === ALL_DAYS.length ? undefined : weekdays;
|
|
190
|
+
}
|
|
191
|
+
function clockMinutes(value) {
|
|
192
|
+
const match = CLOCK_TIME.exec(value);
|
|
193
|
+
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
|
|
194
|
+
}
|
|
195
|
+
function cleanIntervalWindow(value, everyMinutes) {
|
|
196
|
+
if (value == null)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
199
|
+
throw new Error("Choose a valid interval time window");
|
|
200
|
+
}
|
|
201
|
+
const candidate = value;
|
|
202
|
+
const start = typeof candidate.start === "string" ? candidate.start : "";
|
|
203
|
+
const end = typeof candidate.end === "string" ? candidate.end : "";
|
|
204
|
+
const startMinutes = clockMinutes(start);
|
|
205
|
+
const endMinutes = clockMinutes(end);
|
|
206
|
+
if (startMinutes == null || endMinutes == null) {
|
|
207
|
+
throw new Error("Interval window times must use HH:MM");
|
|
208
|
+
}
|
|
209
|
+
if (startMinutes >= endMinutes) {
|
|
210
|
+
throw new Error("Interval window must start before it ends on the same day");
|
|
211
|
+
}
|
|
212
|
+
if (endMinutes - startMinutes < everyMinutes) {
|
|
213
|
+
throw new Error("Interval window must be at least as long as the interval");
|
|
214
|
+
}
|
|
215
|
+
return { start, end };
|
|
216
|
+
}
|
|
217
|
+
function cleanIntervalEndsAt(value, anchorAt) {
|
|
218
|
+
if (value == null)
|
|
219
|
+
return undefined;
|
|
220
|
+
if (typeof value !== "number" ||
|
|
221
|
+
!Number.isSafeInteger(value) ||
|
|
222
|
+
value < anchorAt ||
|
|
223
|
+
value > MAX_DATE_MS) {
|
|
224
|
+
throw new Error("Choose a valid interval end time after its start");
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
167
228
|
function finishedOrder(run) {
|
|
168
229
|
return run.finishedAt ?? run.createdAt;
|
|
169
230
|
}
|
|
@@ -191,7 +252,17 @@ function cleanSchedule(schedule) {
|
|
|
191
252
|
anchorAt > MAX_DATE_MS) {
|
|
192
253
|
throw new Error("Choose a valid interval start time");
|
|
193
254
|
}
|
|
194
|
-
|
|
255
|
+
const weekdays = cleanIntervalWeekdays(schedule.weekdays);
|
|
256
|
+
const window = cleanIntervalWindow(schedule.window, everyMinutes);
|
|
257
|
+
const endsAt = cleanIntervalEndsAt(schedule.endsAt, anchorAt);
|
|
258
|
+
return {
|
|
259
|
+
type: "interval",
|
|
260
|
+
everyMinutes,
|
|
261
|
+
anchorAt,
|
|
262
|
+
...(weekdays ? { weekdays } : {}),
|
|
263
|
+
...(window ? { window } : {}),
|
|
264
|
+
...(endsAt === undefined ? {} : { endsAt }),
|
|
265
|
+
};
|
|
195
266
|
}
|
|
196
267
|
throw new Error("Choose a supported schedule");
|
|
197
268
|
}
|
|
@@ -203,17 +274,56 @@ function loadSchedule(value) {
|
|
|
203
274
|
return null;
|
|
204
275
|
}
|
|
205
276
|
}
|
|
277
|
+
function intervalHasRestrictions(schedule) {
|
|
278
|
+
return schedule.weekdays !== undefined || schedule.window !== undefined || schedule.endsAt !== undefined;
|
|
279
|
+
}
|
|
280
|
+
function intervalAllowsOccurrence(schedule, at) {
|
|
281
|
+
if (schedule.endsAt !== undefined && at > schedule.endsAt)
|
|
282
|
+
return false;
|
|
283
|
+
const date = new Date(at);
|
|
284
|
+
if (schedule.weekdays && !schedule.weekdays.includes(date.getDay()))
|
|
285
|
+
return false;
|
|
286
|
+
if (schedule.window) {
|
|
287
|
+
const minute = date.getHours() * 60 + date.getMinutes();
|
|
288
|
+
const start = clockMinutes(schedule.window.start);
|
|
289
|
+
const end = clockMinutes(schedule.window.end);
|
|
290
|
+
if (minute < start || minute >= end)
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
function isSameLocalDay(left, right) {
|
|
296
|
+
const a = new Date(left);
|
|
297
|
+
const b = new Date(right);
|
|
298
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
299
|
+
}
|
|
300
|
+
function nextAlignedInterval(schedule, after) {
|
|
301
|
+
if (schedule.anchorAt > after)
|
|
302
|
+
return schedule.anchorAt;
|
|
303
|
+
const intervalMs = schedule.everyMinutes * 60_000;
|
|
304
|
+
const intervalsElapsed = Math.floor((after - schedule.anchorAt) / intervalMs);
|
|
305
|
+
const candidate = schedule.anchorAt + (intervalsElapsed + 1) * intervalMs;
|
|
306
|
+
return Number.isSafeInteger(candidate) && candidate <= MAX_DATE_MS ? candidate : null;
|
|
307
|
+
}
|
|
206
308
|
/** Next wall-clock occurrence in this computer's timezone, strictly after `after`. */
|
|
207
309
|
export function nextOccurrence(schedule, after) {
|
|
208
310
|
if (schedule.type === "once")
|
|
209
311
|
return schedule.at > after ? schedule.at : null;
|
|
210
312
|
if (schedule.type === "interval") {
|
|
211
|
-
if (schedule.anchorAt > after)
|
|
212
|
-
return schedule.anchorAt;
|
|
213
313
|
const intervalMs = schedule.everyMinutes * 60_000;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
314
|
+
let candidate = nextAlignedInterval(schedule, after);
|
|
315
|
+
if (!intervalHasRestrictions(schedule))
|
|
316
|
+
return candidate;
|
|
317
|
+
const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
|
|
318
|
+
for (let checked = 0; candidate !== null && checked < maxCandidates; checked++) {
|
|
319
|
+
if (schedule.endsAt !== undefined && candidate > schedule.endsAt)
|
|
320
|
+
return null;
|
|
321
|
+
if (intervalAllowsOccurrence(schedule, candidate))
|
|
322
|
+
return candidate;
|
|
323
|
+
const next = candidate + intervalMs;
|
|
324
|
+
candidate = Number.isSafeInteger(next) && next <= MAX_DATE_MS ? next : null;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
217
327
|
}
|
|
218
328
|
const [hour, minute] = schedule.time.split(":").map(Number);
|
|
219
329
|
const weekdays = new Set(cleanDays(schedule.weekdays));
|
|
@@ -227,10 +337,38 @@ export function nextOccurrence(schedule, after) {
|
|
|
227
337
|
return null;
|
|
228
338
|
}
|
|
229
339
|
function latestIntervalOccurrence(schedule, at) {
|
|
230
|
-
if (schedule.anchorAt > at)
|
|
231
|
-
return null;
|
|
232
340
|
const intervalMs = schedule.everyMinutes * 60_000;
|
|
233
|
-
|
|
341
|
+
const ceiling = Math.min(at, schedule.endsAt ?? at);
|
|
342
|
+
if (schedule.anchorAt > ceiling)
|
|
343
|
+
return null;
|
|
344
|
+
let candidate = schedule.anchorAt + Math.floor((ceiling - schedule.anchorAt) / intervalMs) * intervalMs;
|
|
345
|
+
if (!intervalHasRestrictions(schedule))
|
|
346
|
+
return candidate;
|
|
347
|
+
const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
|
|
348
|
+
for (let checked = 0; checked < maxCandidates; checked++) {
|
|
349
|
+
if (intervalAllowsOccurrence(schedule, candidate))
|
|
350
|
+
return candidate;
|
|
351
|
+
const previous = candidate - intervalMs;
|
|
352
|
+
if (!Number.isSafeInteger(previous) || previous < schedule.anchorAt)
|
|
353
|
+
return null;
|
|
354
|
+
candidate = previous;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function mergeScheduleUpdate(current, incoming) {
|
|
359
|
+
if (current.type !== "interval" || incoming.type !== "interval")
|
|
360
|
+
return incoming;
|
|
361
|
+
const merged = { ...incoming };
|
|
362
|
+
if (!Object.hasOwn(incoming, "weekdays") && current.weekdays) {
|
|
363
|
+
merged.weekdays = [...current.weekdays];
|
|
364
|
+
}
|
|
365
|
+
if (!Object.hasOwn(incoming, "window") && current.window) {
|
|
366
|
+
merged.window = { ...current.window };
|
|
367
|
+
}
|
|
368
|
+
if (!Object.hasOwn(incoming, "endsAt") && current.endsAt !== undefined) {
|
|
369
|
+
merged.endsAt = current.endsAt;
|
|
370
|
+
}
|
|
371
|
+
return merged;
|
|
234
372
|
}
|
|
235
373
|
function sanitizeInput(input) {
|
|
236
374
|
const name = String(input.name ?? "").trim().slice(0, 80);
|
|
@@ -311,6 +449,7 @@ export class RoutineManager {
|
|
|
311
449
|
timeoutMinutes: loadTimeoutMinutes(routine.timeoutMinutes),
|
|
312
450
|
attachments: loadAttachments(routine.attachments),
|
|
313
451
|
sourceThreadId: persistedSourceThreadId.parse(routine.sourceThreadId),
|
|
452
|
+
resultsThreadId: persistedSourceThreadId.parse(routine.resultsThreadId),
|
|
314
453
|
};
|
|
315
454
|
if (loaded.timeoutMinutes === undefined)
|
|
316
455
|
delete loaded.timeoutMinutes;
|
|
@@ -329,6 +468,7 @@ export class RoutineManager {
|
|
|
329
468
|
timeoutMinutes: loadTimeoutMinutes(run.timeoutMinutes),
|
|
330
469
|
attachments: loadAttachments(run.attachments),
|
|
331
470
|
sourceThreadId: persistedSourceThreadId.parse(run.sourceThreadId),
|
|
471
|
+
resultsThreadId: persistedSourceThreadId.parse(run.resultsThreadId),
|
|
332
472
|
};
|
|
333
473
|
if (loaded.timeoutMinutes === undefined)
|
|
334
474
|
delete loaded.timeoutMinutes;
|
|
@@ -467,21 +607,26 @@ export class RoutineManager {
|
|
|
467
607
|
if (this.targetState(clean) === "missing")
|
|
468
608
|
throw new Error(this.missingTargetMessage(clean.target));
|
|
469
609
|
const at = this.now();
|
|
610
|
+
const nextRunAt = clean.enabled ? this.initialOccurrence(clean.schedule, at) : null;
|
|
611
|
+
if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
|
|
612
|
+
throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
|
|
613
|
+
}
|
|
470
614
|
const routine = {
|
|
471
615
|
id: randomUUID(),
|
|
472
616
|
...clean,
|
|
473
617
|
// Only a confirmed chat card supplies `request`; the public calendar
|
|
474
618
|
// API cannot choose an arbitrary transcript as a reporting target.
|
|
475
619
|
sourceThreadId: request?.threadId,
|
|
476
|
-
nextRunAt
|
|
620
|
+
nextRunAt,
|
|
477
621
|
createdAt: at,
|
|
478
622
|
updatedAt: at,
|
|
479
623
|
};
|
|
624
|
+
const discardResults = this.applyResultsInput(routine, input.resultsThreadId);
|
|
480
625
|
this.commitMutation(() => {
|
|
481
626
|
this.routines.unshift(routine);
|
|
482
627
|
if (request)
|
|
483
628
|
this.rememberRoutineRequest(request, routine.id, at);
|
|
484
|
-
});
|
|
629
|
+
}, discardResults);
|
|
485
630
|
this.emitRoutine(routine);
|
|
486
631
|
return cloneRoutine(routine);
|
|
487
632
|
}
|
|
@@ -505,7 +650,7 @@ export class RoutineManager {
|
|
|
505
650
|
groupId: Object.hasOwn(patch, "groupId") ? patch.groupId : routine.groupId,
|
|
506
651
|
runOn: patch.runOn ?? routine.runOn,
|
|
507
652
|
enabled: patch.enabled ?? routine.enabled,
|
|
508
|
-
schedule: patch.schedule
|
|
653
|
+
schedule: patch.schedule ? mergeScheduleUpdate(routine.schedule, patch.schedule) : routine.schedule,
|
|
509
654
|
durationMinutes: patch.durationMinutes ?? routine.durationMinutes,
|
|
510
655
|
timeoutMinutes: Object.hasOwn(patch, "timeoutMinutes") ? patch.timeoutMinutes : routine.timeoutMinutes,
|
|
511
656
|
attachments: patch.attachments ?? routine.attachments,
|
|
@@ -513,16 +658,24 @@ export class RoutineManager {
|
|
|
513
658
|
});
|
|
514
659
|
if (this.targetState(clean) === "missing")
|
|
515
660
|
throw new Error(this.missingTargetMessage(clean.target));
|
|
516
|
-
const cancelledRuns = [];
|
|
517
661
|
const scheduleChanged = JSON.stringify(clean.schedule) !== JSON.stringify(routine.schedule);
|
|
518
662
|
const enabledChanged = clean.enabled !== routine.enabled;
|
|
663
|
+
// Definition-only edits retain due work and offline catch-up.
|
|
664
|
+
const nextRunAt = !clean.enabled ? null : scheduleChanged || enabledChanged
|
|
665
|
+
? this.initialOccurrence(clean.schedule, now)
|
|
666
|
+
: routine.nextRunAt;
|
|
667
|
+
if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
|
|
668
|
+
throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
|
|
669
|
+
}
|
|
670
|
+
const destination = { ...routine, ...clean };
|
|
671
|
+
if (destination.botId !== routine.botId)
|
|
672
|
+
delete destination.resultsThreadId;
|
|
673
|
+
const discardResults = this.applyResultsInput(destination, patch.resultsThreadId);
|
|
674
|
+
const cancelledRuns = [];
|
|
519
675
|
this.commitMutation(() => {
|
|
520
676
|
Object.assign(routine, clean, {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
nextRunAt: !clean.enabled ? null : scheduleChanged || enabledChanged
|
|
524
|
-
? this.initialOccurrence(clean.schedule, now)
|
|
525
|
-
: routine.nextRunAt,
|
|
677
|
+
resultsThreadId: destination.resultsThreadId,
|
|
678
|
+
nextRunAt,
|
|
526
679
|
// `updatedAt` doubles as the optimistic revision on durable routine
|
|
527
680
|
// confirmation cards. Keep it monotonic even for two writes in one ms.
|
|
528
681
|
updatedAt: Math.max(now, routine.updatedAt + 1),
|
|
@@ -547,7 +700,7 @@ export class RoutineManager {
|
|
|
547
700
|
}
|
|
548
701
|
if (request)
|
|
549
702
|
this.rememberRoutineRequest(request, routine.id, now);
|
|
550
|
-
});
|
|
703
|
+
}, discardResults);
|
|
551
704
|
for (const run of cancelledRuns)
|
|
552
705
|
this.emitRun(run);
|
|
553
706
|
this.emitRoutine(routine);
|
|
@@ -658,15 +811,19 @@ export class RoutineManager {
|
|
|
658
811
|
if (!routine)
|
|
659
812
|
return null;
|
|
660
813
|
let run;
|
|
814
|
+
const allocations = [];
|
|
815
|
+
const previousDestination = routine.resultsThreadId;
|
|
661
816
|
this.commitMutation(() => {
|
|
662
|
-
run = this.newRun(routine, this.now(), true);
|
|
663
|
-
//
|
|
664
|
-
//
|
|
817
|
+
run = this.newRun(routine, this.now(), true, allocations, request?.threadId ?? routine.sourceThreadId);
|
|
818
|
+
// Preserve the invoking chat as provenance/fallback for this run.
|
|
819
|
+
// An explicitly configured results destination continues to win.
|
|
665
820
|
if (request)
|
|
666
821
|
run.sourceThreadId = request.threadId;
|
|
667
822
|
if (request)
|
|
668
823
|
this.rememberRoutineRequest(request, run.id, this.now());
|
|
669
|
-
});
|
|
824
|
+
}, () => this.discardResultsThreads(allocations));
|
|
825
|
+
if (routine.resultsThreadId !== previousDestination)
|
|
826
|
+
this.emitRoutine(routine);
|
|
670
827
|
this.emitRun(run);
|
|
671
828
|
queueMicrotask(() => void this.tick());
|
|
672
829
|
return cloneRun(run);
|
|
@@ -833,51 +990,57 @@ export class RoutineManager {
|
|
|
833
990
|
await this.options.interruptTurn?.(run.botId, threadId, run.runOn ?? "maus").catch(() => { });
|
|
834
991
|
}
|
|
835
992
|
}
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
993
|
+
const dueRoutines = this.routines.filter((routine) => routine.enabled && routine.nextRunAt != null && routine.nextRunAt <= now);
|
|
994
|
+
const scheduledRuns = [];
|
|
995
|
+
const allocations = [];
|
|
996
|
+
if (dueRoutines.length > 0) {
|
|
997
|
+
this.commitMutation(() => {
|
|
998
|
+
for (const routine of dueRoutines) {
|
|
999
|
+
const pendingAt = routine.nextRunAt;
|
|
1000
|
+
const late = now - pendingAt;
|
|
1001
|
+
const scheduledFor = routine.schedule.type === "interval" && late <= CATCH_UP_MS
|
|
1002
|
+
? latestIntervalOccurrence(routine.schedule, now) ?? pendingAt
|
|
1003
|
+
: pendingAt;
|
|
1004
|
+
// One slow interval run must not build an unbounded queue of stale
|
|
1005
|
+
// copies behind it. The series still advances on its original phase.
|
|
1006
|
+
const overlapping = routine.schedule.type === "interval" && this.runs.some((run) => run.routineId === routine.id && ["queued", "running", "waiting"].includes(run.status));
|
|
1007
|
+
if (!overlapping) {
|
|
1008
|
+
const run = this.newRun(routine, scheduledFor, false, allocations);
|
|
1009
|
+
if (late > CATCH_UP_MS) {
|
|
1010
|
+
run.status = "missed";
|
|
1011
|
+
run.finishedAt = now;
|
|
1012
|
+
run.error = "This computer was offline for more than 12 hours after the scheduled time";
|
|
1013
|
+
}
|
|
1014
|
+
scheduledRuns.push(run);
|
|
1015
|
+
}
|
|
1016
|
+
routine.nextRunAt =
|
|
1017
|
+
routine.schedule.type === "once" ? null : nextOccurrence(routine.schedule, Math.max(now, scheduledFor));
|
|
1018
|
+
// `updatedAt` is the optimistic definition revision carried by
|
|
1019
|
+
// routine confirmation cards. Moving the scheduler cursor is runtime
|
|
1020
|
+
// progress, not a definition edit, so recurring ticks must not make a
|
|
1021
|
+
// still-accurate pending confirmation stale. A one-time routine does
|
|
1022
|
+
// mutate its definition by auto-disabling after its occurrence.
|
|
1023
|
+
if (routine.schedule.type === "once") {
|
|
1024
|
+
routine.enabled = false;
|
|
1025
|
+
routine.updatedAt = Math.max(now, routine.updatedAt + 1);
|
|
1026
|
+
}
|
|
1027
|
+
else if (routine.schedule.type === "interval" && routine.nextRunAt === null) {
|
|
1028
|
+
routine.enabled = false;
|
|
1029
|
+
routine.updatedAt = Math.max(now, routine.updatedAt + 1);
|
|
1030
|
+
}
|
|
861
1031
|
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
// progress, not a definition edit, so recurring ticks must not make a
|
|
868
|
-
// still-accurate pending confirmation stale. A one-time routine does
|
|
869
|
-
// mutate its definition by auto-disabling after its occurrence.
|
|
870
|
-
if (routine.schedule.type === "once") {
|
|
871
|
-
routine.enabled = false;
|
|
872
|
-
routine.updatedAt = Math.max(now, routine.updatedAt + 1);
|
|
873
|
-
}
|
|
1032
|
+
}, () => this.discardResultsThreads(allocations));
|
|
1033
|
+
}
|
|
1034
|
+
// Reporting may persist transcript cards, so publish only after the
|
|
1035
|
+
// scheduler batch (including each destination) is durable.
|
|
1036
|
+
for (const routine of dueRoutines)
|
|
874
1037
|
this.emitRoutine(routine);
|
|
875
|
-
|
|
1038
|
+
for (const run of scheduledRuns)
|
|
1039
|
+
this.emitRun(run);
|
|
1040
|
+
for (const run of scheduledRuns) {
|
|
1041
|
+
if (run.status === "missed")
|
|
1042
|
+
this.options.onRunFailed?.(cloneRun(run));
|
|
876
1043
|
}
|
|
877
|
-
if (changed)
|
|
878
|
-
this.save();
|
|
879
|
-
for (const missed of missedRuns)
|
|
880
|
-
this.options.onRunFailed?.(missed);
|
|
881
1044
|
// Oldest queued requests have priority. New manual/webhook arrivals
|
|
882
1045
|
// must not continually overtake work that has already waited. Snapshot
|
|
883
1046
|
// the queue because each dispatch can asynchronously add/cancel work.
|
|
@@ -900,6 +1063,15 @@ export class RoutineManager {
|
|
|
900
1063
|
this.save();
|
|
901
1064
|
this.emitRun(run);
|
|
902
1065
|
}
|
|
1066
|
+
const requiresCurrentDayOccurrence = definition.schedule.weekdays !== undefined || definition.schedule.window !== undefined;
|
|
1067
|
+
const mayDispatch = intervalAllowsOccurrence(definition.schedule, now) &&
|
|
1068
|
+
(!requiresCurrentDayOccurrence || (latest !== null && isSameLocalDay(latest, now)));
|
|
1069
|
+
if (!mayDispatch) {
|
|
1070
|
+
if (nextOccurrence(definition.schedule, now) === null) {
|
|
1071
|
+
this.missQueuedRun(run, "The routine ended before this scheduled run could start");
|
|
1072
|
+
}
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
903
1075
|
}
|
|
904
1076
|
const state = this.targetState(run);
|
|
905
1077
|
if (state === "busy")
|
|
@@ -1071,6 +1243,15 @@ export class RoutineManager {
|
|
|
1071
1243
|
this.emitRun(run);
|
|
1072
1244
|
this.options.onRunFailed?.(cloneRun(run));
|
|
1073
1245
|
}
|
|
1246
|
+
missQueuedRun(run, message) {
|
|
1247
|
+
run.status = "missed";
|
|
1248
|
+
run.attention = undefined;
|
|
1249
|
+
run.error = redactSecretsInText(message).slice(0, 500);
|
|
1250
|
+
run.finishedAt = this.now();
|
|
1251
|
+
this.save();
|
|
1252
|
+
this.emitRun(run);
|
|
1253
|
+
this.options.onRunFailed?.(cloneRun(run));
|
|
1254
|
+
}
|
|
1074
1255
|
targetState(target) {
|
|
1075
1256
|
if (target.target === "room-goal") {
|
|
1076
1257
|
if (!target.groupId || !this.options.goalState)
|
|
@@ -1095,7 +1276,16 @@ export class RoutineManager {
|
|
|
1095
1276
|
return schedule.at;
|
|
1096
1277
|
return nextOccurrence(schedule, now);
|
|
1097
1278
|
}
|
|
1098
|
-
newRun(routine, scheduledFor, manual) {
|
|
1279
|
+
newRun(routine, scheduledFor, manual, allocations, sourceThreadId = routine.sourceThreadId) {
|
|
1280
|
+
if (routine.target === "bot" && this.options.resolveResultsThread) {
|
|
1281
|
+
const destination = this.options.resolveResultsThread({ ...routine, sourceThreadId }, false);
|
|
1282
|
+
if (destination !== routine.resultsThreadId) {
|
|
1283
|
+
if (destination)
|
|
1284
|
+
allocations.push({ botId: routine.botId, threadId: destination });
|
|
1285
|
+
routine.resultsThreadId = destination;
|
|
1286
|
+
routine.updatedAt = Math.max(this.now(), routine.updatedAt + 1);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1099
1289
|
const run = {
|
|
1100
1290
|
id: randomUUID(),
|
|
1101
1291
|
routineId: routine.id,
|
|
@@ -1112,12 +1302,44 @@ export class RoutineManager {
|
|
|
1112
1302
|
status: "queued",
|
|
1113
1303
|
manual,
|
|
1114
1304
|
triggerSource: manual ? "manual" : "schedule",
|
|
1115
|
-
sourceThreadId
|
|
1305
|
+
sourceThreadId,
|
|
1306
|
+
resultsThreadId: routine.resultsThreadId,
|
|
1116
1307
|
createdAt: this.now(),
|
|
1117
1308
|
};
|
|
1118
1309
|
this.runs.push(run);
|
|
1119
1310
|
return run;
|
|
1120
1311
|
}
|
|
1312
|
+
discardResultsThreads(allocations) {
|
|
1313
|
+
for (const { botId, threadId } of allocations) {
|
|
1314
|
+
try {
|
|
1315
|
+
this.options.discardResultsThread?.(botId, threadId);
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
console.error("routine: could not discard uncommitted results thread", error);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
applyResultsInput(routine, value) {
|
|
1323
|
+
if (routine.target !== "bot") {
|
|
1324
|
+
if (value != null)
|
|
1325
|
+
throw Object.assign(new Error("Results threads are only available for bot routines"), { status: 400 });
|
|
1326
|
+
delete routine.resultsThreadId;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (value === undefined)
|
|
1330
|
+
return;
|
|
1331
|
+
if (value === null) {
|
|
1332
|
+
const destination = this.options.resolveResultsThread?.(routine, true);
|
|
1333
|
+
if (!destination)
|
|
1334
|
+
throw new Error("Could not create a results thread for this routine");
|
|
1335
|
+
routine.resultsThreadId = destination;
|
|
1336
|
+
return () => this.options.discardResultsThread?.(routine.botId, destination);
|
|
1337
|
+
}
|
|
1338
|
+
if (typeof value !== "string" || !value.trim() || !this.options.isResultsThread?.(routine.botId, value.trim())) {
|
|
1339
|
+
throw Object.assign(new Error("Choose a visible results thread belonging to this bot"), { status: 400 });
|
|
1340
|
+
}
|
|
1341
|
+
routine.resultsThreadId = value.trim();
|
|
1342
|
+
}
|
|
1121
1343
|
emitRoutine(routine) {
|
|
1122
1344
|
this.options.emit?.({ kind: "routine", routine: cloneRoutine(routine) });
|
|
1123
1345
|
}
|
|
@@ -1164,7 +1386,7 @@ export class RoutineManager {
|
|
|
1164
1386
|
* state if writing or renaming that file fails so a retry cannot mistake an
|
|
1165
1387
|
* uncommitted action for a durable one.
|
|
1166
1388
|
*/
|
|
1167
|
-
commitMutation(mutate) {
|
|
1389
|
+
commitMutation(mutate, rollback) {
|
|
1168
1390
|
const before = {
|
|
1169
1391
|
routines: this.routines.map(cloneRoutine),
|
|
1170
1392
|
runs: this.runs.map(cloneRun),
|
|
@@ -1178,6 +1400,12 @@ export class RoutineManager {
|
|
|
1178
1400
|
this.routines = before.routines;
|
|
1179
1401
|
this.runs = before.runs;
|
|
1180
1402
|
this.routineRequestReceipts = before.receipts;
|
|
1403
|
+
try {
|
|
1404
|
+
rollback?.();
|
|
1405
|
+
}
|
|
1406
|
+
catch (cleanupError) {
|
|
1407
|
+
console.error("routine: could not discard uncommitted results thread", cleanupError);
|
|
1408
|
+
}
|
|
1181
1409
|
throw error;
|
|
1182
1410
|
}
|
|
1183
1411
|
}
|
|
@@ -18,6 +18,30 @@
|
|
|
18
18
|
// the feature.
|
|
19
19
|
import { newId } from "./contracts.js";
|
|
20
20
|
const queues = new Map(); // threadId → waiting sends
|
|
21
|
+
const listeners = new Set();
|
|
22
|
+
const changed = () => {
|
|
23
|
+
for (const listener of listeners) {
|
|
24
|
+
try {
|
|
25
|
+
listener();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.warn("steer-queue: change listener failed");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Public pending chips only: never expose provider prompts or reply context. */
|
|
33
|
+
export function queuedSteerSnapshot(ownsThread) {
|
|
34
|
+
return Object.fromEntries([...queues]
|
|
35
|
+
.filter(([threadId, entry]) => ownsThread(entry.botId, threadId))
|
|
36
|
+
.map(([threadId, entry]) => [threadId, entry.items.map((item) => ({
|
|
37
|
+
queueId: item.messageId, text: item.text, ...(item.reason ? { reason: item.reason } : {}),
|
|
38
|
+
}))]));
|
|
39
|
+
}
|
|
40
|
+
/** Publish changes synchronously so every client can restore/cancel the queue. */
|
|
41
|
+
export function onSteeredQueueChange(listener) {
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => { listeners.delete(listener); };
|
|
44
|
+
}
|
|
21
45
|
/** Hold a mid-turn send off the transcript until drain. */
|
|
22
46
|
export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
23
47
|
const id = newId();
|
|
@@ -33,10 +57,27 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
|
33
57
|
replyToId: options.replyToId,
|
|
34
58
|
sendId: options.sendId,
|
|
35
59
|
reason: options.reason,
|
|
60
|
+
unattended: options.unattended,
|
|
36
61
|
});
|
|
37
62
|
queues.set(threadId, entry);
|
|
63
|
+
changed();
|
|
38
64
|
return { id };
|
|
39
65
|
}
|
|
66
|
+
/** Where a thread stands among this bot's threads waiting for a free slot:
|
|
67
|
+
* 1 for the next to start. The drain visits queues in insertion order, so
|
|
68
|
+
* insertion order is the line. Null when nothing of this bot's is waiting
|
|
69
|
+
* on that thread. */
|
|
70
|
+
export function queuedThreadPosition(botId, threadId) {
|
|
71
|
+
let position = 0;
|
|
72
|
+
for (const [candidate, entry] of queues) {
|
|
73
|
+
if (entry.botId !== botId || !entry.items.some((item) => item.reason === "capacity"))
|
|
74
|
+
continue;
|
|
75
|
+
position += 1;
|
|
76
|
+
if (candidate === threadId)
|
|
77
|
+
return position;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
40
81
|
/** Drain every queue whose task is idle: append the held lines (leaf is now
|
|
41
82
|
* the finished turn's last item), then one run per thread whose prompt is
|
|
42
83
|
* the texts separated by a blank line. `userMessage` is the last appended line
|
|
@@ -53,6 +94,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
53
94
|
if (!bot) {
|
|
54
95
|
// the bot or task was deleted while messages waited
|
|
55
96
|
queues.delete(threadId);
|
|
97
|
+
changed();
|
|
56
98
|
continue;
|
|
57
99
|
}
|
|
58
100
|
if (bot.busy || isBlocked?.(entry.botId, threadId))
|
|
@@ -60,6 +102,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
60
102
|
// committed to draining: the entry leaves the map before anything runs,
|
|
61
103
|
// so a settle racing another settle can never fire the same queue twice
|
|
62
104
|
queues.delete(threadId);
|
|
105
|
+
changed();
|
|
63
106
|
const appended = [];
|
|
64
107
|
for (const item of entry.items) {
|
|
65
108
|
// queueId is the pending-chip identity from the 202; append still
|
|
@@ -81,7 +124,10 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
81
124
|
// message into one HTML block, which makes that attachment stop being a
|
|
82
125
|
// native image when the combined follow-up is dispatched.
|
|
83
126
|
const prompt = entry.items.map((item) => item.prompt).join("\n\n");
|
|
84
|
-
void run(entry.botId, threadId, prompt, last, appended.map((message) => message.id)
|
|
127
|
+
void run(entry.botId, threadId, prompt, last, appended.map((message) => message.id),
|
|
128
|
+
// one unattended line makes the whole drained turn unattended: a
|
|
129
|
+
// person's words in the same queue cannot re-attend a bot's own
|
|
130
|
+
entry.items.some((item) => item.unattended === true));
|
|
85
131
|
}
|
|
86
132
|
}
|
|
87
133
|
/** Find the receipt for a retry whose message is still waiting to drain. */
|
|
@@ -107,6 +153,7 @@ export function cancelSteeredMessage(botId, messageId, expectedThreadId) {
|
|
|
107
153
|
queues.delete(threadId);
|
|
108
154
|
else
|
|
109
155
|
queues.set(threadId, { botId: entry.botId, items });
|
|
156
|
+
changed();
|
|
110
157
|
return true;
|
|
111
158
|
}
|
|
112
159
|
return false;
|