@qilin-agent/schedule 3.1.7 → 3.1.9

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.
@@ -1,57 +1,159 @@
1
- /** Durable Host-wide Schedule tasks, independently of Session activation. */
2
1
  import { z } from 'zod';
3
2
  import { SessionId } from '@qilin-agent/session';
4
3
  import { MessageId } from '@qilin-agent/llm/brand';
5
- import type { ScheduleId } from './types.ts';
6
- /** Stored task binds one schedule to its original Session; absent status decodes as active. */
7
- export declare const scheduleTaskSchema: z.ZodObject<{
8
- sessionId: z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>;
9
- record: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord, unknown>>;
4
+ import type { ScheduleDeliveryRecord, ScheduleId, ScheduleTarget } from './types.ts';
5
+ /**
6
+ * Version-2 task schema accepting version-1 rows. New metadata is optional on disk. An explicit target
7
+ * ignores the former Session binding; a missing target uses that binding or a stable create-once destination.
8
+ * Normal decoding does not write. The next edit or delivery writes this current shape without `sessionId`.
9
+ */
10
+ export declare const scheduleTaskSchema: z.ZodPipe<z.ZodObject<{
11
+ record: z.ZodPipe<z.ZodUnknown, z.ZodTransform<import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord | import("./types.ts").RRuleScheduleRecord, unknown>>;
12
+ sessionId: z.ZodOptional<z.ZodUnknown>;
13
+ target: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
14
+ kind: z.ZodLiteral<"session">;
15
+ sessionId: z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>;
16
+ }, z.core.$strict>, z.ZodObject<{
17
+ kind: z.ZodLiteral<"new_once">;
18
+ sessionId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>>;
19
+ create: z.ZodDefault<z.ZodObject<{
20
+ cwd: z.ZodExactOptional<z.ZodString>;
21
+ agentPreset: z.ZodExactOptional<z.ZodString>;
22
+ }, z.core.$strict>>;
23
+ }, z.core.$strict>, z.ZodObject<{
24
+ kind: z.ZodLiteral<"new">;
25
+ create: z.ZodDefault<z.ZodObject<{
26
+ cwd: z.ZodExactOptional<z.ZodString>;
27
+ agentPreset: z.ZodExactOptional<z.ZodString>;
28
+ }, z.core.$strict>>;
29
+ }, z.core.$strict>], "kind">>;
30
+ involvedSessionIds: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>>>;
10
31
  status: z.ZodDefault<z.ZodEnum<{
11
32
  active: "active";
33
+ paused: "paused";
34
+ ended: "ended";
12
35
  inactive: "inactive";
13
36
  }>>;
37
+ createdAt: z.ZodOptional<z.ZodISODateTime>;
38
+ updatedAt: z.ZodOptional<z.ZodISODateTime>;
14
39
  lastDelivery: z.ZodOptional<z.ZodObject<{
15
40
  scheduledAt: z.ZodISODateTime;
16
41
  deliveredAt: z.ZodISODateTime;
17
42
  messageId: z.ZodPipe<z.ZodString, z.ZodTransform<MessageId, string>>;
43
+ targetSessionId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>>;
18
44
  }, z.core.$strict>>;
19
45
  deliveryHistory: z.ZodOptional<z.ZodObject<{
20
46
  records: z.ZodArray<z.ZodObject<{
21
47
  scheduledAt: z.ZodISODateTime;
22
48
  deliveredAt: z.ZodISODateTime;
23
49
  messageId: z.ZodPipe<z.ZodString, z.ZodTransform<MessageId, string>>;
24
- prompt: z.ZodOptional<z.ZodString>;
50
+ targetSessionId: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>>;
51
+ prompt: z.ZodExactOptional<z.ZodString>;
25
52
  }, z.core.$strict>>;
26
53
  earlierRecordsUnavailable: z.ZodBoolean;
27
- earlierRecordsPruned: z.ZodOptional<z.ZodBoolean>;
54
+ earlierRecordsPruned: z.ZodDefault<z.ZodBoolean>;
28
55
  }, z.core.$strict>>;
