Mirror scheduled-run summaries into the origin effective session.

Pin origin at create time, keep isolated fire sessions for audit, and after IM/DSH delivery resume or open the origin chat to append the same summary for follow-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-06 12:45:37 +08:00
parent 2e690c30c8
commit 29d946f3dd
12 changed files with 527 additions and 26 deletions

View file

@ -8,7 +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, mergeDeliveryMention } from './delivery.js'
import { deliverRunToIm, mergeDeliveryMention, mirrorRunToSession, normalizeOrigin } from './delivery.js'
import { workspaceVisibleIds } from './isolation.js'
import { decideDispatch, nextFire, validateSchedule } from './scheduler.js'
import {
@ -112,6 +112,7 @@ function runView(run) {
* @param {string} [options.filePath]
* @param {object} [options.sessionPort] { createAndPrompt, archiveSession, waitForTurn }
* @param {() => object|undefined} [options.getDshIm] soft-injected proactive IM API
* @param {() => object|undefined} [options.getAgents] soft-injected agents service for session mirror
* @param {{ warn?: Function, info?: Function }} [options.logger]
* @param {number} [options.tickIntervalMs]
*/
@ -121,6 +122,7 @@ export function createHostService(options = {}) {
const store = options.store || createStore({ filePath })
const sessionPort = options.sessionPort || null
const getDshIm = typeof options.getDshIm === 'function' ? options.getDshIm : () => undefined
const getAgents = typeof options.getAgents === 'function' ? options.getAgents : () => undefined
const logger = options.logger || console
const tickIntervalMs = Number(options.tickIntervalMs) > 0 ? Number(options.tickIntervalMs) : 15_000
let timer = null
@ -144,6 +146,36 @@ export function createHostService(options = {}) {
}
}
async function maybeMirrorResult(job, run) {
if (!job || !run || run.status === 'skipped') return
try {
const result = await mirrorRunToSession(job, run.summary || run.error || '', {
dshIm: getDshIm(),
getAgents,
pluginName: PLUGIN_NAME,
newId: () => randomUUID(),
})
if (result?.mirrored) {
logger.info?.(`[dsh-ops-cron] mirrored run ${run.id} → session ${result.sessionId} via ${result.via} (${result.method}${result.resumed ? ', resumed' : ''})`)
} else if (result?.reason && result.reason !== 'no_target') {
logger.warn?.(`[dsh-ops-cron] mirror skipped for run ${run.id}: ${result.reason}${result.sessionId ? ` session=${result.sessionId}` : ''}${result.error ? ` (${result.error})` : ''}`)
} else if (!result?.skipped) {
logger.warn?.(`[dsh-ops-cron] mirror incomplete for run ${run.id}: ${result?.reason || 'unknown'}`)
}
} catch (error) {
logger.warn?.(`[dsh-ops-cron] mirror failed for job ${job.id}: ${error instanceof Error ? error.message : error}`)
}
}
async function settleAndNotify(jobId, runId, claimedDecision) {
const state = await snapshot()
const run = (state.runs || []).find((row) => row.id === runId)
const job = getJob(state, jobId)
await maybeDeliverIm(job, run)
await maybeMirrorResult(job, run)
return { job: jobView(job), run: runView(run), decision: claimedDecision }
}
async function createJob(input) {
const t = now()
let created
@ -178,11 +210,15 @@ export function createHostService(options = {}) {
delivery: patch.delivery !== undefined
? mergeDeliveryMention(job.delivery, patch.delivery)
: job.delivery,
origin: patch.origin !== undefined
? (normalizeOrigin(patch.origin) || undefined)
: job.origin,
}
const record = createJobRecord(nextInput, current, t)
record.createdAt = job.createdAt
record.lastRunAt = job.lastRunAt
record.lastStatus = job.lastStatus
if (patch.origin === undefined && job.origin) record.origin = job.origin
if (patch.schedule === undefined && patch.enabled === undefined) {
record.nextRunAt = job.nextRunAt
}
@ -246,13 +282,9 @@ 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 }
return settleAndNotify(jobId, run.id, claimed.decision)
}
const job = getJob(executed, jobId)
await maybeDeliverIm(job, run)
return { job: jobView(job), run: runView(run), decision: claimed.decision }
return settleAndNotify(jobId, claimed.run.id, claimed.decision)
}
async function tick() {