@goodandready/dsh-cron 0.3.6 → 0.3.8

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/README.md CHANGED
@@ -607,6 +607,12 @@ Save LLM tokens and API quotas on watcher tasks by dynamically slowing down exec
607
607
  - Multiplies interval by `backoffFactor` up to `maxInterval` after `idleRunsThreshold` silent or unchanged runs.
608
608
  - Instantly resets to `minInterval` as soon as real changes or non-silent output are detected.
609
609
 
610
+ ### 🔄 Session Resumption & GUI Session Naming (#346, GH #9, Added in v0.3.8)
611
+
612
+ - **Universal Host Resumption (`resumeSessionId`)**: DeepSeek Harness host `0.2.0-rc.2+` specifies `readonly resumeSessionId: SessionId` on `agents.resume()`. The runner now passes both `resumeSessionId` and `sessionId`, guaranteeing seamless session resumption across all DSH core versions without falling back to recreating sessions that already exist.
613
+ - **Enhanced Diagnostics on Failure**: When an existing session cannot be resumed, `dsh-cron` logs an actionable warning and, if recreating the session fails, surfaces the underlying resume error in the task run output.
614
+ - **Automatic GUI Session Naming (`sessionTitle`)**: Executed or resumed sessions now integrate with the host's `sessionTitle` service. Tasks can specify an explicit `sessionTitle` (or begin `task.title` with `[...]`) to automatically label the session in the DeepSeek Harness navigation sidebar instead of leaving it untitled (`cron-exec-...`).
615
+
610
616
  ### 📺 Real-Time Streaming Logs via SSE (#336)
611
617
 
612
618
  Connect to `GET /dsh-cron/tasks/:id/stream` using Server-Sent Events (SSE) to receive live stdout/stderr chunks and execution completion events with an automatic 500-line ring buffer.
