Fork dsh-cron-tasks as dsh-ops-cron with DSH/IM delivery.

Agent cron_* jobs default to IM when created from a WhatsApp session with a matching delivery target; otherwise DSH sidebar sessions. Fire path soft-calls ctx.dshIm.send for summaries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 21:48:17 +08:00
parent 4447404361
commit 2f08136adf
17 changed files with 472 additions and 285 deletions

View file

@ -8,6 +8,7 @@ import { mkdir } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, join } from 'node:path'
import { claimOccurrence, executeClaimedRun, extractAssistantText, interruptActiveRuns, publicJob, settleRun, TITLE_PREFIX } from './fire.js'
import { deliverRunToIm } from './delivery.js'
import { workspaceVisibleIds } from './isolation.js'
import { decideDispatch, nextFire, validateSchedule } from './scheduler.js'
import {
@ -23,15 +24,15 @@ import {
upsertJob,
} from './store.js'
export const PLUGIN_NAME = 'dsh-cron-tasks'
export const API_PREFIX = '/dsh-cron-tasks'
export const PLUGIN_NAME = 'dsh-ops-cron'
export const API_PREFIX = '/dsh-ops-cron'
export function dshHome() {
return process.env.DSH_HOME || join(homedir(), '.dsh')
}
export function defaultCwd() {
return join(dshHome(), 'cron-tasks', 'workspace')
return join(dshHome(), 'ops-cron', 'workspace')
}
function json(res, status, body) {
@ -110,6 +111,8 @@ function runView(run) {
* @param {() => number} [options.now]
* @param {string} [options.filePath]
* @param {object} [options.sessionPort] { createAndPrompt, archiveSession, waitForTurn }
* @param {() => object|undefined} [options.getDshIm] soft-injected proactive IM API
* @param {{ warn?: Function, info?: Function }} [options.logger]
* @param {number} [options.tickIntervalMs]
*/
export function createHostService(options = {}) {
@ -117,6 +120,8 @@ export function createHostService(options = {}) {
const filePath = options.filePath || storePath(dshHome())
const store = options.store || createStore({ filePath })
const sessionPort = options.sessionPort || null
const getDshIm = typeof options.getDshIm === 'function' ? options.getDshIm : () => undefined
const logger = options.logger || console
const tickIntervalMs = Number(options.tickIntervalMs) > 0 ? Number(options.tickIntervalMs) : 15_000
let timer = null
let ticking = false
@ -129,6 +134,16 @@ export function createHostService(options = {}) {
return store.read()
}
async function maybeDeliverIm(job, run) {
if (!job || job.delivery?.kind !== 'im') return
try {
await deliverRunToIm(job, run?.summary, { dshIm: getDshIm() })
logger.info?.(`[dsh-ops-cron] im delivery sent for job ${job.id} → ${job.delivery.targetId}`)
} catch (error) {
logger.warn?.(`[dsh-ops-cron] im delivery failed for job ${job.id}: ${error instanceof Error ? error.message : error}`)
}
}
async function createJob(input) {
const t = now()
let created
@ -159,6 +174,7 @@ export function createHostService(options = {}) {
provider: patch.provider !== undefined ? patch.provider : job.provider,
model: patch.model !== undefined ? patch.model : job.model,
reasoningEffort: patch.reasoningEffort !== undefined ? patch.reasoningEffort : job.reasoningEffort,
delivery: patch.delivery !== undefined ? patch.delivery : job.delivery,
}
const record = createJobRecord(nextInput, current, t)
record.createdAt = job.createdAt
@ -228,9 +244,11 @@ export function createHostService(options = {}) {
const settled = await store.mutate((current) => settleRun(current, run.id, terminal, now()))
run = (settled.runs || []).find((row) => row.id === claimed.run.id)
const job = getJob(settled, jobId)
await maybeDeliverIm(job, run)
return { job: jobView(job), run: runView(run), decision: claimed.decision }
}
const job = getJob(executed, jobId)
await maybeDeliverIm(job, run)
return { job: jobView(job), run: runView(run), decision: claimed.decision }
}
@ -295,7 +313,7 @@ export function createHostService(options = {}) {
}
async function recover() {
await mkdir(join(dshHome(), 'cron-tasks'), { recursive: true })
await mkdir(join(dshHome(), 'ops-cron'), { recursive: true })
const t = now()
await withState((current) => {
let next = interruptActiveRuns(current, t)