29
- }, z.core.$strict>;
30
- /** Persistent task value; absent history retains only its legacy last receipt without a read-time rewrite. */
56
+ pendingDelivery: z.ZodOptional<z.ZodObject<{
57
+ targetSessionId: z.ZodPipe<z.ZodString, z.ZodTransform<SessionId, string>>;
58
+ }, z.core.$strict>>;
59
+ }, z.core.$strict>, z.ZodTransform<{
60
+ deliveryHistory: {
61
+ records: ScheduleDeliveryRecord[];
62
+ earlierRecordsUnavailable: boolean;
63
+ earlierRecordsPruned: boolean;
64
+ };
65
+ lastDelivery?: {
66
+ scheduledAt: string;
67
+ deliveredAt: string;
68
+ messageId: MessageId;
69
+ targetSessionId?: SessionId | undefined;
70
+ } & {
71
+ targetSessionId: SessionId;
72
+ };
73
+ target: ScheduleTarget;
74
+ status: "active" | "paused" | "ended";
75
+ involvedSessionIds: SessionId[];
76
+ record: import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord | import("./types.ts").RRuleScheduleRecord;
77
+ createdAt?: string | undefined;
78
+ updatedAt?: string | undefined;
79
+ pendingDelivery?: {
80
+ targetSessionId: SessionId;
81
+ } | undefined;
82
+ }, {
83
+ record: import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord | import("./types.ts").RRuleScheduleRecord;
84
+ status: "active" | "paused" | "ended" | "inactive";
85
+ sessionId?: unknown;
86
+ target?: {
87
+ kind: "session";
88
+ sessionId: SessionId;
89
+ } | {
90
+ kind: "new_once";
91
+ create: {
92
+ cwd?: string;
93
+ agentPreset?: string;
94
+ };
95
+ sessionId?: SessionId | undefined;
96
+ } | {
97
+ kind: "new";
98
+ create: {
99
+ cwd?: string;
100
+ agentPreset?: string;
101
+ };
102
+ } | undefined;
103
+ involvedSessionIds?: SessionId[] | undefined;
104
+ createdAt?: string | undefined;
105
+ updatedAt?: string | undefined;
106
+ lastDelivery?: {
107
+ scheduledAt: string;
108
+ deliveredAt: string;
109
+ messageId: MessageId;
110
+ targetSessionId?: SessionId | undefined;
111
+ } | undefined;
112
+ deliveryHistory?: {
113
+ records: {
114
+ scheduledAt: string;
115
+ deliveredAt: string;
116
+ messageId: MessageId;
117
+ targetSessionId?: SessionId | undefined;
118
+ prompt?: string;
119
+ }[];
120
+ earlierRecordsUnavailable: boolean;
121
+ earlierRecordsPruned: boolean;
122
+ } | undefined;
123
+ pendingDelivery?: {
124
+ targetSessionId: SessionId;
125
+ } | undefined;
126
+ }>>;
127
+ /** Persistent task value; a delivery receipt does not record model execution completion. */
31
128
  export type ScheduleTask = z.infer<typeof scheduleTaskSchema>;
32
- /** Authoritative Schedule storage; malformed tasks reject opening the domain. */
129
+ /** Authoritative Host tasks retain their domain, backend routing, and envelope; v2 also reads v1 without startup writes. */
33
130
  export declare const scheduleDomain: {
34
131
  name: string;
35
132
  version: number;
133
+ compatibleVersions: number[];
36
134
  tables: {
37
135
  tasks: import("@qilin-agent/storage-domain").DomainTableSpec<ScheduleId, {
38
- sessionId: SessionId;
39
- record: import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord;
40
- status: "active" | "inactive";
136
+ deliveryHistory: {
137
+ records: ScheduleDeliveryRecord[];
138
+ earlierRecordsUnavailable: boolean;
139
+ earlierRecordsPruned: boolean;
140
+ };
41
141
  lastDelivery?: {
42
142
  scheduledAt: string;
43
143
  deliveredAt: string;
44
144
  messageId: MessageId;
45
- } | undefined;
46
- deliveryHistory?: {
47
- records: {
48
- scheduledAt: string;
49
- deliveredAt: string;
50
- messageId: MessageId;
51
- prompt?: string | undefined;
52
- }[];
53
- earlierRecordsUnavailable: boolean;
54
- earlierRecordsPruned?: boolean | undefined;
145
+ targetSessionId?: SessionId | undefined;
146
+ } & {
147
+ targetSessionId: SessionId;
148
+ };
149
+ target: ScheduleTarget;
150
+ status: "active" | "paused" | "ended";
151
+ involvedSessionIds: SessionId[];
152
+ record: import("./types.ts").AfterScheduleRecord | import("./types.ts").AtScheduleRecord | import("./types.ts").EveryScheduleRecord | import("./types.ts").DailyScheduleRecord | import("./types.ts").WeeklyScheduleRecord | import("./types.ts").CronScheduleRecord | import("./types.ts").RRuleScheduleRecord;
153
+ createdAt?: string | undefined;
154
+ updatedAt?: string | undefined;
155
+ pendingDelivery?: {
156
+ targetSessionId: SessionId;
55
157
  } | undefined;
56
158
  }>;
57
159
  };
@@ -1,4 +1,6 @@
1
1
  /** Durable Host-wide Schedule tasks, independently of Session activation. */
2
+ import { createHash } from 'node:crypto';
3
+ import { isDeepStrictEqual } from 'node:util';
2
4
  import { z } from 'zod';
3
5
  import { SessionId } from '@qilin-agent/session';
4
6
  import { MessageId } from '@qilin-agent/llm/brand';
@@ -13,42 +15,109 @@ const recordSchema = z.unknown().transform((value, context) => {
13
15
  }
14
16
  return z.NEVER;
15
17
  });
