@holon-run/agentinbox 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli.js +110 -0
- package/dist/src/current_agent.js +1 -0
- package/dist/src/http.js +157 -1
- package/dist/src/runtime_gate.js +464 -0
- package/dist/src/service.js +645 -7
- package/dist/src/store.js +105 -3
- package/dist/src/terminal.js +12 -4
- package/drizzle/migrations/0005_timers.sql +22 -0
- package/drizzle/migrations/0006_terminal_runtime_pid.sql +1 -0
- package/drizzle/schema.ts +21 -0
- package/package.json +1 -1
package/dist/src/service.js
CHANGED
|
@@ -7,6 +7,7 @@ const source_schema_1 = require("./source_schema");
|
|
|
7
7
|
const source_resolution_1 = require("./source_resolution");
|
|
8
8
|
const filter_1 = require("./filter");
|
|
9
9
|
const terminal_1 = require("./terminal");
|
|
10
|
+
const runtime_gate_1 = require("./runtime_gate");
|
|
10
11
|
const DEFAULT_SUBSCRIPTION_POLL_LIMIT = 100;
|
|
11
12
|
const DEFAULT_ACTIVATION_WINDOW_MS = 3_000;
|
|
12
13
|
const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
|
|
@@ -17,6 +18,12 @@ const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
17
18
|
const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
|
|
18
19
|
const DEFAULT_SYNC_INTERVAL_MS = 2_000;
|
|
19
20
|
const DEFAULT_IDLE_SOURCE_GRACE_MS = 5 * 60 * 1000;
|
|
21
|
+
const DIRECT_INBOX_SOURCE_ID = "__agentinbox_direct_text__";
|
|
22
|
+
const DIRECT_INBOX_EVENT_VARIANT = "agentinbox.direct_text_message";
|
|
23
|
+
const TIMER_INBOX_SOURCE_ID = "__agentinbox_timer__";
|
|
24
|
+
const TIMER_INBOX_EVENT_VARIANT = "agentinbox.timer_fired";
|
|
25
|
+
const DEFAULT_TIMER_SENDER = "timer";
|
|
26
|
+
const MIN_TIMER_INTERVAL_MS = 60 * 1000;
|
|
20
27
|
const WEBHOOK_ACTIVATION_MODES = new Set(["activation_only", "activation_with_items"]);
|
|
21
28
|
const SUBSCRIPTION_START_POLICIES = new Set(["latest", "earliest", "at_offset", "at_time"]);
|
|
22
29
|
class AgentInboxService {
|
|
@@ -30,11 +37,12 @@ class AgentInboxService {
|
|
|
30
37
|
notificationBuffers = new Map();
|
|
31
38
|
inboxWatchers = new Map();
|
|
32
39
|
syncInterval = null;
|
|
40
|
+
timerSyncTimeout = null;
|
|
33
41
|
stopping = false;
|
|
34
42
|
lastAckedInboxGcAt = 0;
|
|
35
43
|
lastLifecycleCleanupAt = 0;
|
|
36
44
|
lastOfflineAgentGcAt = 0;
|
|
37
|
-
constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher()) {
|
|
45
|
+
constructor(store, adapters, activationDispatcher = new ActivationDispatcher(), backend, activationPolicy, terminalDispatcher = new terminal_1.TerminalDispatcher(), activationGate = new runtime_gate_1.DefaultActivationGate()) {
|
|
38
46
|
this.store = store;
|
|
39
47
|
this.adapters = adapters;
|
|
40
48
|
this.activationDispatcher = activationDispatcher;
|
|
@@ -43,9 +51,11 @@ class AgentInboxService {
|
|
|
43
51
|
this.activationMaxItems = activationPolicy?.maxItems ?? DEFAULT_ACTIVATION_MAX_ITEMS;
|
|
44
52
|
this.ackedRetentionMs = DEFAULT_ACKED_RETENTION_MS;
|
|
45
53
|
this.terminalDispatcher = terminalDispatcher;
|
|
54
|
+
this.activationGate = activationGate;
|
|
46
55
|
}
|
|
47
56
|
activationDispatcher;
|
|
48
57
|
terminalDispatcher;
|
|
58
|
+
activationGate;
|
|
49
59
|
async start() {
|
|
50
60
|
if (this.syncInterval) {
|
|
51
61
|
return;
|
|
@@ -57,10 +67,12 @@ class AgentInboxService {
|
|
|
57
67
|
void this.runAckedInboxGcIfDue();
|
|
58
68
|
void this.syncLifecycleGc();
|
|
59
69
|
}, DEFAULT_SYNC_INTERVAL_MS);
|
|
70
|
+
this.refreshTimersOnStart();
|
|
60
71
|
await this.syncAllSubscriptions();
|
|
61
72
|
await this.syncActivationDispatchStates();
|
|
62
73
|
await this.runAckedInboxGcIfDue(true);
|
|
63
74
|
await this.syncLifecycleGc();
|
|
75
|
+
await this.syncTimers();
|
|
64
76
|
}
|
|
65
77
|
async stop() {
|
|
66
78
|
this.stopping = true;
|
|
@@ -74,7 +86,12 @@ class AgentInboxService {
|
|
|
74
86
|
clearInterval(this.syncInterval);
|
|
75
87
|
this.syncInterval = null;
|
|
76
88
|
}
|
|
89
|
+
if (this.timerSyncTimeout) {
|
|
90
|
+
clearTimeout(this.timerSyncTimeout);
|
|
91
|
+
this.timerSyncTimeout = null;
|
|
92
|
+
}
|
|
77
93
|
await this.flushAllPendingNotifications();
|
|
94
|
+
await this.awaitInFlightNotificationBuffers();
|
|
78
95
|
}
|
|
79
96
|
async registerSource(input) {
|
|
80
97
|
const existing = this.store.getSourceByKey(input.sourceType, input.sourceKey);
|
|
@@ -319,6 +336,7 @@ class AgentInboxService {
|
|
|
319
336
|
return this.registerAgent({
|
|
320
337
|
runtimeKind: detected.runtimeKind,
|
|
321
338
|
runtimeSessionId: detected.runtimeSessionId ?? null,
|
|
339
|
+
runtimePid: detected.runtimePid ?? null,
|
|
322
340
|
backend: detected.backend,
|
|
323
341
|
mode: "agent_prompt",
|
|
324
342
|
tmuxPaneId: detected.tmuxPaneId ?? null,
|
|
@@ -365,8 +383,106 @@ class AgentInboxService {
|
|
|
365
383
|
}
|
|
366
384
|
});
|
|
367
385
|
this.inboxWatchers.delete(agentId);
|
|
386
|
+
this.rescheduleTimerSync();
|
|
368
387
|
return { removed: true };
|
|
369
388
|
}
|
|
389
|
+
registerTimer(input) {
|
|
390
|
+
this.getAgent(input.agentId);
|
|
391
|
+
const normalized = normalizeTimerInput(input);
|
|
392
|
+
const timezone = normalized.timezone ?? detectHostTimezone();
|
|
393
|
+
assertValidTimeZone(timezone);
|
|
394
|
+
const now = (0, util_1.nowIso)();
|
|
395
|
+
const nextFireAt = computeNextTimerFire({
|
|
396
|
+
mode: normalized.mode,
|
|
397
|
+
at: normalized.at ?? null,
|
|
398
|
+
intervalMs: normalized.every ?? null,
|
|
399
|
+
cronExpr: normalized.cron ?? null,
|
|
400
|
+
timezone,
|
|
401
|
+
fromIso: now,
|
|
402
|
+
lastFiredAt: null,
|
|
403
|
+
restartRecovery: false,
|
|
404
|
+
});
|
|
405
|
+
const timer = {
|
|
406
|
+
scheduleId: (0, util_1.generateId)("sched"),
|
|
407
|
+
agentId: input.agentId,
|
|
408
|
+
status: nextFireAt ? "active" : "paused",
|
|
409
|
+
mode: normalized.mode,
|
|
410
|
+
at: normalized.at ?? null,
|
|
411
|
+
intervalMs: normalized.every ?? null,
|
|
412
|
+
cronExpr: normalized.cron ?? null,
|
|
413
|
+
timezone,
|
|
414
|
+
message: normalizeDirectInboxMessage(input.message),
|
|
415
|
+
sender: normalizeDirectInboxSender(input.sender) ?? DEFAULT_TIMER_SENDER,
|
|
416
|
+
nextFireAt,
|
|
417
|
+
lastFiredAt: null,
|
|
418
|
+
createdAt: now,
|
|
419
|
+
updatedAt: now,
|
|
420
|
+
};
|
|
421
|
+
this.store.insertTimer(timer);
|
|
422
|
+
this.rescheduleTimerSync();
|
|
423
|
+
return timer;
|
|
424
|
+
}
|
|
425
|
+
listTimers(agentId) {
|
|
426
|
+
if (agentId) {
|
|
427
|
+
this.getAgent(agentId);
|
|
428
|
+
return this.store.listTimersForAgent(agentId);
|
|
429
|
+
}
|
|
430
|
+
return this.store.listTimers();
|
|
431
|
+
}
|
|
432
|
+
getTimer(scheduleId) {
|
|
433
|
+
const timer = this.store.getTimer(scheduleId);
|
|
434
|
+
if (!timer) {
|
|
435
|
+
throw new Error(`unknown timer: ${scheduleId}`);
|
|
436
|
+
}
|
|
437
|
+
return timer;
|
|
438
|
+
}
|
|
439
|
+
pauseTimer(scheduleId) {
|
|
440
|
+
const timer = this.getTimer(scheduleId);
|
|
441
|
+
const updated = this.store.updateTimer(scheduleId, {
|
|
442
|
+
status: "paused",
|
|
443
|
+
nextFireAt: null,
|
|
444
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
445
|
+
});
|
|
446
|
+
this.rescheduleTimerSync();
|
|
447
|
+
return {
|
|
448
|
+
updated: timer.status !== "paused",
|
|
449
|
+
timer: updated,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
resumeTimer(scheduleId) {
|
|
453
|
+
const timer = this.getTimer(scheduleId);
|
|
454
|
+
if (timer.mode === "at" && timer.lastFiredAt) {
|
|
455
|
+
throw new Error(`timer ${scheduleId} already fired`);
|
|
456
|
+
}
|
|
457
|
+
const now = (0, util_1.nowIso)();
|
|
458
|
+
const nextFireAt = computeNextTimerFire({
|
|
459
|
+
mode: timer.mode,
|
|
460
|
+
at: timer.at ?? null,
|
|
461
|
+
intervalMs: timer.intervalMs ?? null,
|
|
462
|
+
cronExpr: timer.cronExpr ?? null,
|
|
463
|
+
timezone: timer.timezone,
|
|
464
|
+
fromIso: now,
|
|
465
|
+
lastFiredAt: timer.lastFiredAt ?? null,
|
|
466
|
+
restartRecovery: false,
|
|
467
|
+
});
|
|
468
|
+
const status = nextFireAt ? "active" : "paused";
|
|
469
|
+
const updated = this.store.updateTimer(scheduleId, {
|
|
470
|
+
status,
|
|
471
|
+
nextFireAt,
|
|
472
|
+
updatedAt: now,
|
|
473
|
+
});
|
|
474
|
+
this.rescheduleTimerSync();
|
|
475
|
+
return {
|
|
476
|
+
updated: timer.status !== status,
|
|
477
|
+
timer: updated,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
removeTimer(scheduleId) {
|
|
481
|
+
this.getTimer(scheduleId);
|
|
482
|
+
this.store.deleteTimer(scheduleId);
|
|
483
|
+
this.rescheduleTimerSync();
|
|
484
|
+
return { removed: true, scheduleId };
|
|
485
|
+
}
|
|
370
486
|
addWebhookActivationTarget(agentId, input) {
|
|
371
487
|
this.getAgent(agentId);
|
|
372
488
|
validateNotifyLeaseMs(input.notifyLeaseMs);
|
|
@@ -578,6 +694,109 @@ class AgentInboxService {
|
|
|
578
694
|
listInboxItems(agentId, options) {
|
|
579
695
|
return this.store.listInboxItems(this.ensureInboxForAgent(agentId).inboxId, options);
|
|
580
696
|
}
|
|
697
|
+
async addDirectInboxTextMessage(agentId, input, env = process.env) {
|
|
698
|
+
this.getAgent(agentId);
|
|
699
|
+
const message = normalizeDirectInboxMessage(input.message);
|
|
700
|
+
const sender = normalizeDirectInboxSender(input.sender) ?? this.detectDirectInboxSender(env);
|
|
701
|
+
return this.materializeAgentInboxItem(agentId, {
|
|
702
|
+
sourceId: DIRECT_INBOX_SOURCE_ID,
|
|
703
|
+
sourceNativeId: (0, util_1.generateId)("direct"),
|
|
704
|
+
eventVariant: DIRECT_INBOX_EVENT_VARIANT,
|
|
705
|
+
summary: summarizeDirectInboxMessage(sender),
|
|
706
|
+
rawPayload: {
|
|
707
|
+
type: "direct_text_message",
|
|
708
|
+
message,
|
|
709
|
+
sender,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async fireTimer(timer, now) {
|
|
714
|
+
const scheduledFireAt = timer.nextFireAt ?? now;
|
|
715
|
+
this.materializeAgentInboxItem(timer.agentId, {
|
|
716
|
+
sourceId: TIMER_INBOX_SOURCE_ID,
|
|
717
|
+
sourceNativeId: `${timer.scheduleId}:${scheduledFireAt}`,
|
|
718
|
+
eventVariant: TIMER_INBOX_EVENT_VARIANT,
|
|
719
|
+
summary: summarizeTimerMessage(timer.scheduleId),
|
|
720
|
+
rawPayload: {
|
|
721
|
+
type: "timer_fired",
|
|
722
|
+
scheduleId: timer.scheduleId,
|
|
723
|
+
message: timer.message,
|
|
724
|
+
sender: timer.sender ?? DEFAULT_TIMER_SENDER,
|
|
725
|
+
},
|
|
726
|
+
occurredAt: scheduledFireAt,
|
|
727
|
+
allowDuplicate: true,
|
|
728
|
+
});
|
|
729
|
+
const nextFireAt = computeNextTimerFire({
|
|
730
|
+
mode: timer.mode,
|
|
731
|
+
at: timer.at ?? null,
|
|
732
|
+
intervalMs: timer.intervalMs ?? null,
|
|
733
|
+
cronExpr: timer.cronExpr ?? null,
|
|
734
|
+
timezone: timer.timezone,
|
|
735
|
+
fromIso: scheduledFireAt,
|
|
736
|
+
lastFiredAt: scheduledFireAt,
|
|
737
|
+
restartRecovery: false,
|
|
738
|
+
});
|
|
739
|
+
this.store.updateTimer(timer.scheduleId, {
|
|
740
|
+
status: nextFireAt ? "active" : "paused",
|
|
741
|
+
nextFireAt,
|
|
742
|
+
lastFiredAt: now,
|
|
743
|
+
updatedAt: now,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
materializeAgentInboxItem(agentId, input) {
|
|
747
|
+
const inbox = this.ensureInboxForAgent(agentId);
|
|
748
|
+
const occurredAt = input.occurredAt ?? (0, util_1.nowIso)();
|
|
749
|
+
const item = {
|
|
750
|
+
itemId: (0, util_1.generateId)("item"),
|
|
751
|
+
sourceId: input.sourceId,
|
|
752
|
+
sourceNativeId: input.sourceNativeId,
|
|
753
|
+
eventVariant: input.eventVariant,
|
|
754
|
+
inboxId: inbox.inboxId,
|
|
755
|
+
occurredAt,
|
|
756
|
+
metadata: {},
|
|
757
|
+
rawPayload: input.rawPayload,
|
|
758
|
+
deliveryHandle: null,
|
|
759
|
+
ackedAt: null,
|
|
760
|
+
};
|
|
761
|
+
const inserted = this.store.insertInboxItem(item);
|
|
762
|
+
if (!inserted && !input.allowDuplicate) {
|
|
763
|
+
throw new Error(`failed to persist direct inbox message item: ${item.itemId}`);
|
|
764
|
+
}
|
|
765
|
+
if (!inserted) {
|
|
766
|
+
return {
|
|
767
|
+
itemId: item.itemId,
|
|
768
|
+
inboxId: inbox.inboxId,
|
|
769
|
+
activated: false,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
this.notifyInboxWatchers(agentId, [item]);
|
|
773
|
+
const activationItem = {
|
|
774
|
+
itemId: item.itemId,
|
|
775
|
+
sourceId: item.sourceId,
|
|
776
|
+
sourceNativeId: item.sourceNativeId,
|
|
777
|
+
eventVariant: item.eventVariant,
|
|
778
|
+
inboxId: item.inboxId,
|
|
779
|
+
occurredAt: item.occurredAt,
|
|
780
|
+
metadata: item.metadata,
|
|
781
|
+
rawPayload: item.rawPayload,
|
|
782
|
+
deliveryHandle: item.deliveryHandle,
|
|
783
|
+
};
|
|
784
|
+
const targets = this.store.listActivationTargetsForAgent(agentId).filter((target) => target.status === "active");
|
|
785
|
+
for (const target of targets) {
|
|
786
|
+
this.enqueueActivationTarget(target, {
|
|
787
|
+
agentId,
|
|
788
|
+
subscriptionId: null,
|
|
789
|
+
sourceId: input.sourceId,
|
|
790
|
+
summary: input.summary,
|
|
791
|
+
item: activationItem,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
itemId: item.itemId,
|
|
796
|
+
inboxId: inbox.inboxId,
|
|
797
|
+
activated: targets.length > 0,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
581
800
|
watchInbox(agentId, options, onEvent) {
|
|
582
801
|
const inbox = this.ensureInboxForAgent(agentId);
|
|
583
802
|
const pendingItems = [];
|
|
@@ -1004,6 +1223,7 @@ class AgentInboxService {
|
|
|
1004
1223
|
const target = this.store.updateTerminalActivationTargetHeartbeat(existing.targetId, {
|
|
1005
1224
|
runtimeKind: input.runtimeKind ?? "unknown",
|
|
1006
1225
|
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
1226
|
+
runtimePid: input.runtimePid ?? null,
|
|
1007
1227
|
tmuxPaneId: input.tmuxPaneId ?? null,
|
|
1008
1228
|
tty: input.tty ?? null,
|
|
1009
1229
|
termProgram: input.termProgram ?? null,
|
|
@@ -1030,6 +1250,7 @@ class AgentInboxService {
|
|
|
1030
1250
|
notifyLeaseMs: input.notifyLeaseMs ?? DEFAULT_NOTIFY_LEASE_MS,
|
|
1031
1251
|
runtimeKind: input.runtimeKind ?? "unknown",
|
|
1032
1252
|
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
1253
|
+
runtimePid: input.runtimePid ?? null,
|
|
1033
1254
|
backend: input.backend,
|
|
1034
1255
|
tmuxPaneId: input.tmuxPaneId ?? null,
|
|
1035
1256
|
tty: input.tty ?? null,
|
|
@@ -1111,6 +1332,15 @@ class AgentInboxService {
|
|
|
1111
1332
|
await this.flushNotificationBuffer(key);
|
|
1112
1333
|
}
|
|
1113
1334
|
}
|
|
1335
|
+
async awaitInFlightNotificationBuffers() {
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const activeBuffers = Array.from(this.notificationBuffers.values());
|
|
1338
|
+
if (activeBuffers.every((buffer) => !buffer.inFlight)) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1114
1344
|
async flushNotificationBuffer(key) {
|
|
1115
1345
|
const buffer = this.notificationBuffers.get(key);
|
|
1116
1346
|
if (!buffer || buffer.inFlight || buffer.pending.length === 0) {
|
|
@@ -1130,7 +1360,7 @@ class AgentInboxService {
|
|
|
1130
1360
|
targetId: buffer.targetId,
|
|
1131
1361
|
newItemCount: entries.length,
|
|
1132
1362
|
summary: entries[0]?.summary ?? null,
|
|
1133
|
-
subscriptionIds:
|
|
1363
|
+
subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1134
1364
|
sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1135
1365
|
items: entries.map((entry) => entry.item),
|
|
1136
1366
|
});
|
|
@@ -1146,7 +1376,7 @@ class AgentInboxService {
|
|
|
1146
1376
|
leaseExpiresAt: state.leaseExpiresAt,
|
|
1147
1377
|
pendingNewItemCount: state.pendingNewItemCount + entries.length,
|
|
1148
1378
|
pendingSummary: state.pendingSummary ?? entries[0]?.summary ?? null,
|
|
1149
|
-
pendingSubscriptionIds:
|
|
1379
|
+
pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
|
|
1150
1380
|
pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
|
|
1151
1381
|
updatedAt: (0, util_1.nowIso)(),
|
|
1152
1382
|
});
|
|
@@ -1154,9 +1384,11 @@ class AgentInboxService {
|
|
|
1154
1384
|
const hasPendingDuringFlight = buffer.pending.length > 0;
|
|
1155
1385
|
buffer.inFlight = false;
|
|
1156
1386
|
if (hasPendingDuringFlight) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1387
|
+
if (!this.stopping) {
|
|
1388
|
+
buffer.timer = setTimeout(() => {
|
|
1389
|
+
void this.flushNotificationBuffer(key);
|
|
1390
|
+
}, this.activationWindowMs);
|
|
1391
|
+
}
|
|
1160
1392
|
return;
|
|
1161
1393
|
}
|
|
1162
1394
|
this.notificationBuffers.delete(key);
|
|
@@ -1233,6 +1465,26 @@ class AgentInboxService {
|
|
|
1233
1465
|
const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
|
|
1234
1466
|
try {
|
|
1235
1467
|
if (target.kind === "terminal") {
|
|
1468
|
+
const gate = await this.activationGate.evaluate(target);
|
|
1469
|
+
if (gate.outcome === "offline") {
|
|
1470
|
+
this.markActivationTargetOffline(target.targetId, `runtime gate: ${gate.reason}`);
|
|
1471
|
+
this.reconcileAgentStatus(target.agentId);
|
|
1472
|
+
return "offline";
|
|
1473
|
+
}
|
|
1474
|
+
if (gate.outcome === "defer") {
|
|
1475
|
+
this.store.upsertActivationDispatchState({
|
|
1476
|
+
agentId: input.agentId,
|
|
1477
|
+
targetId: input.targetId,
|
|
1478
|
+
status: "dirty",
|
|
1479
|
+
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1480
|
+
pendingNewItemCount: input.newItemCount,
|
|
1481
|
+
pendingSummary: input.summary,
|
|
1482
|
+
pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
|
|
1483
|
+
pendingSourceIds: uniqueSorted(input.sourceIds),
|
|
1484
|
+
updatedAt: (0, util_1.nowIso)(),
|
|
1485
|
+
});
|
|
1486
|
+
return "deferred";
|
|
1487
|
+
}
|
|
1236
1488
|
const prompt = (0, terminal_1.renderAgentPrompt)({
|
|
1237
1489
|
inboxId: inbox.inboxId,
|
|
1238
1490
|
newItemCount: input.newItemCount,
|
|
@@ -1297,11 +1549,37 @@ class AgentInboxService {
|
|
|
1297
1549
|
leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
|
|
1298
1550
|
pendingNewItemCount: entries.length,
|
|
1299
1551
|
pendingSummary: entries[0]?.summary ?? null,
|
|
1300
|
-
pendingSubscriptionIds:
|
|
1552
|
+
pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
|
|
1301
1553
|
pendingSourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
|
|
1302
1554
|
updatedAt: (0, util_1.nowIso)(),
|
|
1303
1555
|
});
|
|
1304
1556
|
}
|
|
1557
|
+
detectDirectInboxSender(env) {
|
|
1558
|
+
try {
|
|
1559
|
+
const detected = (0, terminal_1.detectTerminalContext)(env);
|
|
1560
|
+
const target = findExistingTerminalActivationTarget(this.store, {
|
|
1561
|
+
runtimeKind: detected.runtimeKind,
|
|
1562
|
+
runtimeSessionId: detected.runtimeSessionId ?? null,
|
|
1563
|
+
runtimePid: detected.runtimePid ?? null,
|
|
1564
|
+
backend: detected.backend,
|
|
1565
|
+
tmuxPaneId: detected.tmuxPaneId ?? null,
|
|
1566
|
+
tty: detected.tty ?? null,
|
|
1567
|
+
termProgram: detected.termProgram ?? null,
|
|
1568
|
+
itermSessionId: detected.itermSessionId ?? null,
|
|
1569
|
+
});
|
|
1570
|
+
if (target) {
|
|
1571
|
+
return target.agentId;
|
|
1572
|
+
}
|
|
1573
|
+
return detected.runtimeSessionId
|
|
1574
|
+
?? detected.tmuxPaneId
|
|
1575
|
+
?? detected.itermSessionId
|
|
1576
|
+
?? detected.tty
|
|
1577
|
+
?? null;
|
|
1578
|
+
}
|
|
1579
|
+
catch {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1305
1583
|
markActivationTargetDelivered(targetId) {
|
|
1306
1584
|
const now = (0, util_1.nowIso)();
|
|
1307
1585
|
this.store.updateActivationTargetRuntime(targetId, {
|
|
@@ -1474,6 +1752,9 @@ class AgentInboxService {
|
|
|
1474
1752
|
if (agents.length > 0) {
|
|
1475
1753
|
this.store.save();
|
|
1476
1754
|
}
|
|
1755
|
+
if (agents.length > 0) {
|
|
1756
|
+
this.rescheduleTimerSync();
|
|
1757
|
+
}
|
|
1477
1758
|
return { removedAgents: agents.length };
|
|
1478
1759
|
}
|
|
1479
1760
|
async syncAllSubscriptions() {
|
|
@@ -1531,6 +1812,64 @@ class AgentInboxService {
|
|
|
1531
1812
|
console.warn("offline agent gc failed:", error);
|
|
1532
1813
|
}
|
|
1533
1814
|
}
|
|
1815
|
+
async syncTimers() {
|
|
1816
|
+
const now = (0, util_1.nowIso)();
|
|
1817
|
+
const due = this.store.listDueTimers(now);
|
|
1818
|
+
for (const timer of due) {
|
|
1819
|
+
try {
|
|
1820
|
+
await this.fireTimer(timer, now);
|
|
1821
|
+
}
|
|
1822
|
+
catch (error) {
|
|
1823
|
+
console.warn(`timer fire failed for ${timer.scheduleId}:`, error);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
this.rescheduleTimerSync();
|
|
1827
|
+
}
|
|
1828
|
+
refreshTimersOnStart() {
|
|
1829
|
+
const now = (0, util_1.nowIso)();
|
|
1830
|
+
for (const timer of this.store.listTimers()) {
|
|
1831
|
+
if (timer.status !== "active") {
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
const nextFireAt = computeNextTimerFire({
|
|
1835
|
+
mode: timer.mode,
|
|
1836
|
+
at: timer.at ?? null,
|
|
1837
|
+
intervalMs: timer.intervalMs ?? null,
|
|
1838
|
+
cronExpr: timer.cronExpr ?? null,
|
|
1839
|
+
timezone: timer.timezone,
|
|
1840
|
+
fromIso: now,
|
|
1841
|
+
lastFiredAt: timer.lastFiredAt ?? null,
|
|
1842
|
+
restartRecovery: true,
|
|
1843
|
+
});
|
|
1844
|
+
this.store.updateTimer(timer.scheduleId, {
|
|
1845
|
+
nextFireAt,
|
|
1846
|
+
status: nextFireAt ? "active" : "paused",
|
|
1847
|
+
updatedAt: now,
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
rescheduleTimerSync() {
|
|
1852
|
+
if (this.timerSyncTimeout) {
|
|
1853
|
+
clearTimeout(this.timerSyncTimeout);
|
|
1854
|
+
this.timerSyncTimeout = null;
|
|
1855
|
+
}
|
|
1856
|
+
if (this.stopping) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const nearest = this.store.getNearestActiveTimer();
|
|
1860
|
+
if (!nearest?.nextFireAt) {
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
const dueAt = Date.parse(nearest.nextFireAt);
|
|
1864
|
+
if (Number.isNaN(dueAt)) {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const delay = Math.max(0, dueAt - Date.now());
|
|
1868
|
+
this.timerSyncTimeout = setTimeout(() => {
|
|
1869
|
+
this.timerSyncTimeout = null;
|
|
1870
|
+
void this.syncTimers();
|
|
1871
|
+
}, delay);
|
|
1872
|
+
}
|
|
1534
1873
|
refreshSourceIdleState(sourceId) {
|
|
1535
1874
|
const source = this.store.getSource(sourceId);
|
|
1536
1875
|
if (!source) {
|
|
@@ -1841,6 +2180,9 @@ function isSameTerminalIdentity(target, input) {
|
|
|
1841
2180
|
function uniqueSorted(values) {
|
|
1842
2181
|
return Array.from(new Set(values)).sort();
|
|
1843
2182
|
}
|
|
2183
|
+
function uniqueSortedNullable(values) {
|
|
2184
|
+
return uniqueSorted(values.filter((value) => typeof value === "string" && value.length > 0));
|
|
2185
|
+
}
|
|
1844
2186
|
function notificationBufferKey(agentId, targetId) {
|
|
1845
2187
|
return `${agentId}::${targetId}`;
|
|
1846
2188
|
}
|
|
@@ -1873,3 +2215,299 @@ function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
|
|
|
1873
2215
|
}
|
|
1874
2216
|
return `${sourceType}:${sourceKey}:${eventVariant}`;
|
|
1875
2217
|
}
|
|
2218
|
+
function summarizeDirectInboxMessage(sender) {
|
|
2219
|
+
if (sender) {
|
|
2220
|
+
return `direct_text_message:${sender}`;
|
|
2221
|
+
}
|
|
2222
|
+
return "direct_text_message";
|
|
2223
|
+
}
|
|
2224
|
+
function summarizeTimerMessage(scheduleId) {
|
|
2225
|
+
return `timer:${scheduleId}`;
|
|
2226
|
+
}
|
|
2227
|
+
function normalizeDirectInboxMessage(message) {
|
|
2228
|
+
if (typeof message !== "string") {
|
|
2229
|
+
throw new Error("direct inbox message must be a string");
|
|
2230
|
+
}
|
|
2231
|
+
const trimmed = message.trim();
|
|
2232
|
+
if (trimmed.length === 0) {
|
|
2233
|
+
throw new Error("direct inbox message must not be empty");
|
|
2234
|
+
}
|
|
2235
|
+
return trimmed;
|
|
2236
|
+
}
|
|
2237
|
+
function normalizeDirectInboxSender(sender) {
|
|
2238
|
+
if (sender == null) {
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
if (typeof sender !== "string") {
|
|
2242
|
+
throw new Error("direct inbox sender must be a string");
|
|
2243
|
+
}
|
|
2244
|
+
const trimmed = sender.trim();
|
|
2245
|
+
if (trimmed.length === 0) {
|
|
2246
|
+
throw new Error("direct inbox sender must not be empty");
|
|
2247
|
+
}
|
|
2248
|
+
return trimmed;
|
|
2249
|
+
}
|
|
2250
|
+
function normalizeTimerInput(input) {
|
|
2251
|
+
const modes = [input.at != null, input.every != null, input.cron != null].filter(Boolean).length;
|
|
2252
|
+
if (modes !== 1) {
|
|
2253
|
+
throw new Error("timers require exactly one of at, every, or cron");
|
|
2254
|
+
}
|
|
2255
|
+
if (input.at != null) {
|
|
2256
|
+
const at = normalizeIsoTimestamp(input.at, "timer at");
|
|
2257
|
+
return { mode: "at", at, timezone: input.timezone ?? null };
|
|
2258
|
+
}
|
|
2259
|
+
if (input.every != null) {
|
|
2260
|
+
if (!Number.isInteger(input.every) || input.every < MIN_TIMER_INTERVAL_MS) {
|
|
2261
|
+
throw new Error("timer every interval must be an integer number of milliseconds and at least 60000");
|
|
2262
|
+
}
|
|
2263
|
+
return { mode: "every", every: input.every, timezone: input.timezone ?? null };
|
|
2264
|
+
}
|
|
2265
|
+
const cron = normalizeCronExpression(input.cron);
|
|
2266
|
+
return { mode: "cron", cron, timezone: input.timezone ?? null };
|
|
2267
|
+
}
|
|
2268
|
+
function normalizeIsoTimestamp(value, label) {
|
|
2269
|
+
const parsed = Date.parse(value);
|
|
2270
|
+
if (Number.isNaN(parsed)) {
|
|
2271
|
+
throw new Error(`${label} must be a valid ISO8601 timestamp`);
|
|
2272
|
+
}
|
|
2273
|
+
return new Date(parsed).toISOString();
|
|
2274
|
+
}
|
|
2275
|
+
function normalizeCronExpression(value) {
|
|
2276
|
+
if (typeof value !== "string") {
|
|
2277
|
+
throw new Error("timer cron must be a string");
|
|
2278
|
+
}
|
|
2279
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
2280
|
+
const fields = trimmed.split(" ");
|
|
2281
|
+
if (fields.length !== 5) {
|
|
2282
|
+
throw new Error("timer cron must use standard 5-field syntax");
|
|
2283
|
+
}
|
|
2284
|
+
for (const field of fields) {
|
|
2285
|
+
if (/[LW#@]/.test(field)) {
|
|
2286
|
+
throw new Error("timer cron does not support extensions like L, W, #, or @reboot");
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
parseCronExpression(trimmed);
|
|
2290
|
+
return trimmed;
|
|
2291
|
+
}
|
|
2292
|
+
function detectHostTimezone() {
|
|
2293
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
2294
|
+
}
|
|
2295
|
+
function assertValidTimeZone(timezone) {
|
|
2296
|
+
try {
|
|
2297
|
+
Intl.DateTimeFormat("en-US", { timeZone: timezone }).format(new Date());
|
|
2298
|
+
}
|
|
2299
|
+
catch {
|
|
2300
|
+
throw new Error(`invalid timezone: ${timezone}`);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function computeNextTimerFire(input) {
|
|
2304
|
+
const fromMs = Date.parse(input.fromIso);
|
|
2305
|
+
if (Number.isNaN(fromMs)) {
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
if (input.mode === "at") {
|
|
2309
|
+
if (!input.at) {
|
|
2310
|
+
return null;
|
|
2311
|
+
}
|
|
2312
|
+
const atMs = Date.parse(input.at);
|
|
2313
|
+
if (Number.isNaN(atMs)) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
if (input.lastFiredAt) {
|
|
2317
|
+
return null;
|
|
2318
|
+
}
|
|
2319
|
+
if (atMs <= fromMs) {
|
|
2320
|
+
return input.restartRecovery ? input.at : input.at;
|
|
2321
|
+
}
|
|
2322
|
+
return input.at;
|
|
2323
|
+
}
|
|
2324
|
+
if (input.mode === "every") {
|
|
2325
|
+
if (!input.intervalMs) {
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
if (!input.lastFiredAt) {
|
|
2329
|
+
return new Date(fromMs + input.intervalMs).toISOString();
|
|
2330
|
+
}
|
|
2331
|
+
const lastFiredMs = Date.parse(input.lastFiredAt);
|
|
2332
|
+
if (Number.isNaN(lastFiredMs)) {
|
|
2333
|
+
return new Date(fromMs + input.intervalMs).toISOString();
|
|
2334
|
+
}
|
|
2335
|
+
const base = input.restartRecovery ? fromMs : Math.max(fromMs, lastFiredMs);
|
|
2336
|
+
return new Date(base + input.intervalMs).toISOString();
|
|
2337
|
+
}
|
|
2338
|
+
if (!input.cronExpr) {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
return nextCronOccurrence(input.cronExpr, input.timezone, new Date(fromMs));
|
|
2342
|
+
}
|
|
2343
|
+
function parseCronExpression(expr) {
|
|
2344
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = expr.split(" ");
|
|
2345
|
+
return {
|
|
2346
|
+
minute: parseCronField(minute, 0, 59),
|
|
2347
|
+
hour: parseCronField(hour, 0, 23),
|
|
2348
|
+
dayOfMonth: parseCronField(dayOfMonth, 1, 31),
|
|
2349
|
+
month: parseCronField(month, 1, 12),
|
|
2350
|
+
dayOfWeek: parseCronField(dayOfWeek, 0, 7, true),
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
function parseCronField(field, min, max, normalizeSunday = false) {
|
|
2354
|
+
if (field === "*") {
|
|
2355
|
+
return { any: true, values: new Set() };
|
|
2356
|
+
}
|
|
2357
|
+
const values = new Set();
|
|
2358
|
+
for (const part of field.split(",")) {
|
|
2359
|
+
const [rangePart, stepPart] = part.split("/");
|
|
2360
|
+
const step = stepPart ? Number(stepPart) : 1;
|
|
2361
|
+
if (!Number.isInteger(step) || step <= 0) {
|
|
2362
|
+
throw new Error(`invalid cron field: ${field}`);
|
|
2363
|
+
}
|
|
2364
|
+
const [startRaw, endRaw] = rangePart === "*" ? [String(min), String(max)] : rangePart.split("-");
|
|
2365
|
+
const start = Number(startRaw);
|
|
2366
|
+
const end = endRaw != null ? Number(endRaw) : start;
|
|
2367
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || start > end) {
|
|
2368
|
+
throw new Error(`invalid cron field: ${field}`);
|
|
2369
|
+
}
|
|
2370
|
+
for (let value = start; value <= end; value += step) {
|
|
2371
|
+
values.add(normalizeSunday && value === 7 ? 0 : value);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return { any: false, values };
|
|
2375
|
+
}
|
|
2376
|
+
function nextCronOccurrence(expr, timezone, after) {
|
|
2377
|
+
const parsed = parseCronExpression(expr);
|
|
2378
|
+
let candidate = addMinutes(after, 1);
|
|
2379
|
+
candidate.setUTCSeconds(0, 0);
|
|
2380
|
+
for (let guard = 0; guard < 10_000; guard += 1) {
|
|
2381
|
+
const zoned = zonedDateParts(candidate, timezone);
|
|
2382
|
+
const nextMonth = nextAllowedValue(parsed.month, zoned.month, 1, 12);
|
|
2383
|
+
if (nextMonth == null) {
|
|
2384
|
+
candidate = utcDateForZoned(timezone, zoned.year + 1, firstAllowedValue(parsed.month, 1, 12), 1, 0, 0);
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
if (nextMonth !== zoned.month) {
|
|
2388
|
+
candidate = utcDateForZoned(timezone, zoned.year, nextMonth, 1, 0, 0);
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
if (!matchesCronDay(parsed, zoned)) {
|
|
2392
|
+
candidate = utcDateForZoned(timezone, zoned.year, zoned.month, zoned.day + 1, 0, 0);
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const nextHour = nextAllowedValue(parsed.hour, zoned.hour, 0, 23);
|
|
2396
|
+
if (nextHour == null) {
|
|
2397
|
+
candidate = utcDateForZoned(timezone, zoned.year, zoned.month, zoned.day + 1, firstAllowedValue(parsed.hour, 0, 23), 0);
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
if (nextHour !== zoned.hour) {
|
|
2401
|
+
candidate = utcDateForZoned(timezone, zoned.year, zoned.month, zoned.day, nextHour, 0);
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
const nextMinute = nextAllowedValue(parsed.minute, zoned.minute, 0, 59);
|
|
2405
|
+
if (nextMinute == null) {
|
|
2406
|
+
candidate = utcDateForZoned(timezone, zoned.year, zoned.month, zoned.day, zoned.hour + 1, firstAllowedValue(parsed.minute, 0, 59));
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (nextMinute !== zoned.minute) {
|
|
2410
|
+
candidate = utcDateForZoned(timezone, zoned.year, zoned.month, zoned.day, zoned.hour, nextMinute);
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (matchesCron(parsed, zoned)) {
|
|
2414
|
+
return candidate.toISOString();
|
|
2415
|
+
}
|
|
2416
|
+
candidate = addMinutes(candidate, 1);
|
|
2417
|
+
}
|
|
2418
|
+
return null;
|
|
2419
|
+
}
|
|
2420
|
+
function matchesCron(parsed, parts) {
|
|
2421
|
+
const monthMatches = parsed.month.any || parsed.month.values.has(parts.month);
|
|
2422
|
+
const hourMatches = parsed.hour.any || parsed.hour.values.has(parts.hour);
|
|
2423
|
+
const minuteMatches = parsed.minute.any || parsed.minute.values.has(parts.minute);
|
|
2424
|
+
if (!monthMatches || !hourMatches || !minuteMatches) {
|
|
2425
|
+
return false;
|
|
2426
|
+
}
|
|
2427
|
+
const dayOfMonthMatches = parsed.dayOfMonth.any || parsed.dayOfMonth.values.has(parts.day);
|
|
2428
|
+
const dayOfWeekMatches = parsed.dayOfWeek.any || parsed.dayOfWeek.values.has(parts.dayOfWeek);
|
|
2429
|
+
if (!parsed.dayOfMonth.any && !parsed.dayOfWeek.any) {
|
|
2430
|
+
return dayOfMonthMatches || dayOfWeekMatches;
|
|
2431
|
+
}
|
|
2432
|
+
return dayOfMonthMatches && dayOfWeekMatches;
|
|
2433
|
+
}
|
|
2434
|
+
function matchesCronDay(parsed, parts) {
|
|
2435
|
+
const dayOfMonthMatches = parsed.dayOfMonth.any || parsed.dayOfMonth.values.has(parts.day);
|
|
2436
|
+
const dayOfWeekMatches = parsed.dayOfWeek.any || parsed.dayOfWeek.values.has(parts.dayOfWeek);
|
|
2437
|
+
if (!parsed.dayOfMonth.any && !parsed.dayOfWeek.any) {
|
|
2438
|
+
return dayOfMonthMatches || dayOfWeekMatches;
|
|
2439
|
+
}
|
|
2440
|
+
return dayOfMonthMatches && dayOfWeekMatches;
|
|
2441
|
+
}
|
|
2442
|
+
function zonedDateParts(date, timezone) {
|
|
2443
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
2444
|
+
timeZone: timezone,
|
|
2445
|
+
year: "numeric",
|
|
2446
|
+
month: "2-digit",
|
|
2447
|
+
day: "2-digit",
|
|
2448
|
+
hour: "2-digit",
|
|
2449
|
+
minute: "2-digit",
|
|
2450
|
+
hourCycle: "h23",
|
|
2451
|
+
weekday: "short",
|
|
2452
|
+
});
|
|
2453
|
+
const entries = Object.fromEntries(formatter.formatToParts(date)
|
|
2454
|
+
.filter((part) => part.type !== "literal")
|
|
2455
|
+
.map((part) => [part.type, part.value]));
|
|
2456
|
+
const hour = Number(entries.hour);
|
|
2457
|
+
return {
|
|
2458
|
+
year: Number(entries.year),
|
|
2459
|
+
month: Number(entries.month),
|
|
2460
|
+
day: Number(entries.day),
|
|
2461
|
+
// Some ICU builds format midnight as 24:00 even for exact timestamps.
|
|
2462
|
+
// Normalize that representation so cron matching stays cross-platform.
|
|
2463
|
+
hour: hour === 24 ? 0 : hour,
|
|
2464
|
+
minute: Number(entries.minute),
|
|
2465
|
+
dayOfWeek: weekdayToNumber(entries.weekday),
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
function nextAllowedValue(field, current, min, max) {
|
|
2469
|
+
if (field.any) {
|
|
2470
|
+
return current;
|
|
2471
|
+
}
|
|
2472
|
+
for (let value = current; value <= max; value += 1) {
|
|
2473
|
+
if (field.values.has(value)) {
|
|
2474
|
+
return value;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
function firstAllowedValue(field, min, max) {
|
|
2480
|
+
if (field.any) {
|
|
2481
|
+
return min;
|
|
2482
|
+
}
|
|
2483
|
+
for (let value = min; value <= max; value += 1) {
|
|
2484
|
+
if (field.values.has(value)) {
|
|
2485
|
+
return value;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return min;
|
|
2489
|
+
}
|
|
2490
|
+
function addMinutes(date, minutes) {
|
|
2491
|
+
return new Date(date.getTime() + minutes * 60_000);
|
|
2492
|
+
}
|
|
2493
|
+
function utcDateForZoned(timezone, year, month, day, hour, minute) {
|
|
2494
|
+
const approxUtc = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
|
|
2495
|
+
const approx = new Date(approxUtc);
|
|
2496
|
+
const zoned = zonedDateParts(approx, timezone);
|
|
2497
|
+
const targetNaiveUtc = Date.UTC(year, month - 1, day, hour, minute, 0, 0);
|
|
2498
|
+
const zonedNaiveUtc = Date.UTC(zoned.year, zoned.month - 1, zoned.day, zoned.hour, zoned.minute, 0, 0);
|
|
2499
|
+
return new Date(approxUtc - (zonedNaiveUtc - targetNaiveUtc));
|
|
2500
|
+
}
|
|
2501
|
+
function weekdayToNumber(value) {
|
|
2502
|
+
switch (value) {
|
|
2503
|
+
case "Sun": return 0;
|
|
2504
|
+
case "Mon": return 1;
|
|
2505
|
+
case "Tue": return 2;
|
|
2506
|
+
case "Wed": return 3;
|
|
2507
|
+
case "Thu": return 4;
|
|
2508
|
+
case "Fri": return 5;
|
|
2509
|
+
case "Sat": return 6;
|
|
2510
|
+
default:
|
|
2511
|
+
throw new Error(`unsupported weekday: ${value}`);
|
|
2512
|
+
}
|
|
2513
|
+
}
|