package/lib/index.js CHANGED
@@ -305,6 +305,10 @@ export function apply(ctx, rawConfig) {
305
305
  path: '/dsh-cron/settings',
306
306
  handler: async (req, res) => {
307
307
  try {
308
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
309
+ const apiToken = settings.apiToken;
310
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
311
+
308
312
  if (req.method === 'GET') {
309
313
  const unwrapped = {
310
314
  ...unwrapConfig(getContextConfig(ctx, rawConfig)),
package/lib/metrics.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { rejectCrossOrigin } from './http-utils.js';
1
2
  /**
2
3
  * Prometheus exposition of the scheduler (#53).
3
4
  *
@@ -108,6 +109,10 @@ export function createMetricsHandler({ store, scheduler }) {
108
109
  res.end('Method not allowed' + NL);
109
110
  return;
110
111
  }
112
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
113
+ const apiToken = settings.apiToken;
114
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
115
+
111
116
  const body = renderMetrics({
112
117
  tasks: store.list({ status: 'all' }),
113
118
  stats: store.getAggregatedStats(),
package/lib/routes.js CHANGED
@@ -88,6 +88,9 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
88
88
  kind: 'exact',
89
89
  path: '/dsh-cron/models',
90
90
  handler: async (req, res) => {
91
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
92
+ const apiToken = settings.apiToken;
93
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
91
94
  await getModelsHandler(ctx, req, res, sendJson);
92
95
  }
93
96
  }), 'dsh-cron: /models');
@@ -108,6 +111,10 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
108
111
  path: '/dsh-cron/analytics',
109
112
  handler: async (req, res) => {
110
113
  try {
114
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
115
+ const apiToken = settings.apiToken;
116
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
117
+
111
118
  const url = new URL(req.url, 'http://127.0.0.1');
112
119
  const path = url.pathname;
113
120
 
@@ -211,6 +218,8 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
211
218
  return;
212
219
  }
213
220
  const settings = store.getSettings ? store.getSettings() : {};
221
+ const apiToken = settings.apiToken;
222
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
214
223
  const categories = recipesByCategory();
215
224
  const recipes = listVerifiedRecipes(settings);
216
225
  sendJson(res, 200, { ok: true, categories, recipes });
@@ -230,6 +239,10 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
230
239
  sendJson(res, 405, { ok: false, error: 'Method not allowed' });
231
240
  return;
232
241
  }
242
+ const settings = store.getSettings ? store.getSettings() : {};
243
+ const apiToken = settings.apiToken;
244
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
245
+
233
246
  const tasks = store.list({ status: 'active' });
234
247
  const lastRuns = tasks.map((t) => t.lastRunAt || 0);
235
248
  sendJson(res, 200, {
@@ -319,7 +332,9 @@ export function registerRoutes(ctx, { store, scheduler, recommendations }) {
319
332
  * Handler for chain step retries (#319)
320
333
  */
321
334
  export async function handleChainRoutes(store, scheduler, req, res) {
322
- if (req.method === 'POST' && rejectCrossOrigin(req, res)) return;
335
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
336
+ const apiToken = settings.apiToken;
337
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
323
338
  const url = new URL(req.url, 'http://localhost');
324
339
 
325
340
  // GET /dsh-cron/chains/:chainRunId (#334)
@@ -374,6 +389,10 @@ export async function handleRunArtifactRoutes(store, req, res) {
374
389
  sendJson(res, 405, { ok: false, error: 'Method not allowed' });
375
390
  return;
376
391
  }
392
+ const settings = store && typeof store.getSettings === 'function' ? store.getSettings() : {};
393
+ const apiToken = settings.apiToken;
394
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
395
+
377
396
  const [, runId, artifactId, action] = m;
378
397
  if (!artifactId) {
379
398
  const artifacts = store.getArtifactsForRun(runId);
package/lib/runner.js CHANGED
@@ -475,11 +475,13 @@ export class SessionRunner {
475
475
  }
476
476
  } : {};
477
477
 
478
- // If targetSessionId is set, attempt to resume the existing session (#143)
478
+ // If targetSessionId is set, attempt to resume the existing session (#143, #346, GH #9)
479
+ let resumeFailure = null;
479
480
  if (prep.targetSessionId && this.ctx.agents && typeof this.ctx.agents.resume === 'function') {
480
481
  try {
481
482
  handle = await this.ctx.agents.resume({
482
483
  sessionId: prep.targetSessionId,
484
+ resumeSessionId: prep.targetSessionId,
483
485
  meta: {
484
486
  cwd: prep.cwd,
485
487
  ...(preset ? { agentPreset: preset.id } : {})
@@ -490,27 +492,36 @@ export class SessionRunner {
490
492
  isResumed = true;
491
493
  logger.info(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
492
494
  } catch (resumeErr) {
493
- logger.info(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), creating new persistent session`);
495
+ resumeFailure = resumeErr;
496
+ logger.warn(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), fallback creating new session`);
494
497
  handle = null;
495
498
  }
496
499
  }
497
500
 
498
501
  // Create new session if not resumed
499
502
  if (!handle) {
500
- handle = await this.ctx.agents.create({
501
- sessionId: prep.mintSessionId(),
502
- meta: {
503
- cwd: prep.cwd,
504
- ephemeral: !prep.targetSessionId,
505
- internal: !prep.targetSessionId,
506
- ...(preset ? { agentPreset: preset.id } : {})
507
- },
508
- agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
509
- ...presetSetup,
510
- });
503
+ try {
504
+ handle = await this.ctx.agents.create({
505
+ sessionId: prep.mintSessionId(),
506
+ meta: {
507
+ cwd: prep.cwd,
508
+ ephemeral: !prep.targetSessionId,
509
+ internal: !prep.targetSessionId,
510
+ ...(preset ? { agentPreset: preset.id } : {})
511
+ },
512
+ agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
513
+ ...presetSetup,
514
+ });
515
+ } catch (createErr) {
516
+ if (resumeFailure) {
517
+ throw new Error(`could not resume session "${prep.targetSessionId}" (${resumeFailure.message}): ${createErr.message}`);
518
+ }
519
+ throw createErr;
520
+ }
511
521
  }
512
522
 
513
523
  this._applyPermissionPreset(task, handle);
524
+ this._nameSession(task, handle, isResumed);
514
525
  await prep.raceAbort(handle.agent.whenIdle());
515
526
 
516
527
  // 1. Capture starting sequence before sending user message (#227, #228)
@@ -639,6 +650,27 @@ export class SessionRunner {
639
650
  }
640
651
  }
641
652
 
653
+ _nameSession(task, handle, isResumed) {
654
+ if (!handle?.agent?.session) return;
655
+ const session = handle.agent.session;
656
+ try {
657
+ const sessionTitle = this.ctx.get ? this.ctx.get('sessionTitle') : this.ctx.sessionTitle;
658
+ if (!sessionTitle || typeof sessionTitle.rename !== 'function') return;
659
+
660
+ const explicitTitle = task.sessionTitle ? String(task.sessionTitle).trim() : '';
661
+ const taskTitle = task.title ? String(task.title).trim() : '';
662
+ const label = explicitTitle || (taskTitle.startsWith('[') ? taskTitle : '');
663
+
664
+ if (!label) return;
665
+ if (isResumed && !explicitTitle) return;
666
+
667
+ sessionTitle.rename(session, label);
668
+ logger.info(`[dsh-cron] assigned session title "${label}" for session ${session.id}`);
669
+ } catch (titleErr) {
670
+ logger.debug?.(`[dsh-cron] could not rename session ${session?.id}: ${titleErr.message}`);
671
+ }
672
+ }
673
+
642
674
  /** Token usage reported by session events or the handle (#228). */
643
675
  _extractUsage(handle, turnEvents = []) {
644
676
  const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
@@ -48,6 +48,7 @@ export function executeCreateTask(store, scheduler, args) {
48
48
  agentPreset: args.agentPreset ? String(args.agentPreset).trim() : '',
49
49
  targetSessionId: args.targetSessionId ? String(args.targetSessionId).trim() : '',
50
50
  targetSessionReset: args.targetSessionReset ? String(args.targetSessionReset).trim() : 'never',
51
+ sessionTitle: args.sessionTitle ? String(args.sessionTitle).trim() : '',
51
52
  onSuccess: args.onSuccess ? String(args.onSuccess).trim() : '',
52
53
  onFailure: args.onFailure ? String(args.onFailure).trim() : '',
53
54
  dependsOn: args.dependsOn !== undefined ? sanitizeDependsOn(args.dependsOn) : undefined,
@@ -33,6 +33,7 @@ export const PATCHABLE_TASK_FIELDS = [
33
33
  'agentPreset',
34
34
  'targetSessionId',
35
35
  'targetSessionReset',
36
+ 'sessionTitle',
36
37
  'env',
37
38
  'cwd',
38
39
  'workspaceId',
package/lib/validators.js CHANGED
@@ -128,7 +128,8 @@ export function validateRunResult(task, runInfo, options = {}) {
128
128
  // Guard against directory traversal escaping baseDir
129
129
  const resolvedPath = path.resolve(baseDir, relPath);
130
130
  const resolvedBase = path.resolve(baseDir);
131
- if (!resolvedPath.startsWith(resolvedBase)) {
131
+ const basePrefix = resolvedBase.endsWith(path.sep) ? resolvedBase : resolvedBase + path.sep;
132
+ if (resolvedPath !== resolvedBase && !resolvedPath.startsWith(basePrefix)) {
132
133
  const message = `File path "${relPath}" escapes allowed directory boundary`;
133
134
  details.push({ type: 'file', passed: false, message });
134
135
  return { ok: false, status: 'failed', error: message, details };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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",