18
+ /** Canonical millisecond UTC instant with a four-digit year. */
16
19
  const instantSchema = z.iso.datetime({ precision: 3 }).refine(value => !value.startsWith('0000-'), {
17
20
  message: 'Expected a canonical four-digit-year UTC calendar instant',
18
21
  });
22
+ /** Identity of one delivered inbox message: non-empty, without surrounding whitespace. */
23
+ const messageIdSchema = z.string().min(1).refine(value => value.trim() === value).transform(MessageId);
24
+ /** Non-empty saved Session identity. */
25
+ const sessionIdSchema = z.string().min(1).transform(SessionId);
19
26
  const deliveryReceiptSchema = z.object({
20
27
  scheduledAt: instantSchema,
21
28
  deliveredAt: instantSchema,
22
- messageId: z.string().min(1).refine(value => value.trim() === value).transform(MessageId),
29
+ messageId: messageIdSchema,
30
+ targetSessionId: sessionIdSchema.optional(),
23
31
  }).strict();
24
32
  const deliveryHistorySchema = z.object({
25
- records: z.array(deliveryReceiptSchema.extend({ prompt: z.string().optional() }).strict()),
33
+ records: z.array(deliveryReceiptSchema.extend({ prompt: z.string().exactOptional() }).strict()),
26
34
  earlierRecordsUnavailable: z.boolean(),
27
- earlierRecordsPruned: z.boolean().optional(),
35
+ earlierRecordsPruned: z.boolean().default(false),
28
36
  }).strict().refine(history => new Set(history.records.map(record => record.messageId)).size === history.records.length, {
29
37
  message: 'Delivery history message identities must be unique within a task',
30
38
  });
