@qilin-agent/schedule 0.0.0-stage → 3.1.4

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.
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Durable and model-facing Schedule value types.
3
+ * @module @qilin-agent/schedule
4
+ */
5
+ import type { SessionId } from '@qilin-agent/session/types';
6
+ import type { MessageId } from '@qilin-agent/llm/brand';
7
+ import type { Branded } from '@qilin-agent/brand';
8
+ /** Stable globally unique reminder identity. */
9
+ export type ScheduleId = Branded<'ScheduleId'>;
10
+ /** Durable one-shot reminder created from a positive delay. */
11
+ export interface AfterScheduleRecord {
12
+ /** Globally unique task identity. */
13
+ readonly id: ScheduleId;
14
+ /** Rule discriminator for a delayed one-shot reminder. */
15
+ readonly kind: 'after';
16
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
17
+ readonly title: string;
18
+ /** Trimmed reminder content supplied at creation. */
19
+ readonly prompt: string;
20
+ /** Positive safe-integer delay accepted at creation. */
21
+ readonly afterSeconds: number;
22
+ /** Four-digit-year RFC 3339 UTC target. */
23
+ readonly scheduledAt: string;
24
+ }
25
+ /** Durable one-shot reminder created from an absolute instant. */
26
+ export interface AtScheduleRecord {
27
+ /** Globally unique task identity. */
28
+ readonly id: ScheduleId;
29
+ /** Rule discriminator for an absolute one-shot reminder. */
30
+ readonly kind: 'at';
31
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
32
+ readonly title: string;
33
+ /** Trimmed reminder content supplied at creation. */
34
+ readonly prompt: string;
35
+ /** Four-digit-year RFC 3339 UTC target. */
36
+ readonly scheduledAt: string;
37
+ }
38
+ /** Durable fixed-rate reminder aligned to creation or its most recent interval edit. */
39
+ export interface EveryScheduleRecord {
40
+ /** Globally unique task identity. */
41
+ readonly id: ScheduleId;
42
+ /** Rule discriminator for a fixed-rate recurring reminder. */
43
+ readonly kind: 'every';
44
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
45
+ readonly title: string;
46
+ /** Trimmed reminder content supplied at creation. */
47
+ readonly prompt: string;
48
+ /** Fixed safe-integer interval, never below one minute. */
49
+ readonly everySeconds: number;
50
+ /** Next anchor-aligned occurrence while active, or final occurrence when inactive. */
51
+ readonly scheduledAt: string;
52
+ }
53
+ /** Durable daily wall-clock reminder; gaps skip a date and overlaps use the earlier instant. */
54
+ export interface DailyScheduleRecord {
55
+ /** Globally unique task identity. */
56
+ readonly id: ScheduleId;
57
+ /** Rule discriminator for a daily wall-clock reminder. */
58
+ readonly kind: 'daily';
59
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
60
+ readonly title: string;
61
+ /** Trimmed reminder content supplied at creation. */
62
+ readonly prompt: string;
63
+ /** Local time normalized to HH:mm:ss.SSS. */
64
+ readonly time: string;
65
+ /** Explicit IANA zone; equivalent timing edits retain the stored spelling. */
66
+ readonly timeZone: string;
67
+ /** Committed next UTC instant while active, or final occurrence when inactive. */
68
+ readonly scheduledAt: string;
69
+ }
70
+ /** Durable weekly wall-clock reminder; gaps skip a date and overlaps use the earlier instant. */
71
+ export interface WeeklyScheduleRecord {
72
+ /** Globally unique task identity. */
73
+ readonly id: ScheduleId;
74
+ /** Rule discriminator for a weekly wall-clock reminder. */
75
+ readonly kind: 'weekly';
76
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
77
+ readonly title: string;
78
+ /** Trimmed reminder content supplied at creation. */
79
+ readonly prompt: string;
80
+ /** Local time normalized to HH:mm:ss.SSS. */
81
+ readonly time: string;
82
+ /** Explicit canonical IANA zone; equivalent timing edits retain the stored spelling. */
83
+ readonly timeZone: string;
84
+ /** Unique ascending ISO weekdays, Monday 1 through Sunday 7. */
85
+ readonly weekdays: number[];
86
+ /** Committed next UTC instant while active, or final occurrence when inactive. */
87
+ readonly scheduledAt: string;
88
+ }
89
+ /** Durable cron wall-clock reminder; gaps skip a date and overlaps use the earlier instant. */
90
+ export interface CronScheduleRecord {
91
+ /** Globally unique task identity. */
92
+ readonly id: ScheduleId;
93
+ /** Rule discriminator for a five-field cron wall-clock reminder. */
94
+ readonly kind: 'cron';
95
+ /** Required stored task name; already trimmed, non-empty, and at most 120 characters. */
96
+ readonly title: string;
97
+ /** Trimmed reminder content supplied at creation. */
98
+ readonly prompt: string;
99
+ /** Canonical five-field cron expression: minute hour day-of-month month day-of-week. */
100
+ readonly expression: string;
101
+ /** Explicit IANA zone; equivalent timing edits retain the stored spelling. */
102
+ readonly timeZone: string;
103
+ /** Committed next UTC instant while active, or final occurrence when inactive. */
104
+ readonly scheduledAt: string;
105
+ }
106
+ /** Daily local-time selector accepted by creation and timing edits. */
107
+ export interface DailyInput {
108
+ /** Local HH:mm:ss time with optional one-to-three fractional digits. */
109
+ readonly time: string;
110
+ /** Explicit UTC or IANA Area/Location zone. */
111
+ readonly time_zone: string;
112
+ }
113
+ /** Weekly local-time and weekday selector accepted by creation and timing edits. */
114
+ export interface WeeklyInput {
115
+ /** Local HH:mm:ss time with optional one-to-three fractional digits. */
116
+ readonly time: string;
117
+ /** Explicit UTC or IANA Area/Location zone. */
118
+ readonly time_zone: string;
119
+ /** Non-empty ISO weekday set, Monday 1 through Sunday 7, without repetitions. */
120
+ readonly weekdays: number[];
121
+ }
122
+ /** Five-field cron selector accepted by creation and timing edits. */
123
+ export interface CronInput {
124
+ /** Five-field Vixie cron expression: minute hour day-of-month month day-of-week. */
125
+ readonly expression: string;
126
+ /** Explicit UTC or IANA Area/Location zone. */
127
+ readonly time_zone: string;
128
+ }
129
+ /** Structured local-calendar input accepted by creation and timing edits. */
130
+ export interface LocalAtInput {
131
+ /** Four-digit ISO calendar date. */
132
+ readonly date: string;
133
+ /** Local wall-clock time with optional one-to-three digit milliseconds. */
134
+ readonly time: string;
135
+ /** Explicit UTC or IANA Area/Location zone. */
136
+ readonly time_zone: string;
137
+ }
138
+ /** Absolute selector accepted by creation and timing edits. */
139
+ export type AtInput = string | LocalAtInput;
140
+ /** One-shot task variants. */
141
+ export type OneShotScheduleRecord = AfterScheduleRecord | AtScheduleRecord;
142
+ /** One-shot delay persisted by a version-1 Session event, without the later `title`. */
143
+ export interface LegacyAfterScheduleRecord extends Omit<AfterScheduleRecord, 'title'> {
144
+ /** Stored task name; absent in an event written before titles existed. */
145
+ readonly title?: string;
146
+ }
147
+ /** Absolute one-shot persisted by a version-1 Session event, without the later `title`. */
148
+ export interface LegacyAtScheduleRecord extends Omit<AtScheduleRecord, 'title'> {
149
+ /** Stored task name; absent in an event written before titles existed. */
150
+ readonly title?: string;
151
+ }
152
+ /** Fixed-rate reminder persisted by a version-1 Session event, without the later `title`. */
153
+ export interface LegacyEveryScheduleRecord extends Omit<EveryScheduleRecord, 'title'> {
154
+ /** Stored task name; absent in an event written before titles existed. */
155
+ readonly title?: string;
156
+ }
157
+ /**
158
+ * Frozen Session event and fold vocabulary; daily rules belong only to Host storage.
159
+ *
160
+ * A version-1 event written before titles existed persists no `title` member, so
161
+ * `after`, `at`, and `every` decode without one and stay readable. The Host task
162
+ * record requires the member and never persists a record without it.
163
+ */
164
+ export type LegacyScheduleRecord = LegacyAfterScheduleRecord | LegacyAtScheduleRecord | LegacyEveryScheduleRecord;
165
+ /** Recurring Host task variants. */
166
+ export type RecurringScheduleRecord = EveryScheduleRecord | DailyScheduleRecord | WeeklyScheduleRecord | CronScheduleRecord;
167
+ /** Reminder rule and target, stored with its original Session binding. */
168
+ export type ScheduleRecord = OneShotScheduleRecord | RecurringScheduleRecord;
169
+ /** Durable Session inbox delivery acknowledgment, not model execution completion. */
170
+ export interface ScheduleDeliveryReceipt {
171
+ /** Canonical UTC target of the delivered occurrence. */
172
+ readonly scheduledAt: string;
173
+ /** Canonical UTC time sampled after Session persistence acknowledged delivery. */
174
+ readonly deliveredAt: string;
175
+ /** Identity of the delivered message, shared by tasks in one recurring batch. */
176
+ readonly messageId: MessageId;
177
+ }
178
+ /** Saved inbox delivery with its immutable sent prompt when recorded by this Host. */
179
+ export interface ScheduleDeliveryRecord extends ScheduleDeliveryReceipt {
180
+ /** Prompt sent for this occurrence; unavailable for legacy receipts. */
181
+ readonly prompt?: string;
182
+ }
183
+ /** Browser-safe retained reminder with its original Session binding. */
184
+ export type ScheduleCatalogEntry = ScheduleRecord & {
185
+ /** Session receiving this reminder when it becomes due. */
186
+ readonly sessionId: SessionId;
187
+ /** Inactive reminders remain visible but never schedule another delivery. */
188
+ readonly status: 'active' | 'inactive';
189
+ /** Most recent durably acknowledged inbox delivery, when available. */
190
+ readonly lastDelivery?: ScheduleDeliveryReceipt;
191
+ };
192
+ /** Creates one durable reminder record. */
193
+ export interface ScheduleCreateChange {
194
+ readonly version: 1;
195
+ readonly operation: 'create';
196
+ readonly schedule: LegacyScheduleRecord;
197
+ }
198
+ /** Deletes one currently active reminder. */
199
+ export interface ScheduleDeleteChange {
200
+ readonly version: 1;
201
+ readonly operation: 'delete';
202
+ readonly id: ScheduleId;
203
+ }
204
+ /** Records that one active one-shot reminder entered the durable dispatch history. */
205
+ export interface OneShotScheduleDispatchChange {
206
+ readonly version: 1;
207
+ readonly operation: 'dispatch';
208
+ readonly id: ScheduleId;
209
+ }
210
+ /** Records one fixed-rate decision and advances directly past missed occurrences. */
211
+ export interface EveryScheduleDispatchChange {
212
+ readonly version: 1;
213
+ readonly operation: 'dispatch';
214
+ readonly id: ScheduleId;
215
+ /** Wall-clock decision time used to select the latest due occurrence. */
216
+ readonly acceptedAt: string;
217
+ }
218
+ /** Durable dispatch shapes supported by the current rule set. */
219
+ export type ScheduleDispatchChange = OneShotScheduleDispatchChange | EveryScheduleDispatchChange;
220
+ /** Strict version-1 durable Schedule mutation union. */
221
+ export type ScheduleChange = ScheduleCreateChange | ScheduleDeleteChange | ScheduleDispatchChange;
222
+ /** Current delivery timing derived from the durable record and wall clock. */
223
+ export type ScheduleState = 'scheduled' | 'overdue';
224
+ /** Host-driven delivery resumes the original Session when needed. */
225
+ export type ScheduleDeliveryMode = 'host';
226
+ /** Complete model-facing view of one active reminder. */
227
+ export type ScheduleView = ScheduleRecord & {
228
+ /** Whether the target remains in the future. */
229
+ readonly state: ScheduleState;
230
+ /** Reminder delivery never leaves the owning session. */
231
+ readonly deliveryMode: ScheduleDeliveryMode;
232
+ };
233
+ /** Stable error returned for an empty, over-long, or untrimmed reminder prompt or title. */
234
+ export interface InvalidPromptError {
235
+ readonly code: 'invalid_prompt';
236
+ readonly message: string;
237
+ }
238
+ /** Stable error returned for a missing, conflicting, or unsupported rule selector. */
239
+ export interface InvalidSelectorError {
240
+ readonly code: 'invalid_selector';
241
+ readonly message: string;
242
+ }
243
+ /** Stable error returned for an invalid rule or management argument. */
244
+ export interface InvalidRuleError {
245
+ readonly code: 'invalid_rule';
246
+ readonly message: string;
247
+ }
248
+ /** Stable error returned for an invalid or unsupported IANA time zone. */
249
+ export interface InvalidTimeZoneError {
250
+ readonly code: 'invalid_time_zone';
251
+ readonly message: string;
252
+ }
253
+ /** Stable error returned when an absolute target is not strictly future. */
254
+ export interface NotFutureError {
255
+ readonly code: 'not_future';
256
+ readonly message: string;
257
+ }
258
+ /** Stable error returned when the computed instant cannot use a four-digit UTC year. */
259
+ export interface TimeOutOfRangeError {
260
+ readonly code: 'time_out_of_range';
261
+ readonly message: string;
262
+ }
263
+ /** Stable error returned when a fixed-rate rule runs more often than supported. */
264
+ export interface FrequencyTooHighError {
265
+ readonly code: 'frequency_too_high';
266
+ readonly message: string;
267
+ }
268
+ /** Stable fallback that does not disclose an internal exception. */
269
+ export interface InternalScheduleError {
270
+ readonly code: 'internal_error';
271
+ readonly message: string;
272
+ }
273
+ /** Stable error returned when the target Session belongs to subagent routing, which never receives reminder delivery. */
274
+ export interface SubagentSessionError {
275
+ readonly code: 'subagent_session';
276
+ readonly message: string;
277
+ }
278
+ /** v1 Schedule management error union; new codes join as additional members. */
279
+ export type ScheduleToolError = InvalidPromptError | InvalidSelectorError | InvalidRuleError | InvalidTimeZoneError | NotFutureError | TimeOutOfRangeError | FrequencyTooHighError | SubagentSessionError | InternalScheduleError;
280
+ /** Canonical `schedule_create` value. */
281
+ export type ScheduleCreateValue = ScheduleView | ScheduleToolError;
282
+ /** Canonical `schedule_list` value. */
283
+ export type ScheduleListValue = ScheduleView[] | ScheduleToolError;
284
+ /** Successful `schedule_delete` value, including the non-mutating not-found result. */
285
+ export type ScheduleDeleteResult = {
286
+ readonly id: ScheduleId;
287
+ readonly deleted: true;
288
+ } | {
289
+ readonly id: ScheduleId;
290
+ readonly deleted: false;
291
+ readonly code: 'schedule_not_found';
292
+ };
293
+ /** Canonical `schedule_delete` value. */
294
+ export type ScheduleDeleteValue = ScheduleDeleteResult | ScheduleToolError;
295
+ declare module '@qilin-agent/workspace/types' {
296
+ interface SessionActivityKindMap {
297
+ /** A scheduled follow-up for this session is still active. */
298
+ schedule: true;
299
+ }
300
+ }
301
+ declare module '@qilin-agent/session/types' {
302
+ interface SessionEventMap {
303
+ /**
304
+ * Versioned Schedule mutation. The owning package validates the complete
305
+ * session-local transition stream before accepting a candidate event.
306
+ */
307
+ 'schedule/change': ScheduleChange;
308
+ }
309
+ }
310
+ /** Reminder creation selector, shared by the model consumer and Host service. */
311
+ export interface ScheduleCreateRequest {
312
+ /** Non-empty reminder text. */
313
+ prompt: string;
314
+ /** Required task name of at most 120 characters, non-empty after trimming; names the card, detail heading, and task lists. */
315
+ title: string;
316
+ /** Relative one-shot delay in seconds. */
317
+ after_seconds?: number;
318
+ /** Absolute one-shot target. */
319
+ at?: AtInput;
320
+ /** Fixed recurrence interval in seconds. */
321
+ every_seconds?: number;
322
+ /** Daily wall-clock time in an explicit IANA zone. */
323
+ daily?: DailyInput;
324
+ /** Weekly wall-clock time and explicit ISO weekday set in an IANA zone. */
325
+ weekly?: WeeklyInput;
326
+ /** Five-field cron expression evaluated in an explicit IANA zone. */
327
+ cron?: CronInput;
328
+ }
329
+ /** Session-scoped task list, without Agent activation. */
330
+ export interface ScheduleListRequest {
331
+ /** Original Session binding. */
332
+ sessionId: SessionId;
333
+ }
334
+ /** Delete request identifying a task within its original Session binding. */
335
+ export interface ScheduleDeleteRequest extends ScheduleListRequest {
336
+ /** Task to remove. */
337
+ id: ScheduleId;
338
+ }
339
+ /** Timing-only edit; it may select a different recurrence kind than the stored record, and one-shot targets use an absolute `at`. */
340
+ export type ScheduleTimingChange = {
341
+ readonly kind: 'at';
342
+ readonly at: AtInput;
343
+ } | {
344
+ readonly kind: 'every';
345
+ readonly every_seconds: number;
346
+ } | {
347
+ readonly kind: 'daily';
348
+ readonly daily: DailyInput;
349
+ } | {
350
+ readonly kind: 'weekly';
351
+ readonly weekly: WeeklyInput;
352
+ } | {
353
+ readonly kind: 'cron';
354
+ readonly cron: CronInput;
355
+ };
356
+ /** Replacement task name and instruction carried by one compare-and-update request. */
357
+ export interface ScheduleUpdateContent {
358
+ /** Task name of at most 120 characters, non-empty after trimming; omitted keeps the stored name. */
359
+ readonly title?: string;
360
+ /** Reminder instruction, non-empty after trimming; omitted keeps the stored instruction. */
361
+ readonly prompt?: string;
362
+ }
363
+ /** Compare-and-update request within the original Session binding. */
364
+ export interface ScheduleUpdateRequest extends ScheduleDeleteRequest, ScheduleUpdateContent {
365
+ /** Complete record observed when editing began, including the committed target. */
366
+ readonly expected: ScheduleRecord;
367
+ /** New timing; its kind may differ from the stored record's kind, and an omitted value keeps the committed target. */
368
+ readonly change?: ScheduleTimingChange;
369
+ }
370
+ /** Non-mutating compare-and-update outcome: unknown, inactive, or changed since the read. */
371
+ export interface ScheduleUpdateMiss {
372
+ readonly id: ScheduleId;
373
+ readonly updated: false;
374
+ readonly code: 'schedule_not_found' | 'schedule_ended' | 'schedule_conflict';
375
+ }
376
+ /** Successful current record, non-mutating lookup/conflict failure, or invalid name/instruction/timing. */
377
+ export type ScheduleUpdateResult = {
378
+ readonly id: ScheduleId;
379
+ readonly updated: boolean;
380
+ readonly record: ScheduleRecord;
381
+ } | ScheduleUpdateMiss | ScheduleToolError;
382
+ /** Canonical `schedule_update` value: the committed record as a view, or the non-mutating lookup/conflict result. */
383
+ export type ScheduleUpdateValue = ScheduleView | ScheduleUpdateMiss | ScheduleToolError;
384
+ /** Explicitly bounded saved-delivery query within one Session binding. */
385
+ export interface ScheduleDeliveryHistoryRequest extends ScheduleDeleteRequest {
386
+ /** Required safe-integer page size from 1 through 100. */
387
+ limit: number;
388
+ /** Message identity of the oldest entry in the previous page; excluded from this page. */
389
+ before?: MessageId;
390
+ }
391
+ /** Configured limits applied when appending one task's delivery receipt. */
392
+ export interface DeliveryRetentionBounds {
393
+ /** Retained window in days, measured back from the acknowledgment being appended. */
394
+ readonly days: number;
395
+ /** Maximum retained records per task; the newest survive. */
396
+ readonly records: number;
397
+ }
398
+ /** Newest-first saved deliveries, or a non-mutating task/cursor lookup failure. */
399
+ export type ScheduleDeliveryHistoryResult = {
400
+ readonly id: ScheduleId;
401
+ readonly records: ScheduleDeliveryRecord[];
402
+ /** Whether earlier delivery records may be unavailable; missing receipts are never reconstructed. */
403
+ readonly earlierRecordsUnavailable: boolean;
404
+ /** True only after an append removed saved records; absent legacy evidence is false. */
405
+ readonly earlierRecordsPruned: boolean;
406
+ /** Current Host retention configuration, also used by the delivery writer. */
407
+ readonly retention: DeliveryRetentionBounds;
408
+ /** Oldest returned message identity, present only when older saved records remain. */
409
+ readonly nextBefore?: MessageId;
410
+ } | {
411
+ readonly id: ScheduleId;
412
+ readonly code: 'schedule_not_found' | 'delivery_cursor_not_found';
413
+ };
414
+ declare module '@qilin-agent/kylin' {
415
+ interface Events {
416
+ /** Durable task set changed; clients refetch global task and Session-active catalogs.
417
+ * @mode emit
418
+ */
419
+ 'schedule/changed'(): void;
420
+ }
421
+ }
422
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Durable and model-facing Schedule value types.
3
+ * @module @qilin-agent/schedule
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,26 @@
1
+ import type { ScheduleRecord, ScheduleTimingChange, ScheduleUpdateContent, ScheduleUpdateResult } from './types.ts';
2
+ /**
3
+ * Title one update must either keep or replace.
4
+ *
5
+ * A decoded record always carries a stored title. A record value without a
6
+ * valid one is refused with the durable decode error instead of deriving a name.
7
+ * @param record - Current active Host record.
8
+ * @returns The stored title; a missing, blank, untrimmed, or over-long title throws ScheduleLogError.
9
+ */
10
+ export declare function retainedTitle(record: ScheduleRecord): string;
11
+ /**
12
+ * Compare the complete observed record and resolve one name, instruction, and timing update
13
+ * using a single queue-time sample.
14
+ *
15
+ * An omitted change, name, or instruction keeps the stored value. A supplied name or
16
+ * instruction never re-anchors the schedule on its own; an equivalent normalized timing
17
+ * change keeps the committed target too.
18
+ * @param current - Current active Host record.
19
+ * @param expected - Untrusted complete record observed by the caller.
20
+ * @param change - Strict timing selector, whose kind may differ from the current record's kind, or undefined to keep timing.
21
+ * @param now - Single wall-clock sample from the accepted FIFO slot.
22
+ * @param content - Untrusted replacement name and instruction; each omitted field keeps its stored value.
23
+ * @returns Current/new record or a bounded input/conflict result; unrelated failures throw.
24
+ */
25
+ export declare function resolveScheduleUpdate(current: ScheduleRecord, expected: ScheduleRecord, change: ScheduleTimingChange | undefined, now: number, content?: ScheduleUpdateContent): ScheduleUpdateResult;
26
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1,164 @@
1
+ /** Optimistic name, instruction, and timing updates of retained Host tasks, without Session mutations. */
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { canonicalizeCronExpression, canonicalizeTimeZone, createAtScheduleRecord, createCronScheduleRecord, createDailyScheduleRecord, createEveryScheduleRecord, createWeeklyScheduleRecord, decodeScheduleRecord, decodeStoredTitle, parseAtInput, parseCronInput, parseDailyInput, parseWeeklyInput, ScheduleInputError, ScheduleLogError, scheduleTitle, } from "./domain.js";
4
+ /** Name the exact selector property each timing kind must carry, or undefined for an unknown discriminant. */
5
+ function timingSelector(kind) {
6
+ switch (kind) {
7
+ case 'at': return 'at';
8
+ case 'every': return 'every_seconds';
9
+ case 'daily': return 'daily';
10
+ case 'weekly': return 'weekly';
11
+ case 'cron': return 'cron';
12
+ default: return undefined;
13
+ }
14
+ }
15
+ /**
16
+ * Validate the exact timing selector keys received over RPC.
17
+ * @param value - Untrusted timing change; the wire can carry a null, array, or foreign discriminant.
18
+ * @returns The same value narrowed to the closed timing-change union.
19
+ */
20
+ function validateChange(value) {
21
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
22
+ throw new ScheduleInputError('invalid_rule', 'Timing change must be an object with exactly one supported timing selector.');
23
+ }
24
+ const change = value;
25
+ const selector = timingSelector(change.kind);
26
+ const keys = Object.keys(change);
27
+ if (selector === undefined || keys.length !== 2 || !keys.includes('kind') || !keys.includes(selector)) {
28
+ throw new ScheduleInputError('invalid_rule', 'Timing change must contain exactly kind and its matching timing selector.');
29
+ }
30
+ return change;
31
+ }
32
+ /**
33
+ * Validate a replacement instruction received over RPC.
34
+ * @param prompt - Untrusted instruction value; the wire can carry a non-string.
35
+ * @returns The trimmed instruction; an invalid instruction throws ScheduleInputError.
36
+ */
37
+ function schedulePrompt(prompt) {
38
+ if (typeof prompt !== 'string' || prompt.trim().length === 0) {
39
+ throw new ScheduleInputError('invalid_prompt', 'prompt must be non-empty after trimming.');
40
+ }
41
+ return prompt.trim();
42
+ }
43
+ /**
44
+ * Title one update must either keep or replace.
45
+ *
46
+ * A decoded record always carries a stored title. A record value without a
47
+ * valid one is refused with the durable decode error instead of deriving a name.
48
+ * @param record - Current active Host record.
49
+ * @returns The stored title; a missing, blank, untrimmed, or over-long title throws ScheduleLogError.
50
+ */
51
+ export function retainedTitle(record) {
52
+ return decodeStoredTitle(record.title);
53
+ }
54
+ /**
55
+ * Apply a replacement name and instruction to a stored record without touching its rule.
56
+ * @param current - Current active record holding the committed target.
57
+ * @param title - Validated replacement or retained name.
58
+ * @param prompt - Validated replacement or retained instruction.
59
+ * @returns The same record when both are unchanged, otherwise a frozen copy with the new content.
60
+ */
61
+ function withContent(current, title, prompt) {
62
+ return title === current.title && prompt === current.prompt
63
+ ? current
64
+ : Object.freeze({ ...current, title, prompt });
65
+ }
66
+ /**
67
+ * Resolve the requested rule against the current record after the complete expected record has matched.
68
+ *
69
+ * The change kind may differ from the current record's kind; a change whose normalized
70
+ * timing equals the current rule keeps the committed target and only applies the supplied
71
+ * name and instruction, while any other change recomputes the target exactly as creation
72
+ * computes it from the same now.
73
+ */
74
+ function changedRecord(current, title, prompt, value, now) {
75
+ if (value === undefined)
76
+ return withContent(current, title, prompt);
77
+ const change = validateChange(value);
78
+ switch (change.kind) {
79
+ case 'at':
80
+ if ((current.kind === 'after' || current.kind === 'at')
81
+ && parseAtInput(change.at) === Date.parse(current.scheduledAt))
82
+ return withContent(current, title, prompt);
83
+ return createAtScheduleRecord(current.id, prompt, change.at, now, title);
84
+ case 'every':
85
+ if (current.kind === 'every' && change.every_seconds === current.everySeconds) {
86
+ return withContent(current, title, prompt);
87
+ }
88
+ return createEveryScheduleRecord(current.id, prompt, change.every_seconds, now, title);
89
+ case 'daily': {
90
+ if (current.kind === 'daily') {
91
+ const normalized = parseDailyInput(change.daily);
92
+ if (normalized.time === current.time
93
+ && normalized.timeZone === canonicalizeTimeZone(current.timeZone))
94
+ return withContent(current, title, prompt);
95
+ }
96
+ return createDailyScheduleRecord(current.id, prompt, change.daily, now, title);
97
+ }
98
+ case 'weekly': {
99
+ if (current.kind === 'weekly') {
100
+ const normalized = parseWeeklyInput(change.weekly);
101
+ if (normalized.time === current.time
102
+ && normalized.timeZone === canonicalizeTimeZone(current.timeZone)
103
+ && isDeepStrictEqual(normalized.weekdays, current.weekdays))
104
+ return withContent(current, title, prompt);
105
+ }
106
+ return createWeeklyScheduleRecord(current.id, prompt, change.weekly, now, title);
107
+ }
108
+ case 'cron': {
109
+ if (current.kind === 'cron') {
110
+ const normalized = parseCronInput(change.cron);
111
+ if (normalized.expression === canonicalizeCronExpression(current.expression)
112
+ && normalized.timeZone === canonicalizeTimeZone(current.timeZone))
113
+ return withContent(current, title, prompt);
114
+ }
115
+ return createCronScheduleRecord(current.id, prompt, change.cron, now, title);
116
+ }
117
+ /* v8 ignore next 4 -- validateChange rejects unknown discriminants before this closed union switch. */
118
+ default: {
119
+ const unreachable = change;
120
+ throw new Error(`Unknown validated timing change: ${String(unreachable)}`);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Compare the complete observed record and resolve one name, instruction, and timing update
126
+ * using a single queue-time sample.
127
+ *
128
+ * An omitted change, name, or instruction keeps the stored value. A supplied name or
129
+ * instruction never re-anchors the schedule on its own; an equivalent normalized timing
130
+ * change keeps the committed target too.
131
+ * @param current - Current active Host record.
132
+ * @param expected - Untrusted complete record observed by the caller.
133
+ * @param change - Strict timing selector, whose kind may differ from the current record's kind, or undefined to keep timing.
134
+ * @param now - Single wall-clock sample from the accepted FIFO slot.
135
+ * @param content - Untrusted replacement name and instruction; each omitted field keeps its stored value.
136
+ * @returns Current/new record or a bounded input/conflict result; unrelated failures throw.
137
+ */
138
+ export function resolveScheduleUpdate(current, expected, change, now, content = {}) {
139
+ let decoded;
140
+ try {
141
+ decoded = decodeScheduleRecord(expected);
142
+ }
143
+ catch (error) {
144
+ if (!(error instanceof ScheduleLogError))
145
+ throw error;
146
+ return { code: 'invalid_rule', message: 'expected must be a complete valid Schedule record.' };
147
+ }
148
+ if (!isDeepStrictEqual(current, decoded)) {
149
+ return { id: current.id, updated: false, code: 'schedule_conflict' };
150
+ }
151
+ try {
152
+ const retained = retainedTitle(current);
153
+ const title = content.title === undefined ? retained : scheduleTitle(content.title);
154
+ const prompt = content.prompt === undefined ? current.prompt : schedulePrompt(content.prompt);
155
+ const record = changedRecord(current, title, prompt, change, now);
156
+ return { id: current.id, updated: record !== current, record };
157
+ }
158
+ catch (error) {
159
+ if (!(error instanceof ScheduleInputError))
160
+ throw error;
161
+ return { code: error.code, message: error.message };
162
+ }
163
+ }
164
+ //# sourceMappingURL=update.js.map