@goodandready/dsh-cron 0.3.22 → 0.3.23

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/lib/chat-start.js CHANGED
@@ -18,7 +18,12 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
18
18
  return;
19
19
  }
20
20
 
21
- const agents = ctx.agents;
21
+ let agents = null;
22
+ try {
23
+ agents = (ctx && typeof ctx.get === 'function' ? ctx.get('agents') : null) || (ctx && ctx.agents);
24
+ } catch {
25
+ agents = null;
26
+ }
22
27
  if (!agents || typeof agents.create !== 'function') {
23
28
  sendJson(res, 500, { ok: false, error: 'The agents service is unavailable in DSH' });
24
29
  return;
@@ -45,7 +50,13 @@ export async function chatStartHandler(ctx, req, res, parseJsonBody, sendJson, r
45
50
  (ctx.logger || console).debug?.('[dsh-cron] @deepseek-ai/dsh-session SessionId unavailable, using string fallback:', err?.message || err);
46
51
  }
47
52
 
48
- const defaultSel = ctx.get('agentDefaultModel')?.currentSelection?.();
53
+ let defaultSel = null;
54
+ try {
55
+ const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
56
+ defaultSel = adm?.currentSelection?.();
57
+ } catch {
58
+ defaultSel = null;
59
+ }
49
60
  const provider = body.provider || defaultSel?.provider;
50
61
  const model = body.model || defaultSel?.model;
51
62
 
package/lib/index.js CHANGED
@@ -1,3 +1,16 @@
1
+ import {
2
+ TaskSchema,
3
+ validateTaskRecord,
4
+ safeValidateTaskRecord,
5
+ validateTaskPatch,
6
+ } from './task-schema.js';
7
+
8
+ export {
9
+ TaskSchema,
10
+ validateTaskRecord,
11
+ safeValidateTaskRecord,
12
+ validateTaskPatch,
13
+ };
1
14
  import fs from 'node:fs';
2
15
  import path from 'node:path';
3
16
  import { bestEffort } from './best-effort.js';
@@ -95,7 +108,7 @@ export {
95
108
  };
96
109
 
97
110
  export const name = '@goodandready/dsh-cron';
98
- export const inject = ['tools', 'webServer', 'llm', 'agents', 'agentDefaultModel', 'credentials', 'workspaceRegistry'];
111
+ export const inject = ['tools', 'webServer'];
99
112
 
100
113
  import { executeCreateTask } from './task-create.js';
101
114
  import {
@@ -3,11 +3,18 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
3
3
  sendJson(res, 405, { ok: false, error: 'Method not allowed' });
4
4
  return;
5
5
  }
6
- const current = ctx.get('agentDefaultModel')?.currentSelection?.() ?? null;
6
+ let current = null;
7
+ try {
8
+ const adm = (ctx && typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) || (ctx && ctx.agentDefaultModel);
9
+ current = adm?.currentSelection?.() ?? null;
10
+ } catch {
11
+ current = null;
12
+ }
7
13
  let providers = [];
8
14
  try {
9
- if (ctx.llm && typeof ctx.llm.listProviders === 'function') {
10
- providers = (ctx.llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }));
15
+ const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
16
+ if (llm && typeof llm.listProviders === 'function') {
17
+ providers = (llm.listProviders() ?? []).map((p) => ({ id: p.id, name: p.name ?? p.id }));
11
18
  }
12
19
  } catch {
13
20
  providers = [];
@@ -22,8 +29,9 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
22
29
 
23
30
  try {
24
31
  let rows = [];
25
- if (ctx.llm && typeof ctx.llm.listModels === 'function') {
26
- rows = await ctx.llm.listModels(wanted);
32
+ const llm = (ctx && typeof ctx.get === 'function' ? ctx.get('llm') : null) || (ctx && ctx.llm);
33
+ if (llm && typeof llm.listModels === 'function') {
34
+ rows = await llm.listModels(wanted);
27
35
  }
28
36
  sendJson(res, 200, {
29
37
  current,
@@ -33,4 +41,4 @@ export async function getModelsHandler(ctx, req, res, sendJson) {
33
41
  } catch {
34
42
  sendJson(res, 200, { current, providers, models: [] });
35
43
  }
36
- }
44
+ }
package/lib/routes.js CHANGED
@@ -22,6 +22,30 @@ const LOCALES_JSON = readFileSync(new URL('./locales.json', import.meta.url), 'u
22
22
 
23
23
 
24
24
  export function registerRoutes(ctx, { store, scheduler, recommendations }) {
25
+ // Health probe route (#384)
26
+ ctx.effect(() => ctx.webServer.register({
27
+ kind: 'exact',
28
+ path: '/dsh-cron/health',
29
+ handler: (req, res) => {
30
+ if (req.method !== 'GET') {
31
+ res.writeHead(405, { 'content-type': 'text/plain; charset=utf-8' });
32
+ res.end('GET only');
33
+ return;
34
+ }
35
+ res.writeHead(200, {
36
+ 'content-type': 'application/json; charset=utf-8',
37
+ 'cache-control': 'no-store'
38
+ });
39
+ const tasks = typeof store?.list === 'function' ? store.list().length : 0;
40
+ res.end(JSON.stringify({
41
+ status: 'ok',
42
+ plugin: '@goodandready/dsh-cron',
43
+ tasks,
44
+ uptimeSec: Math.floor(process.uptime()),
45
+ }));
46
+ }
47
+ }), 'dsh-cron: /health');
48
+
25
49
  // Client locales route (#374)
26
50
  ctx.effect(() => ctx.webServer.register({
27
51
  kind: 'exact',
package/lib/runner.js CHANGED
@@ -79,6 +79,40 @@ export class SessionRunner {
79
79
  this.store = store;
80
80
  }
81
81
 
82
+ _getAgentsService() {
83
+ if (!this.ctx) return null;
84
+ try {
85
+ if (typeof this.ctx.get === 'function') {
86
+ const svc = this.ctx.get('agents');
87
+ if (svc) return svc;
88
+ }
89
+ } catch (_err) {
90
+ // service unavailable via ctx.get
91
+ }
92
+ try {
93
+ return this.ctx.agents || null;
94
+ } catch (_err) {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ _getSessionService() {
100
+ if (!this.ctx) return null;
101
+ try {
102
+ if (typeof this.ctx.get === 'function') {
103
+ const svc = this.ctx.get('session');
104
+ if (svc) return svc;
105
+ }
106
+ } catch (_err) {
107
+ // service unavailable via ctx.get
108
+ }
109
+ try {
110
+ return this.ctx.session || null;
111
+ } catch (_err) {
112
+ return null;
113
+ }
114
+ }
115
+
82
116
  async execute(task, options = {}) {
83
117
  const timeoutSeconds = Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) : 1800;
84
118
  const timeoutMs = timeoutSeconds * 1000;
@@ -234,12 +268,14 @@ export class SessionRunner {
234
268
  }
235
269
 
236
270
  // 2. Agent session, when the harness exposes one
237
- if (this.ctx && this.ctx.agents && typeof this.ctx.agents.create === 'function') {
271
+ const agentsSvc = this._getAgentsService();
272
+ if (agentsSvc && typeof agentsSvc.create === 'function') {
238
273
  return this._runAgentSession(effectiveTask, runtime, signal, options);
239
274
  }
240
275
 
241
276
  // 3. Legacy session service
242
- if (this.ctx && this.ctx.session && typeof this.ctx.session.create === 'function') {
277
+ const sessionSvc = this._getSessionService();
278
+ if (sessionSvc && typeof sessionSvc.create === 'function') {
243
279
  return this._runLegacySession(effectiveTask, options);
244
280
  }
245
281
 
@@ -270,12 +306,14 @@ export class SessionRunner {
270
306
  }
271
307
 
272
308
  // 2. Agent session, when the harness exposes one
273
- if (this.ctx && this.ctx.agents && typeof this.ctx.agents.create === 'function') {
309
+ const agentsSvc2 = this._getAgentsService();
310
+ if (agentsSvc2 && typeof agentsSvc2.create === 'function') {
274
311
  return this._runAgentSession(task, runtime, signal, options);
275
312
  }
276
313
 
277
314
  // 3. Legacy session service
278
- if (this.ctx && this.ctx.session && typeof this.ctx.session.create === 'function') {
315
+ const sessionSvc2 = this._getSessionService();
316
+ if (sessionSvc2 && typeof sessionSvc2.create === 'function') {
279
317
  return this._runLegacySession(task, options);
280
318
  }
281
319
 
@@ -484,9 +522,10 @@ export class SessionRunner {
484
522
 
485
523
  // If targetSessionId is set, attempt to resume the existing session (#143, #346, GH #9)
486
524
  let resumeFailure = null;
487
- if (prep.targetSessionId && this.ctx.agents && typeof this.ctx.agents.resume === 'function') {
525
+ const agents = this._getAgentsService();
526
+ if (prep.targetSessionId && agents && typeof agents.resume === 'function') {
488
527
  try {
489
- handle = await this.ctx.agents.resume({
528
+ handle = await agents.resume({
490
529
  sessionId: prep.targetSessionId,
491
530
  resumeSessionId: prep.targetSessionId,
492
531
  meta: {
@@ -508,7 +547,7 @@ export class SessionRunner {
508
547
  // Create new session if not resumed
509
548
  if (!handle) {
510
549
  try {
511
- handle = await this.ctx.agents.create({
550
+ handle = await agents.create({
512
551
  sessionId: prep.mintSessionId(),
513
552
  meta: {
514
553
  cwd: prep.cwd,
@@ -1,3 +1,4 @@
1
+ import { validateTaskRecord } from './task-schema.js';
1
2
  import { sanitizeSwarmConfig } from './swarm.js';
2
3
  import { sanitizeBranchRules } from './branching.js';
3
4
  import { sanitizePromptHistory } from './prompt-evolver.js';
@@ -132,7 +133,8 @@ export function executeCreateTask(store, scheduler, args) {
132
133
  throw new Error(dagCheck.error);
133
134
  }
134
135
 
135
- const task = store.set(taskData);
136
+ const validTask = validateTaskRecord(taskData);
137
+ const task = store.set(validTask);
136
138
  scheduler.scheduleTask(task);
137
139
  return {
138
140
  success: true,
@@ -0,0 +1,175 @@
1
+ import z from '@deepseek-ai/schemastery';
2
+ import { normalizeTaskType } from './runtimes.js';
3
+ import { validateTaskType, PATCHABLE_TASK_FIELDS } from './task-transfer.js';
4
+ import { CHANNEL_IDS } from './channels.js';
5
+
6
+ export const TaskSchema = z.object({
7
+ id: z.string(),
8
+ title: z.string().required(),
9
+ schedule: z.string().required(),
10
+ scheduleText: z.string(),
11
+ type: z.string().default('llm'),
12
+ prompt: z.string().default(''),
13
+ delivery: z.string().default('isolated'),
14
+ status: z.string().default('active'),
15
+ provider: z.string(),
16
+ model: z.string(),
17
+ fallbackProvider: z.string(),
18
+ fallbackModel: z.string(),
19
+ silentRule: z.string().default(''),
20
+ inspectOnFailure: z.boolean().default(false),
21
+ agentPreset: z.string().default(''),
22
+ targetSessionId: z.string().default(''),
23
+ targetSessionReset: z.string().default('never'),
24
+ sessionTitle: z.string().default(''),
25
+ timezone: z.string().default(''),
26
+ misfirePolicy: z.string().default('skip'),
27
+ maxRetries: z.number().default(0),
28
+ retryBackoffMs: z.number().default(30000),
29
+ permissionPreset: z.string().default('default'),
30
+ captureGitDiff: z.boolean(),
31
+ kanbanMode: z.string().default('none'),
32
+ channels: z.array(z.string()).default([]),
33
+ template: z.string().default(''),
34
+ env: z.any(),
35
+ cwd: z.string().default(''),
36
+ workspaceId: z.string().default(''),
37
+ worktree: z.boolean().default(false),
38
+ keepWorktree: z.boolean().default(false),
39
+ httpMethod: z.string().default('GET'),
40
+ httpUrl: z.string().default(''),
41
+ httpHeaders: z.any(),
42
+ httpBody: z.string().default(''),
43
+ sshProfileId: z.string().default(''),
44
+ sshTarget: z.string().default(''),
45
+ sshPort: z.number().default(0),
46
+ sshKeyPath: z.string().default(''),
47
+ dockerImage: z.string().default(''),
48
+ pythonPath: z.string().default(''),
49
+ nodePath: z.string().default(''),
50
+ skillName: z.string().default(''),
51
+ workflowName: z.string().default(''),
52
+ webhookEnabled: z.boolean().default(false),
53
+ webhookToken: z.string(),
54
+ webhookSecret: z.string(),
55
+ webhookDebounceMs: z.number(),
56
+ webhookTrigger: z.any(),
57
+ onSuccess: z.string().default(''),
58
+ onFailure: z.string().default(''),
59
+ heartbeatIntervalSeconds: z.number().default(0),
60
+ gracePeriodSeconds: z.number().default(300),
61
+ preflightType: z.string().default('none'),
62
+ preflightTarget: z.string().default(''),
63
+ preflightCommand: z.string(),
64
+ preflightScript: z.string(),
65
+ concurrencyGroup: z.string().default('default'),
66
+ priority: z.number().default(5),
67
+ selfHealingCommand: z.string().default(''),
68
+ autoDiagnose: z.boolean().default(false),
69
+ notifyTelegram: z.boolean(),
70
+ onlyOnFailure: z.boolean(),
71
+ timeoutSeconds: z.number().default(1800),
72
+ overlapPolicy: z.string().default('skip'),
73
+ oneShot: z.boolean().default(false),
74
+ targetTimestamp: z.number(),
75
+ nextRunAt: z.number(),
76
+ costLimitUsd: z.number(),
77
+ dailyCostLimitUsd: z.number(),
78
+ tokenLimit: z.number(),
79
+ pausedReason: z.string(),
80
+ validators: z.any(),
81
+ requireApproval: z.boolean().default(false),
82
+ fileWatch: z.any(),
83
+ dependsOn: z.array(z.string()).default([]),
84
+ joinPolicy: z.string().default('all_success'),
85
+ adaptive: z.any(),
86
+ adaptiveState: z.any(),
87
+ promptVersion: z.number(),
88
+ promptHistory: z.any(),
89
+ branchRules: z.any(),
90
+ elseTask: z.string(),
91
+ autoHeal: z.boolean(),
92
+ selfHealing: z.any(),
93
+ revisions: z.any(),
94
+ previousPrompt: z.string(),
95
+ previousScript: z.string(),
96
+ swarm: z.any(),
97
+ gitDiff: z.any(),
98
+ schemaVersion: z.number().default(1),
99
+ createdAt: z.number(),
100
+ updatedAt: z.number(),
101
+ });
102
+
103
+ export function validateTaskRecord(data) {
104
+ if (!data || typeof data !== 'object') {
105
+ throw new TypeError('Task record must be an object');
106
+ }
107
+ if (data.title !== undefined && typeof data.title !== 'string') {
108
+ throw new Error('Task schema validation failed: title must be a string');
109
+ }
110
+ const title = String(data.title || '').trim();
111
+ if (!title) {
112
+ throw new Error('Task title cannot be empty');
113
+ }
114
+ if (data.schedule !== undefined && typeof data.schedule !== 'string') {
115
+ throw new Error('Task schema validation failed: schedule must be a string');
116
+ }
117
+ const schedule = String(data.schedule || '').trim();
118
+ if (!schedule) {
119
+ throw new Error('Task schedule cannot be empty');
120
+ }
121
+
122
+ const cleanType = normalizeTaskType(data.type);
123
+ const typeError = validateTaskType(cleanType, data);
124
+ if (typeError) {
125
+ throw new Error(typeError);
126
+ }
127
+
128
+ let validated;
129
+ try {
130
+ validated = TaskSchema({
131
+ ...data,
132
+ title,
133
+ schedule,
134
+ type: cleanType,
135
+ });
136
+ } catch (err) {
137
+ throw new Error(`Task schema validation failed: ${err.message}`);
138
+ }
139
+
140
+ // Filter channels to known channel ids if present
141
+ if (Array.isArray(validated.channels)) {
142
+ validated.channels = validated.channels.filter((c) => CHANNEL_IDS.includes(c));
143
+ }
144
+
145
+ // Strip undefined keys
146
+ for (const k of Object.keys(validated)) {
147
+ if (validated[k] === undefined) {
148
+ delete validated[k];
149
+ }
150
+ }
151
+
152
+ return validated;
153
+ }
154
+
155
+ export function safeValidateTaskRecord(data) {
156
+ try {
157
+ const task = validateTaskRecord(data);
158
+ return { ok: true, task };
159
+ } catch (err) {
160
+ return { ok: false, error: err.message };
161
+ }
162
+ }
163
+
164
+ export function validateTaskPatch(patch) {
165
+ if (!patch || typeof patch !== 'object') {
166
+ return { ok: false, error: 'Task patch must be an object' };
167
+ }
168
+ const clean = {};
169
+ for (const [key, value] of Object.entries(patch)) {
170
+ if (PATCHABLE_TASK_FIELDS.includes(key)) {
171
+ clean[key] = value;
172
+ }
173
+ }
174
+ return { ok: true, patch: clean };
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
4
4
  "description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -22,7 +22,8 @@
22
22
  "test": "node --test test/*.test.mjs && node scripts/check-pack-size.mjs",
23
23
  "build:client": "node scripts/build-client.mjs",
24
24
  "pretest": "node scripts/build-client.mjs",
25
- "preflight": "node scripts/ci-preflight.mjs"
25
+ "preflight": "node scripts/ci-preflight.mjs",
26
+ "test:engine": "node scripts/test-engines.mjs"
26
27
  },
27
28
  "keywords": [
28
29
  "dsh",
@@ -51,6 +52,14 @@
51
52
  "@deepseek-ai/dsh-client-locale",
52
53
  "@deepseek-ai/dsh-client-ui-slots"
53
54
  ]
55
+ },
56
+ "compatibility": {
57
+ "dshReleases": {
58
+ "0.2.1-rc.2": "compatible",
59
+ "0.2.1-rc.3": "compatible",
60
+ "0.2.1-rc.4": "compatible"
61
+ },
62
+ "dsh": ">=0.2.1-rc.2 <0.2.2-0"
54
63
  }
55
64
  },
56
65
  "dependencies": {