31
- /** Stored task binds one schedule to its original Session; absent status decodes as active. */
39
+ const createSpecSchema = z.object({
40
+ cwd: z.string().min(1).exactOptional(),
41
+ agentPreset: z.string().min(1).exactOptional(),
42
+ }).strict();
43
+ const targetSchema = z.discriminatedUnion('kind', [
44
+ z.object({ kind: z.literal('session'), sessionId: sessionIdSchema }).strict(),
45
+ z.object({ kind: z.literal('new_once'), sessionId: sessionIdSchema.optional(), create: createSpecSchema.default({}) }).strict(),
46
+ z.object({ kind: z.literal('new'), create: createSpecSchema.default({}) }).strict(),
47
+ ]);
48
+ /** Stable reservation for a task without a saved destination; reads never allocate or persist a Session. */
49
+ function defaultSessionId(id) {
50
+ return SessionId(`session-schedule-${createHash('sha256').update(id).digest('hex')}`);
51
+ }
52
+ /**
53
+ * Version-2 task schema accepting version-1 rows. New metadata is optional on disk. An explicit target
54
+ * ignores the former Session binding; a missing target uses that binding or a stable create-once destination.
55
+ * Normal decoding does not write. The next edit or delivery writes this current shape without `sessionId`.
56
+ */
32
57
  export const scheduleTaskSchema = z.object({
33
- sessionId: z.string().min(1).transform(SessionId),
34
58
  record: recordSchema,
35
- status: z.enum(['active', 'inactive']).default('active'),
59
+ sessionId: z.unknown().optional(),
60
+ target: targetSchema.optional(),
61
+ involvedSessionIds: z.array(sessionIdSchema).optional(),
62
+ status: z.enum(['active', 'paused', 'ended', 'inactive']).default('active'),
63
+ createdAt: instantSchema.optional(),
64
+ updatedAt: instantSchema.optional(),
36
65
  lastDelivery: deliveryReceiptSchema.optional(),
37
66
  deliveryHistory: deliveryHistorySchema.optional(),
38
- }).strict().refine((task) => {
39
- if (task.deliveryHistory === undefined)
40
- return true;
67
+ pendingDelivery: z.object({ targetSessionId: sessionIdSchema }).strict().optional(),
68
+ }).strict().transform(({ sessionId, target: savedTarget, lastDelivery, deliveryHistory, ...task }, context) => {
69
+ const binding = savedTarget === undefined && sessionId !== undefined ? sessionIdSchema.safeParse(sessionId) : undefined;
70
+ if (binding?.success === false) {
71
+ context.addIssue({ code: 'custom', path: ['sessionId'], message: 'A missing target requires a valid stored Session binding' });
72
+ return z.NEVER;
73
+ }
74
+ const target = savedTarget === undefined
75
+ ? binding?.success === true
76
+ ? { kind: 'session', sessionId: binding.data }
77
+ : { kind: 'new_once', sessionId: defaultSessionId(task.record.id), create: {} }
78
+ : savedTarget.kind === 'new_once'
79
+ ? { ...savedTarget, sessionId: savedTarget.sessionId ?? defaultSessionId(task.record.id) }
80
+ : savedTarget;
81
+ const hasReceiptTarget = savedTarget === undefined
82
+ ? binding?.success === true
83
+ : savedTarget.kind !== 'new' && savedTarget.sessionId !== undefined;
84
+ const receipts = [lastDelivery, ...(deliveryHistory?.records ?? [])];
85
+ if (!hasReceiptTarget && receipts.some(value => value !== undefined && value.targetSessionId === undefined)) {
86
+ context.addIssue({ code: 'custom', message: 'A saved delivery without an existing destination must name its target Session' });
87
+ return z.NEVER;
88
+ }
89
+ const receipt = (value) => {
90
+ if (value.targetSessionId !== undefined)
91
+ return { ...value, targetSessionId: value.targetSessionId };
92
+ /* v8 ignore next -- the receipt-destination validation above rejects this combination before normalization. */
93
+ if (target.kind === 'new')
94
+ throw new Error('A delivery to a fresh Session must name its target Session');
95
+ return { ...value, targetSessionId: target.sessionId };
96
+ };
97
+ const last = lastDelivery === undefined ? undefined : receipt(lastDelivery);
98
+ const history = deliveryHistory === undefined
99
+ ? { records: last === undefined ? [] : [last], earlierRecordsUnavailable: true, earlierRecordsPruned: false }
100
+ : { ...deliveryHistory, records: deliveryHistory.records.map(value => receipt(value)) };
101
+ return {
102
+ ...task,
103
+ target,
104
+ status: task.status === 'inactive' ? 'ended' : task.status,
105
+ involvedSessionIds: task.involvedSessionIds ?? (target.kind === 'new' ? [] : [target.sessionId]),
106
+ ...(last === undefined ? {} : { lastDelivery: last }),
107
+ deliveryHistory: history,
108
+ };
109
+ }).refine((task) => {
41
110
  const latest = task.deliveryHistory.records.at(-1);
42
111
  if (latest === undefined)
43
112
  return task.lastDelivery === undefined;
44
- return task.lastDelivery !== undefined
45
- && latest.scheduledAt === task.lastDelivery.scheduledAt
46
- && latest.deliveredAt === task.lastDelivery.deliveredAt
47
- && latest.messageId === task.lastDelivery.messageId;
48
- }, { message: 'Last delivery must match the latest saved delivery receipt' });
49
- /** Authoritative Schedule storage; malformed tasks reject opening the domain. */
113
+ const { prompt: _prompt, ...receipt } = latest;
114
+ return isDeepStrictEqual(receipt, task.lastDelivery);
115
+ }, {
116
+ message: 'Last delivery must match the latest saved delivery receipt',
117
+ });
118
+ /** Authoritative Host tasks retain their domain, backend routing, and envelope; v2 also reads v1 without startup writes. */
50
119
  export const scheduleDomain = defineDomain({
51
- name: 'schedule', version: 1,
120
+ name: 'schedule', version: 2, compatibleVersions: [1],
52
121
  tables: { tasks: domainTable(scheduleTaskSchema) },
53
122
  });
54
123
  //# sourceMappingURL=storage.js.map