@qihongmu/dsh-plugins-scheduled-task 0.1.1-alpha.2 → 0.1.1-alpha.3

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/cordis.patch.yml CHANGED
@@ -1,5 +1,3 @@
1
- # @qihongmu/dsh-plugins-scheduled-task: plugins-scheduled-task — merged into the profile patch list by `dsh plugin` once
2
- # this package is added to dsh.profile.bundles.
3
1
  - insert:
4
2
  - id: plugins-scheduled-task
5
3
  name: '@qihongmu/dsh-plugins-scheduled-task'
@@ -0,0 +1,5 @@
1
+ /* Generated face for the vendored host TYPERT manifest (typert.host.js).
2
+ * @module @qihongmu/dsh-plugins-scheduled-task/typert
3
+ */
4
+
5
+ export declare const TYPERT: unknown
@@ -0,0 +1,37 @@
1
+ /* Vendored by hand for this external plugin (the DSH generator cannot see
2
+ * external repos — see FEASIBILITY.md). Host-face TYPERT manifest: the
3
+ * invocation descriptors are the SAME objects the vendored client
4
+ * contribution carries (single source of truth; the loader/registry
5
+ * validation treats both faces identically for direct, non-lookup methods),
6
+ * and `model` carries the minimal reflection face the loader accepts.
7
+ * Kept in sync with host/src/types.ts and typert.remote-client.js.
8
+ */
9
+ import TYPERT_REMOTE from './typert.remote-client.js'
10
+
11
+ export const TYPERT = {
12
+ package: '@qihongmu/dsh-plugins-scheduled-task',
13
+ face: 'host',
14
+ schemas: [],
15
+ invocations: TYPERT_REMOTE.descriptors,
16
+ model: {
17
+ services: [
18
+ {
19
+ key: 'scheduledTasks',
20
+ exportName: 'ScheduledTaskService',
21
+ summary: 'Global scheduled tasks: durable registry, scheduler, delivery, Remote surface.',
22
+ tags: [],
23
+ members: [
24
+ { name: 'list', signature: '() => Promise<RemoteResult<ScheduledTaskView[]>>', kind: 'method' },
25
+ { name: 'create', signature: '(input: ScheduledTaskCreateInput) => Promise<RemoteResult<ScheduledTaskMutationResult>>', kind: 'method' },
26
+ { name: 'update', signature: '(id: ScheduledTaskId, input: ScheduledTaskUpdateInput) => Promise<RemoteResult<ScheduledTaskMutationResult>>', kind: 'method' },
27
+ { name: 'setStatus', signature: '(id: ScheduledTaskId, status: ScheduledTaskSettableStatus) => Promise<RemoteResult<ScheduledTaskMutationResult>>', kind: 'method' },
28
+ { name: 'delete', signature: '(id: ScheduledTaskId) => Promise<RemoteResult<ScheduledTaskDeleteResult>>', kind: 'method' },
29
+ { name: 'markRead', signature: '(id: ScheduledTaskId) => Promise<RemoteResult<null>>', kind: 'method' },
30
+ ],
31
+ types: [],
32
+ },
33
+ ],
34
+ events: [],
35
+ objects: [],
36
+ },
37
+ }
@@ -0,0 +1,304 @@
1
+ /* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit.
2
+ * Vendored by hand for this external plugin (the DSH generator cannot see external
3
+ * repos); kept in sync with host/src/types.ts. Shared sub-schemas are factored into
4
+ * constants so the rule/view union only lives in one place.
5
+ */
6
+ import { z } from 'zod'
7
+
8
+ const $id = z.intersection(z.string(), z.unknown())
9
+ const $status = z.union([z.literal("active"), z.literal("paused"), z.literal("completed")])
10
+ const $state = z.union([z.literal("scheduled"), z.literal("overdue"), z.literal("completed")])
11
+ const $model = z.object({
12
+ 'provider': z.string(),
13
+ 'model': z.string(),
14
+ })
15
+ const $errorCode = z.union([z.literal("invalid_title"), z.literal("invalid_prompt"), z.literal("invalid_selector"), z.literal("invalid_rule"), z.literal("invalid_time_zone"), z.literal("not_future"), z.literal("time_out_of_range"), z.literal("frequency_too_high"), z.literal("task_not_found"), z.literal("internal_error")])
16
+ const $failure = z.object({
17
+ 'ok': z.literal(false),
18
+ 'code': $errorCode,
19
+ 'message': z.string(),
20
+ })
21
+ const $rule = z.union([z.object({
22
+ 'kind': z.literal("after"),
23
+ 'afterSeconds': z.number(),
24
+ 'scheduledAt': z.string(),
25
+ }), z.object({
26
+ 'kind': z.literal("at"),
27
+ 'scheduledAt': z.string(),
28
+ }), z.object({
29
+ 'kind': z.literal("every"),
30
+ 'everySeconds': z.number(),
31
+ 'scheduledAt': z.string(),
32
+ }), z.object({
33
+ 'kind': z.literal("hourly"),
34
+ 'minute': z.number(),
35
+ 'scheduledAt': z.string(),
36
+ }), z.object({
37
+ 'kind': z.literal("daily"),
38
+ 'time': z.string(),
39
+ 'time_zone': z.string(),
40
+ 'scheduledAt': z.string(),
41
+ }), z.object({
42
+ 'kind': z.literal("weekly"),
43
+ 'weekdays': z.array(z.number()),
44
+ 'time': z.string(),
45
+ 'time_zone': z.string(),
46
+ 'scheduledAt': z.string(),
47
+ }), z.object({
48
+ 'kind': z.literal("monthly"),
49
+ 'dayOfMonth': z.number(),
50
+ 'time': z.string(),
51
+ 'time_zone': z.string(),
52
+ 'scheduledAt': z.string(),
53
+ })])
54
+ const $taskView = z.object({
55
+ 'id': $id,
56
+ 'title': z.string(),
57
+ 'prompt': z.string(),
58
+ 'rule': $rule,
59
+ 'status': $status,
60
+ 'sessionId': $id,
61
+ 'createdAt': z.string(),
62
+ 'lastRunAt': z.string().optional(),
63
+ 'nextRunAt': z.string().optional(),
64
+ 'lastError': z.object({
65
+ at: z.string(),
66
+ message: z.string().min(1),
67
+ }).optional(),
68
+ 'state': $state,
69
+ 'unread': z.boolean(),
70
+ 'workspaceId': z.string().optional(),
71
+ 'cwd': z.string().optional(),
72
+ 'model': $model.optional(),
73
+ 'confirmBeforeChange': z.boolean(),
74
+ })
75
+ const $mutationResult = z.union([z.object({
76
+ 'ok': z.literal(true),
77
+ 'task': $taskView,
78
+ }), $failure])
79
+ const $carryFields = {
80
+ 'prompt': z.string().optional(),
81
+ 'workspaceId': z.string().optional(),
82
+ 'cwd': z.string().optional(),
83
+ 'model': $model.optional(),
84
+ 'confirmBeforeChange': z.boolean().optional(),
85
+ }
86
+ const $selectorFields = {
87
+ 'after_seconds': z.number().optional(),
88
+ 'at': z.object({
89
+ 'date': z.string(),
90
+ 'time': z.string(),
91
+ 'time_zone': z.string(),
92
+ }).optional(),
93
+ 'every_seconds': z.number().optional(),
94
+ 'hourly': z.object({
95
+ 'minute': z.number(),
96
+ }).optional(),
97
+ 'daily': z.object({
98
+ 'time': z.string(),
99
+ 'time_zone': z.string(),
100
+ }).optional(),
101
+ 'weekly': z.object({
102
+ 'weekdays': z.array(z.number()),
103
+ 'time': z.string(),
104
+ 'time_zone': z.string(),
105
+ }).optional(),
106
+ 'monthly': z.object({
107
+ 'dayOfMonth': z.number(),
108
+ 'time': z.string(),
109
+ 'time_zone': z.string(),
110
+ }).optional(),
111
+ }
112
+
113
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_create_parameter_0$schema = z.object({
114
+ 'title': z.string(),
115
+ ...$carryFields,
116
+ ...$selectorFields,
117
+ })
118
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_create_result$schema = $mutationResult
119
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_delete_parameter_0$schema = $id
120
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_delete_result$schema = z.union([z.object({
121
+ 'ok': z.literal(true),
122
+ 'deleted': z.boolean(),
123
+ }), $failure])
124
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_list_result$schema = z.array($taskView)
125
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_markRead_parameter_0$schema = $id
126
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_markRead_result$schema = z.literal(null)
127
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_parameter_0$schema = $id
128
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_parameter_1$schema = z.union([z.literal("active"), z.literal("paused")])
129
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_result$schema = $mutationResult
130
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_parameter_0$schema = $id
131
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_parameter_1$schema = z.object({
132
+ 'title': z.string().optional(),
133
+ ...$carryFields,
134
+ ...$selectorFields,
135
+ })
136
+ const _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_result$schema = $mutationResult
137
+
138
+ export const TYPERT_REMOTE = {
139
+ package: '@qihongmu/dsh-plugins-scheduled-task',
140
+ descriptors: [
141
+ {
142
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/create',
143
+ service: 'scheduledTasks',
144
+ namespace: 'scheduledTasks',
145
+ method: 'create',
146
+ invocation: { kind: 'direct' },
147
+ parameters: [
148
+ {
149
+ name: 'input',
150
+ wire: 'input',
151
+ source: 'json',
152
+ codec: {
153
+ mode: 'strict',
154
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskCreateInput',
155
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_create_parameter_0$schema,
156
+ },
157
+ },
158
+ ],
159
+ result: {
160
+ mode: 'strict',
161
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskMutationResult',
162
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_create_result$schema,
163
+ },
164
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":166,"column":3},
165
+ },
166
+ {
167
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/delete',
168
+ service: 'scheduledTasks',
169
+ namespace: 'scheduledTasks',
170
+ method: 'delete',
171
+ invocation: { kind: 'direct' },
172
+ parameters: [
173
+ {
174
+ name: 'id',
175
+ wire: 'id',
176
+ source: 'json',
177
+ codec: {
178
+ mode: 'strict',
179
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskId',
180
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_delete_parameter_0$schema,
181
+ },
182
+ },
183
+ ],
184
+ result: {
185
+ mode: 'strict',
186
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskDeleteResult',
187
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_delete_result$schema,
188
+ },
189
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":244,"column":3},
190
+ },
191
+ {
192
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/list',
193
+ service: 'scheduledTasks',
194
+ namespace: 'scheduledTasks',
195
+ method: 'list',
196
+ invocation: { kind: 'direct' },
197
+ parameters: [
198
+ ],
199
+ result: {
200
+ mode: 'strict',
201
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/list:result',
202
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_list_result$schema,
203
+ },
204
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":151,"column":3},
205
+ },
206
+ {
207
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/markRead',
208
+ service: 'scheduledTasks',
209
+ namespace: 'scheduledTasks',
210
+ method: 'markRead',
211
+ invocation: { kind: 'direct' },
212
+ parameters: [
213
+ {
214
+ name: 'id',
215
+ wire: 'id',
216
+ source: 'json',
217
+ codec: {
218
+ mode: 'strict',
219
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskId',
220
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_markRead_parameter_0$schema,
221
+ },
222
+ },
223
+ ],
224
+ result: {
225
+ mode: 'strict',
226
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/markRead:result',
227
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_markRead_result$schema,
228
+ },
229
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":268,"column":3},
230
+ },
231
+ {
232
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/setStatus',
233
+ service: 'scheduledTasks',
234
+ namespace: 'scheduledTasks',
235
+ method: 'setStatus',
236
+ invocation: { kind: 'direct' },
237
+ parameters: [
238
+ {
239
+ name: 'id',
240
+ wire: 'id',
241
+ source: 'json',
242
+ codec: {
243
+ mode: 'strict',
244
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskId',
245
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_parameter_0$schema,
246
+ },
247
+ },
248
+ {
249
+ name: 'status',
250
+ wire: 'status',
251
+ source: 'json',
252
+ codec: {
253
+ mode: 'strict',
254
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskSettableStatus',
255
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_parameter_1$schema,
256
+ },
257
+ },
258
+ ],
259
+ result: {
260
+ mode: 'strict',
261
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskMutationResult',
262
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_setStatus_result$schema,
263
+ },
264
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":222,"column":3},
265
+ },
266
+ {
267
+ id: '@qihongmu/dsh-plugins-scheduled-task#scheduledTasks/update',
268
+ service: 'scheduledTasks',
269
+ namespace: 'scheduledTasks',
270
+ method: 'update',
271
+ invocation: { kind: 'direct' },
272
+ parameters: [
273
+ {
274
+ name: 'id',
275
+ wire: 'id',
276
+ source: 'json',
277
+ codec: {
278
+ mode: 'strict',
279
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskId',
280
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_parameter_0$schema,
281
+ },
282
+ },
283
+ {
284
+ name: 'input',
285
+ wire: 'input',
286
+ source: 'json',
287
+ codec: {
288
+ mode: 'strict',
289
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskUpdateInput',
290
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_parameter_1$schema,
291
+ },
292
+ },
293
+ ],
294
+ result: {
295
+ mode: 'strict',
296
+ typeSymbol: '@qihongmu/dsh-plugins-scheduled-task/types#ScheduledTaskMutationResult',
297
+ create: () => _deepseek_ai_dsh_scheduled_task_scheduledTasks_update_result$schema,
298
+ },
299
+ sourceLocation: {"file":"packages/scheduled-task/host/src/index.ts","line":200,"column":3},
300
+ },
301
+ ],
302
+ }
303
+
304
+ export default TYPERT_REMOTE
@@ -89,7 +89,7 @@ function translate(selector) {
89
89
  const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
90
90
  /** Convert `date` (YYYY-MM-DD) + `time` (HH:mm or HH:mm:ss) in `timeZone` to canonical epoch ms. */
91
91
  function atInstant(date, time, timeZone, now) {
92
- const record = createAtScheduleRecord(ScheduleId('task'), 'task', { date, time: normalizeTime(time), time_zone: timeZone }, now);
92
+ const record = createAtScheduleRecord(ScheduleId('task'), 'task', { date, time: normalizeTime(time), time_zone: timeZone }, now, 'task');
93
93
  return Date.parse(record.scheduledAt);
94
94
  }
95
95
  /**
@@ -218,18 +218,18 @@ export function buildRule(input, now) {
218
218
  return {
219
219
  kind: 'after',
220
220
  afterSeconds: input.after_seconds,
221
- scheduledAt: translate(() => createAfterScheduleRecord(ScheduleId('task'), 'task', input.after_seconds, now)),
221
+ scheduledAt: translate(() => createAfterScheduleRecord(ScheduleId('task'), 'task', input.after_seconds, now, 'task')),
222
222
  };
223
223
  }
224
224
  if (input.at !== undefined) {
225
225
  const at = validateAt(input.at);
226
- return { kind: 'at', scheduledAt: translate(() => createAtScheduleRecord(ScheduleId('task'), 'task', at, now)) };
226
+ return { kind: 'at', scheduledAt: translate(() => createAtScheduleRecord(ScheduleId('task'), 'task', at, now, 'task')) };
227
227
  }
228
228
  if (input.every_seconds !== undefined) {
229
229
  return {
230
230
  kind: 'every',
231
231
  everySeconds: input.every_seconds,
232
- scheduledAt: translate(() => createEveryScheduleRecord(ScheduleId('task'), 'task', input.every_seconds, now)),
232
+ scheduledAt: translate(() => createEveryScheduleRecord(ScheduleId('task'), 'task', input.every_seconds, now, 'task')),
233
233
  };
234
234
  }
235
235
  if (input.hourly !== undefined) {
@@ -6,6 +6,14 @@
6
6
  import { Service } from '@deepseek-ai/cordis';
7
7
  import type { Context } from '@deepseek-ai/cordis';
8
8
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
9
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
10
+ declare module '@deepseek-ai/dsh-llm' {
11
+ interface MessageSourceMap {
12
+ 'scheduled-task': {
13
+ kind: 'scheduled-task';
14
+ } & ContextFormed;
15
+ }
16
+ }
9
17
  import type { ScheduledTaskRecord } from './spec.ts';
10
18
  import type { ScheduledTaskCreateInput, ScheduledTaskDeleteResult, ScheduledTaskId as ScheduledTaskIdType, ScheduledTaskMutationResult, ScheduledTaskSettableStatus, ScheduledTaskUpdateInput, ScheduledTaskView } from './types.ts';
11
19
  export { ScheduledTaskError, ScheduledTaskId, advanceRule, buildRule, renderTaskFraming, scheduledTaskView, validatePrompt, validateTitle, } from './domain.ts';
@@ -29,6 +37,7 @@ export declare function nextDelayMs(records: Iterable<readonly [ScheduledTaskIdT
29
37
  * the single timer is a disposable projection of the earliest due target. Each
30
38
  * task's run Session is created lazily on first fire and kept live while the
31
39
  * process runs.
40
+ * @typert service scheduledTasks
32
41
  */
33
42
  export declare class ScheduledTaskService extends TypertRemoteService {
34
43
  /** Services required before tasks can be listed, mutated, or fired. */
@@ -36,6 +45,16 @@ export declare class ScheduledTaskService extends TypertRemoteService {
36
45
  private table?;
37
46
  private timer;
38
47
  private readonly handles;
48
+ /** The composition each retained handle carries: its mutable model
49
+ * selection (edits reach the live agent through this ref) and the preset
50
+ * id mounted at compose time (a changed default forces recomposition). */
51
+ private readonly compositions;
52
+ /** Tasks with a fire currently in flight. Composing a run agent takes
53
+ * seconds (a first preset standing mount), and the record only stops
54
+ * being due at the END of fireOne — a rearm in between (an edit, another
55
+ * task's mutation) would otherwise admit the SAME due task twice and
56
+ * compose two live agents for one session id. */
57
+ private readonly firing;
39
58
  /** Last task title pinned onto each run session (avoids re-rename spam). */
40
59
  private readonly appliedTitles;
41
60
  /** Run sessions already attached to their bound workspace (avoids re-attach churn). */
@@ -106,12 +125,33 @@ export declare class ScheduledTaskService extends TypertRemoteService {
106
125
  * re-pins only when the stored title changed since the last applied pin.
107
126
  */
108
127
  private pinSessionTitle;
128
+ /**
129
+ * Resolve the model selection for one run: the task's stored model when it
130
+ * names one, else the deployment's current default. Returns `undefined` when
131
+ * neither exists (legacy shape: the request waterfall picks a model).
132
+ */
133
+ private runSelection;
134
+ /**
135
+ * Resolve the default agent preset for one run. Its standing mount is what
136
+ * makes the standard toolset visible — an agent composed without it gets a
137
+ * bare scope with ZERO tools and answers in one text-only step (the "task
138
+ * finishes in seconds" failure). Returns `undefined` when the deployment
139
+ * provides no preset roster at all; a present-but-broken roster throws so
140
+ * the fire records the reason instead of silently running tool-less.
141
+ */
142
+ private composeRun;
109
143
  /**
110
144
  * Resolve the live run Agent for a task's CURRENT project. The durable
111
145
  * session identity is derived from the task id + cwd (`runSessionIdOf`), so
112
146
  * an edited project transparently starts a fresh conversation inside the new
113
147
  * workspace while returning to an earlier project resumes that project's
114
148
  * existing history. Returns the possibly-new sessionId for persistence.
149
+ *
150
+ * The agent is composed like a host-owned conversation: the default preset's
151
+ * toolset plus the bound model selection (the preset's prompt assembly reads
152
+ * `{{model}}` from it). Edits are picked up on the next fire: a model change
153
+ * reaches the live agent IN PLACE through the installed selection ref, a
154
+ * preset or project change recomposes via dispose + resume.
115
155
  */
116
156
  private ensureAgent;
117
157
  /** Cancel the timer and dispose every retained run Agent. */
@@ -39,6 +39,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
39
39
  };
40
40
  import { createHash, randomUUID } from 'node:crypto';
41
41
  import { Service } from '@deepseek-ai/cordis';
42
+ import { installModelSelection } from '@deepseek-ai/dsh-agent';
42
43
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
43
44
  import { SessionId } from '@deepseek-ai/dsh-session';
44
45
  import { WorkspaceId } from '@deepseek-ai/dsh-workspace';
@@ -97,6 +98,17 @@ function isMissingSessionError(error) {
97
98
  return /\bnot found\b|\bdoes not exist\b|\bno such session\b|\bunknown session\b/
98
99
  .test(renderThrown(error).toLowerCase());
99
100
  }
101
+ /**
102
+ * Whether two model selections name the same route; `undefined` on both sides
103
+ * counts as equal (no selection bound).
104
+ */
105
+ function selectionEquals(left, right) {
106
+ if (left === undefined || right === undefined)
107
+ return left === right;
108
+ return left.provider === right.provider
109
+ && left.model === right.model
110
+ && left.reasoningEffort === right.reasoningEffort;
111
+ }
100
112
  /**
101
113
  * Deterministic run-session identity for one task+project pair. Deriving the id
102
114
  * from the cwd makes a project edit migrate the task to a FRESH conversation in
@@ -132,6 +144,7 @@ function applyCarry(target, carry) {
132
144
  * the single timer is a disposable projection of the earliest due target. Each
133
145
  * task's run Session is created lazily on first fire and kept live while the
134
146
  * process runs.
147
+ * @typert service scheduledTasks
135
148
  */
136
149
  let ScheduledTaskService = (() => {
137
150
  let _classSuper = TypertRemoteService;
@@ -164,6 +177,16 @@ let ScheduledTaskService = (() => {
164
177
  table = __runInitializers(this, _instanceExtraInitializers);
165
178
  timer;
166
179
  handles = new Map();
180
+ /** The composition each retained handle carries: its mutable model
181
+ * selection (edits reach the live agent through this ref) and the preset
182
+ * id mounted at compose time (a changed default forces recomposition). */
183
+ compositions = new Map();
184
+ /** Tasks with a fire currently in flight. Composing a run agent takes
185
+ * seconds (a first preset standing mount), and the record only stops
186
+ * being due at the END of fireOne — a rearm in between (an edit, another
187
+ * task's mutation) would otherwise admit the SAME due task twice and
188
+ * compose two live agents for one session id. */
189
+ firing = new Set();
167
190
  /** Last task title pinned onto each run session (avoids re-rename spam). */
168
191
  appliedTitles = new Map();
169
192
  /** Run sessions already attached to their bound workspace (avoids re-attach churn). */
@@ -284,6 +307,7 @@ let ScheduledTaskService = (() => {
284
307
  this.handles.delete(id);
285
308
  await handle.dispose();
286
309
  }
310
+ this.compositions.delete(id);
287
311
  this.failures.delete(id);
288
312
  this.rearm();
289
313
  }
@@ -354,6 +378,9 @@ let ScheduledTaskService = (() => {
354
378
  }
355
379
  /** Queue one task run, advance its durable record, and flush its Session. */
356
380
  async fireOne(id, record, now) {
381
+ if (this.firing.has(id))
382
+ return;
383
+ this.firing.add(id);
357
384
  try {
358
385
  const { agent, sessionId } = await this.ensureAgent(record);
359
386
  // Pin the conversation title to the task title (re-pins only on change,
@@ -366,7 +393,7 @@ let ScheduledTaskService = (() => {
366
393
  setApprovalPolicy(agent.session, record.confirmBeforeChange ? 'ask' : 'never');
367
394
  agent.followup(createUserMessage({
368
395
  content: [{ type: 'text', text: renderTaskFraming(record) }],
369
- source: { kind: 'plugin', plugin: 'scheduled-task' },
396
+ source: { kind: 'scheduled-task' },
370
397
  }));
371
398
  const runAt = new Date(now).toISOString();
372
399
  const advanced = advanceRule(record.rule, now);
@@ -403,6 +430,9 @@ let ScheduledTaskService = (() => {
403
430
  this.ctx.logger.warn(`scheduled-task: could not persist failure of task "${id}": ${renderThrown(persistError)}`);
404
431
  }
405
432
  }
433
+ finally {
434
+ this.firing.delete(id);
435
+ }
406
436
  }
407
437
  /**
408
438
  * Attach the run session to the task's bound workspace so the conversation
@@ -447,26 +477,88 @@ let ScheduledTaskService = (() => {
447
477
  this.ctx.logger.warn(`scheduled-task: could not title session "${sessionId}": ${renderThrown(error)}`);
448
478
  }
449
479
  }
480
+ /**
481
+ * Resolve the model selection for one run: the task's stored model when it
482
+ * names one, else the deployment's current default. Returns `undefined` when
483
+ * neither exists (legacy shape: the request waterfall picks a model).
484
+ */
485
+ runSelection(record) {
486
+ if (record.model !== undefined)
487
+ return { provider: record.model.provider, model: record.model.model };
488
+ const defaults = this.ctx.get('agentDefaultModel');
489
+ return defaults?.currentSelection();
490
+ }
491
+ /**
492
+ * Resolve the default agent preset for one run. Its standing mount is what
493
+ * makes the standard toolset visible — an agent composed without it gets a
494
+ * bare scope with ZERO tools and answers in one text-only step (the "task
495
+ * finishes in seconds" failure). Returns `undefined` when the deployment
496
+ * provides no preset roster at all; a present-but-broken roster throws so
497
+ * the fire records the reason instead of silently running tool-less.
498
+ */
499
+ async composeRun() {
500
+ const presets = this.ctx.get('agentPresets');
501
+ if (presets === undefined)
502
+ return undefined;
503
+ const preset = await presets.resolve(undefined);
504
+ if (preset.broken !== undefined) {
505
+ throw new Error(`default agent preset "${preset.id}" failed to activate: ${preset.broken}`);
506
+ }
507
+ return {
508
+ presetId: preset.id,
509
+ mountPreset: async (agentCtx) => {
510
+ await presets.mount(agentCtx, preset.id);
511
+ },
512
+ };
513
+ }
450
514
  /**
451
515
  * Resolve the live run Agent for a task's CURRENT project. The durable
452
516
  * session identity is derived from the task id + cwd (`runSessionIdOf`), so
453
517
  * an edited project transparently starts a fresh conversation inside the new
454
518
  * workspace while returning to an earlier project resumes that project's
455
519
  * existing history. Returns the possibly-new sessionId for persistence.
520
+ *
521
+ * The agent is composed like a host-owned conversation: the default preset's
522
+ * toolset plus the bound model selection (the preset's prompt assembly reads
523
+ * `{{model}}` from it). Edits are picked up on the next fire: a model change
524
+ * reaches the live agent IN PLACE through the installed selection ref, a
525
+ * preset or project change recomposes via dispose + resume.
456
526
  */
457
527
  async ensureAgent(record) {
458
528
  const desiredCwd = record.cwd ?? process.cwd();
459
529
  const targetId = runSessionIdOf(record.id, desiredCwd);
460
- const agentOptions = record.model === undefined
530
+ const selection = this.runSelection(record);
531
+ const agentOptions = selection === undefined
461
532
  ? undefined
462
- : { provider: record.model.provider, model: record.model.model };
463
- // A retained run handle is reused only when it still matches the target.
533
+ : { provider: selection.provider, model: selection.model };
534
+ const selectionRef = { current: selection, assembled: undefined };
535
+ const composition = await this.composeRun();
536
+ const presetMeta = composition === undefined ? {} : { agentPreset: composition.presetId };
537
+ const setup = selection === undefined && composition === undefined
538
+ ? undefined
539
+ : async (agentCtx) => {
540
+ if (selection !== undefined)
541
+ installModelSelection(agentCtx, selectionRef);
542
+ if (composition !== undefined)
543
+ await composition.mountPreset(agentCtx);
544
+ };
545
+ const setupOption = setup === undefined ? {} : { setup };
546
+ // A retained run handle is reused only when it still matches the target
547
+ // AND carries the composition it was composed with.
464
548
  const retained = this.handles.get(record.id);
465
549
  if (retained !== undefined) {
466
- if (retained.agent.session.id === targetId && retained.agent.session.header.cwd === desiredCwd) {
550
+ const carried = this.compositions.get(record.id);
551
+ if (retained.agent.session.id === targetId
552
+ && retained.agent.session.header.cwd === desiredCwd
553
+ && carried !== undefined
554
+ && carried.presetId === composition?.presetId) {
555
+ if (!selectionEquals(carried.selectionRef.current, selection)) {
556
+ carried.selectionRef.current = selection;
557
+ }
467
558
  return { agent: retained.agent, sessionId: targetId };
468
559
  }
469
560
  this.handles.delete(record.id);
561
+ this.compositions.delete(record.id);
470
562
  await retained.dispose();
471
563
  }
472
564
  let handle;
@@ -474,6 +566,7 @@ let ScheduledTaskService = (() => {
474
566
  handle = await this.ctx.agents.resume({
475
567
  resumeSessionId: targetId,
476
568
  ...agentOptions === undefined ? {} : { agentOptions },
569
+ ...setupOption,
477
570
  });
478
571
  }
479
572
  catch (resumeError) {
@@ -482,11 +575,16 @@ let ScheduledTaskService = (() => {
482
575
  // Not persisted yet (first run for this task+project pair): create it in-project.
483
576
  handle = await this.ctx.agents.create({
484
577
  sessionId: targetId,
485
- meta: { cwd: desiredCwd },
578
+ meta: { cwd: desiredCwd, ...presetMeta },
486
579
  ...agentOptions === undefined ? {} : { agentOptions },
580
+ ...setupOption,
487
581
  });
488
582
  }
489
583
  this.handles.set(record.id, handle);
584
+ this.compositions.set(record.id, {
585
+ selectionRef,
586
+ ...composition === undefined ? {} : { presetId: composition.presetId },
587
+ });
490
588
  return { agent: handle.agent, sessionId: targetId };
491
589
  }
492
590
  /** Cancel the timer and dispose every retained run Agent. */
@@ -498,6 +596,7 @@ let ScheduledTaskService = (() => {
498
596
  }
499
597
  const handles = [...this.handles.values()];
500
598
  this.handles.clear();
599
+ this.compositions.clear();
501
600
  await Promise.allSettled(handles.map(handle => handle.dispose()));
502
601
  }
503
602
  };
package/lib/types/spec.js CHANGED
@@ -21,7 +21,8 @@ const atRule = z.object({
21
21
  });
22
22
  const everyRule = z.object({
23
23
  kind: z.literal('every'),
24
- everySeconds: z.number().int().min(300),
24
+ // Mirrors dsh MIN_EVERY_INTERVAL_SECONDS (lowered 300 → 60 in 0.1.7).
25
+ everySeconds: z.number().int().min(60),
25
26
  scheduledAt: z.string(),
26
27
  });
27
28
  const hourlyRule = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihongmu/dsh-plugins-scheduled-task",
3
- "version": "0.1.1-alpha.2",
3
+ "version": "0.1.1-alpha.3",
4
4
  "license": "MIT",
5
5
  "author": "Qihong <qihong.m.u+dev@gmail.com>",
6
6
  "repository": {
@@ -26,7 +26,12 @@
26
26
  "default": "./lib/types/types.js"
27
27
  },
28
28
  "./remote": {
29
- "types": "./lib/typert.remote-client.d.ts"
29
+ "types": "./lib/typert.remote-client.d.ts",
30
+ "default": "./lib/typert.remote-client.js"
31
+ },
32
+ "./typert": {
33
+ "types": "./lib/typert.host.d.ts",
34
+ "default": "./lib/typert.host.js"
30
35
  },
31
36
  "./src/*": "./src/*",
32
37
  "./package.json": "./package.json"
@@ -41,6 +46,8 @@
41
46
  "peerDependencies": {
42
47
  "@deepseek-ai/cordis": "*",
43
48
  "@deepseek-ai/dsh-agent": "*",
49
+ "@deepseek-ai/dsh-agent-default-model": "*",
50
+ "@deepseek-ai/dsh-agent-preset-registry": "*",
44
51
  "@deepseek-ai/dsh-brand": "*",
45
52
  "@deepseek-ai/dsh-invariants": "*",
46
53
  "@deepseek-ai/dsh-llm": "*",
@@ -56,6 +63,10 @@
56
63
  },
57
64
  "files": [
58
65
  "lib",
66
+ "lib/typert.host.js",
67
+ "lib/typert.host.d.ts",
68
+ "lib/typert.remote-client.js",
69
+ "lib/typert.remote-client.d.ts",
59
70
  "cordis.patch.yml",
60
71
  "!lib/**/*.map",
61
72
  "!lib/**/*.tsbuildinfo"