dsh-taskboard 0.7.2 → 0.7.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { newCommentId, nextCronTime, normalizeBody, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport { DEFAULT_MAX_CONCURRENT, type ExecutionService } from './execution.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Tick cadence. */\nconst TICK_MS = 60_000\n\n/** A due window older than this is skipped (missed while the host was down). */\nconst SKIP_AFTER_MS = 5 * 60_000\n\n/** Everything the scheduler needs. */\nexport interface SchedulerDeps {\n store: TaskStore\n execution: Pick<ExecutionService, 'run' | 'inFlight'>\n now: () => number\n /** Max concurrently running executions (default 3; must match the execution service). */\n maxConcurrent?: number\n /** Timer face (injectable for tests). The timeout pair is optional so\n * older injections keep working; gaps fall back to the globals. */\n timers?: {\n setInterval(fn: () => void, ms: number): unknown\n clearInterval(handle: unknown): void\n setTimeout?(fn: () => void, ms: number): unknown\n clearTimeout?(handle: unknown): void\n }\n}\n\ntype SchedulerTimers = NonNullable<SchedulerDeps['timers']>\n\nconst DEFAULT_TIMERS: Required<SchedulerTimers> = {\n setInterval: (fn: () => void, ms: number): unknown => setInterval(fn, ms),\n clearInterval: (handle: unknown): void => { clearInterval(handle as Parameters<typeof clearInterval>[0]) },\n setTimeout: (fn: () => void, ms: number): unknown => setTimeout(fn, ms),\n clearTimeout: (handle: unknown): void => { clearTimeout(handle as Parameters<typeof clearTimeout>[0]) },\n}\n\n/**\n * The cron scheduler.\n */\nexport class SchedulerService {\n private handle: unknown\n private catchup: unknown\n private timers: Required<SchedulerTimers> = DEFAULT_TIMERS\n\n /** @param deps - store + execution + clock. */\n constructor(private readonly deps: SchedulerDeps) {}\n\n /** Start ticking. */\n start(): void {\n // Fill optional timer slots from the globals so a legacy injection that\n // only carries the interval pair still works end to end.\n this.timers = this.deps.timers === undefined ? DEFAULT_TIMERS : { ...DEFAULT_TIMERS, ...this.deps.timers }\n // A tick rejection (disk error inside a mutation) must never surface as\n // an unhandled rejection — log it and keep the schedule alive.\n this.handle = this.timers.setInterval(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, TICK_MS)\n // Catch up promptly on host restart: run one tick soon after start. The\n // handles are cleared on dispose so a torn-down scheduler never fires.\n this.catchup = this.timers.setTimeout(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, 3_000)\n }\n\n /** Stop ticking. */\n dispose(): void {\n if (this.catchup !== undefined) {\n this.timers.clearTimeout(this.catchup)\n this.catchup = undefined\n }\n if (this.handle === undefined) return\n this.timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** One scheduler pass (exported for tests). */\n async tick(): Promise<void> {\n // Load once before reading: snapshot() does not trigger a load, and the\n // scheduler may be the first consumer after a host restart (otherwise it\n // would tick over an empty ledger until something else loads it).\n await this.deps.store.load()\n const now = this.deps.now()\n const ledger: TaskLedger = this.deps.store.snapshot()\n for (const task of ledger.tasks) {\n if (task.execution.mode !== 'scheduled' || task.execution.cron === undefined) continue\n if (task.execution.nextRunAt === undefined) continue\n if (task.status === 'in_progress' || task.trashedAt !== undefined) continue\n if (task.execution.nextRunAt > now) continue\n // At the concurrency cap (S4: checked FRESH per task — runs register\n // only after agent creation, so a once-per-tick snapshot under-counted\n // the startup window): leave nextRunAt in the past and retry next tick\n // — advancing here would silently burn this window.\n if (this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT)) continue\n const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS\n\n // Advance the schedule AND record the trigger in ONE mutation (S13:\n // one revision bump, one broadcast, and the two writes can no longer\n // straddle a status change), then run unless the window was missed.\n await this.advanceAndMark(task.id, now, missed ? undefined : task.execution.nextRunAt)\n if (missed) continue\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n }\n }\n\n /**\n * Recompute the next run and record the trigger instant for one scheduled\n * task, in one serial-queue mutation. S12: a cron that can no longer match\n * anything within the 4-year scan window (only reachable through a\n * hand-edited ledger — every normal entry point validates) would otherwise\n * leave nextRunAt in the past and spin a full ~2M-iteration scan every\n * tick; it is cleared with a system comment instead of dying silently.\n */\n private async advanceAndMark(taskId: string, now: number, triggeredAt: number | undefined): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.cron === undefined) return undefined\n if (task.status === 'in_progress' || task.trashedAt !== undefined) return undefined\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) {\n const deadCron = task.execution.cron\n task.execution.cron = undefined\n task.execution.nextRunAt = undefined\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 定时表达式 ${deadCron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。`),\n systemKey: 'sys.cronDead',\n systemParams: { cron: deadCron },\n version: 1,\n createdAt: now,\n })\n return [task]\n }\n task.execution.nextRunAt = next\n if (triggeredAt !== undefined) task.execution.lastTriggeredAt = triggeredAt\n return [task]\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scheduler.js","names":[],"sources":["../../src/host/scheduler.ts"],"sourcesContent":["/**\n * Host-side cron scheduler: one tick per minute over the ledger's scheduled\n * tasks. A due task (nextRunAt reached, not running, not trashed) first has\n * its next run advanced to the next cron match — then it executes through\n * the same path as the manual button. Missed windows (host was down, tab\n * closed — irrelevant here, this is the host process) simply advance: a\n * nextRunAt more than one window in the past is skipped, not caught up.\n *\n * @module dsh-taskboard/host/scheduler\n */\nimport { newCommentId, nextCronTime, normalizeBody, parseCron, type TaskLedger } from '../shared/protocol.ts'\nimport { DEFAULT_MAX_CONCURRENT, type ExecutionService } from './execution.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Tick cadence. */\nconst TICK_MS = 60_000\n\n/** A due window older than this is skipped (missed while the host was down). */\nconst SKIP_AFTER_MS = 5 * 60_000\n\n/** Everything the scheduler needs. */\nexport interface SchedulerDeps {\n store: TaskStore\n execution: Pick<ExecutionService, 'run' | 'inFlight'>\n now: () => number\n /** Max concurrently running executions (default 3; must match the execution service). */\n maxConcurrent?: number\n /** Timer face (injectable for tests). The timeout pair is optional so\n * older injections keep working; gaps fall back to the globals. */\n timers?: {\n setInterval(fn: () => void, ms: number): unknown\n clearInterval(handle: unknown): void\n setTimeout?(fn: () => void, ms: number): unknown\n clearTimeout?(handle: unknown): void\n }\n}\n\ntype SchedulerTimers = NonNullable<SchedulerDeps['timers']>\n\nconst DEFAULT_TIMERS: Required<SchedulerTimers> = {\n setInterval: (fn: () => void, ms: number): unknown => setInterval(fn, ms),\n clearInterval: (handle: unknown): void => { clearInterval(handle as Parameters<typeof clearInterval>[0]) },\n setTimeout: (fn: () => void, ms: number): unknown => setTimeout(fn, ms),\n clearTimeout: (handle: unknown): void => { clearTimeout(handle as Parameters<typeof clearTimeout>[0]) },\n}\n\n/**\n * The cron scheduler.\n */\nexport class SchedulerService {\n private handle: unknown\n private catchup: unknown\n private timers: Required<SchedulerTimers> = DEFAULT_TIMERS\n\n /** @param deps - store + execution + clock. */\n constructor(private readonly deps: SchedulerDeps) {}\n\n /** Start ticking. */\n start(): void {\n // Fill optional timer slots from the globals so a legacy injection that\n // only carries the interval pair still works end to end.\n this.timers = this.deps.timers === undefined ? DEFAULT_TIMERS : { ...DEFAULT_TIMERS, ...this.deps.timers }\n // A tick rejection (disk error inside a mutation) must never surface as\n // an unhandled rejection — log it and keep the schedule alive.\n this.handle = this.timers.setInterval(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, TICK_MS)\n // Catch up promptly on host restart: run one tick soon after start. The\n // handles are cleared on dispose so a torn-down scheduler never fires.\n this.catchup = this.timers.setTimeout(() => { void this.tick().catch(error => {\n console.error('[dsh-taskboard] scheduler tick failed:', error)\n }) }, 3_000)\n }\n\n /** Stop ticking. */\n dispose(): void {\n if (this.catchup !== undefined) {\n this.timers.clearTimeout(this.catchup)\n this.catchup = undefined\n }\n if (this.handle === undefined) return\n this.timers.clearInterval(this.handle)\n this.handle = undefined\n }\n\n /** One scheduler pass (exported for tests). */\n async tick(): Promise<void> {\n // Load once before reading: snapshot() does not trigger a load, and the\n // scheduler may be the first consumer after a host restart (otherwise it\n // would tick over an empty ledger until something else loads it).\n await this.deps.store.load()\n const now = this.deps.now()\n const ledger: TaskLedger = this.deps.store.snapshot()\n for (const task of ledger.tasks) {\n if (task.execution.mode !== 'scheduled' || task.trashedAt !== undefined) continue\n // Only todo fires. A finished periodic run settles in review (its cron\n // moves to a freshly minted successor todo card), and terminal/parked\n // states retain their config so an explicit reopen can resume — none\n // of these ever refire from here.\n if (task.status !== 'todo') continue\n // At the concurrency cap (S4: checked FRESH per task — runs register\n // only after agent creation, so a once-per-tick snapshot under-counted\n // the startup window): leave the due time in place and retry next tick\n // — advancing/consuming here would silently burn this window.\n if (this.deps.execution.inFlight() >= (this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT)) continue\n\n if (task.execution.cron !== undefined) {\n // Periodic (定期执行): refire at every cron match.\n if (task.execution.nextRunAt === undefined) continue\n if (task.execution.nextRunAt > now) continue\n const missed = now - task.execution.nextRunAt > SKIP_AFTER_MS\n\n // Advance the schedule AND record the trigger in ONE mutation (S13:\n // one revision bump, one broadcast, and the two writes can no longer\n // straddle a status change), then run unless the window was missed.\n await this.advanceAndMark(task.id, now, missed ? undefined : task.execution.nextRunAt)\n if (missed) continue\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n } else if (task.execution.runAt !== undefined) {\n // One-shot (定时执行): fire once at the instant, then consume it.\n const due = task.execution.runAt\n if (due > now) continue\n const missed = now - due > SKIP_AFTER_MS\n\n // Consume the one-shot AND record the trigger in ONE mutation, so a\n // failure can never re-fire it (the settle path returns the card to\n // todo with a comment, and the gone runAt keeps it there).\n await this.consumeRunAt(task.id, now, missed ? due : undefined)\n if (missed) continue\n await this.deps.execution.run(task.id, 'scheduled').catch(error => {\n console.error('[dsh-taskboard] scheduled run failed:', error)\n })\n }\n }\n }\n\n /**\n * Consume a one-shot task's runAt in one serial-queue mutation: the field\n * and nextRunAt are cleared the moment the trigger fires, so the task can\n * never fire twice. A window missed while the host was down is consumed\n * too, with a system comment instead of a silent drop.\n */\n private async consumeRunAt(taskId: string, now: number, missedDue: number | undefined): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.runAt === undefined) return undefined\n if (task.status !== 'todo' || task.trashedAt !== undefined) return undefined\n delete task.execution.runAt\n task.execution.nextRunAt = undefined\n task.execution.lastTriggeredAt = now\n if (missedDue !== undefined) {\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 定时执行错过触发时间(主机当时未运行),本次不再补跑;可手动执行或修改定时。'),\n systemKey: 'sys.runAtMissed',\n version: 1,\n createdAt: now,\n })\n }\n return [task]\n })\n }\n\n /**\n * Recompute the next run and record the trigger instant for one scheduled\n * task, in one serial-queue mutation. S12: a cron that can no longer match\n * anything within the 4-year scan window (only reachable through a\n * hand-edited ledger — every normal entry point validates) would otherwise\n * leave nextRunAt in the past and spin a full ~2M-iteration scan every\n * tick; it is cleared with a system comment instead of dying silently.\n */\n private async advanceAndMark(taskId: string, now: number, triggeredAt: number | undefined): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(t => t.id === taskId)\n if (task === undefined || task.execution.cron === undefined) return undefined\n if (task.status !== 'todo' || task.trashedAt !== undefined) return undefined\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next === undefined) {\n const deadCron = task.execution.cron\n task.execution.cron = undefined\n task.execution.nextRunAt = undefined\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 定时表达式 ${deadCron} 在 4 年内没有可触发时间,已停用定时;请修正 cron 后重新开启。`),\n systemKey: 'sys.cronDead',\n systemParams: { cron: deadCron },\n version: 1,\n createdAt: now,\n })\n return [task]\n }\n task.execution.nextRunAt = next\n if (triggeredAt !== undefined) task.execution.lastTriggeredAt = triggeredAt\n return [task]\n })\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { defaultSyncExternalSessionsOf, newCommentId, newExecutionId, newTaskId, normalizeBody, normalizeTitle } from "../shared/protocol.js";
1
+ import { defaultSyncExternalSessionsOf, newCommentId, newExecutionId, newTaskId, nextCronTime, normalizeBody, normalizeTitle, parseCron, spawnNextCycle } from "../shared/protocol.js";
2
2
  //#region src/host/session-sync.ts
3
3
  /**
4
4
  * External workspace session synchronization service.
@@ -406,7 +406,9 @@ var ExternalSessionSyncService = class {
406
406
  await this.deps.store.mutate("execution-recorded", (ledger) => {
407
407
  const task = ledger.tasks.find((t) => t.claimedBy === sessionId || t.executions.some((e) => e.sessionId === sessionId));
408
408
  if (task === void 0 || task.trashedAt !== void 0) return void 0;
409
+ let scheduledTrigger = false;
409
410
  for (const exec of task.executions) if (exec.sessionId === sessionId && exec.outcome === "running") {
411
+ scheduledTrigger = exec.trigger === "scheduled";
410
412
  exec.endedAt = now;
411
413
  if (isFailure) {
412
414
  exec.outcome = "failed";
@@ -441,6 +443,31 @@ var ExternalSessionSyncService = class {
441
443
  version: 1,
442
444
  createdAt: now
443
445
  });
446
+ if (scheduledTrigger && task.execution.cron !== void 0) {
447
+ const match = parseCron(task.execution.cron);
448
+ if ((match === null ? void 0 : nextCronTime(match, now) ?? void 0) !== void 0) {
449
+ const successor = spawnNextCycle(task, void 0, now);
450
+ task.execution = { mode: "claim" };
451
+ task.comments.push({
452
+ id: newCommentId(),
453
+ body: normalizeBody(`[系统] 定期任务本轮执行完毕,定时已由新待办卡 ${successor.id} 承接,请审查本卡后验收。`),
454
+ systemKey: "sys.periodicHandoff",
455
+ systemParams: { nextTaskId: successor.id },
456
+ version: 1,
457
+ createdAt: now
458
+ });
459
+ ledger.tasks.push(successor);
460
+ return [task, successor];
461
+ }
462
+ task.execution = { mode: "claim" };
463
+ task.comments.push({
464
+ id: newCommentId(),
465
+ body: normalizeBody("[系统] 定期表达式已无未来触发时间,本轮结束后定时停用;如需继续请重新设置。"),
466
+ systemKey: "sys.cronDead",
467
+ version: 1,
468
+ createdAt: now
469
+ });
470
+ }
444
471
  }
445
472
  return [task];
446
473
  });
@@ -1 +1 @@
1
- {"version":3,"file":"session-sync.js","names":[],"sources":["../../src/host/session-sync.ts"],"sourcesContent":["/**\n * External workspace session synchronization service.\n *\n * When `settings.syncExternalSessions` is enabled (0.5.4):\n * - Listens to session lifecycle events from outside the taskboard.\n * - On `turn/start`: automatically captures or resumes the session on the board\n * (status: `in_progress`, claimedBy: sessionId).\n * - On `user/message` / `session/title`: enriches/updates task title & description.\n * - On `turn/end`: settles the execution (success -> `in_review` 待验收, failure -> `todo`).\n *\n * @module dsh-taskboard/host/session-sync\n */\nimport {\n defaultSyncExternalSessionsOf,\n newCommentId,\n newExecutionId,\n newTaskId,\n normalizeBody,\n normalizeTitle,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { EventsFace } from './execution.ts'\nimport type { TaskStore } from './store.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Extract text content from a user message event payload. */\nexport function extractUserMessageText(msg: unknown): string {\n if (typeof msg !== 'object' || msg === null) return ''\n const content = (msg as { content?: unknown }).content\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content\n .map(part => {\n if (typeof part === 'string') return part\n if (typeof part === 'object' && part !== null && 'text' in part && typeof (part as { text: unknown }).text === 'string') {\n return (part as { text: string }).text\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n }\n return ''\n}\n\n/** Extract a short one-line title from prompt text. */\nexport function titleFromText(text: string): string {\n const clean = text.trim().replace(/^#+\\s*/, '')\n const firstLine = clean.split('\\n')[0]?.trim() ?? ''\n return firstLine.slice(0, 50).trim()\n}\n\n/**\n * Detect whether a session represents a subagent child conversation.\n * Subagents are created by agent delegation (e.g. invoke_subagent / subagents service)\n * and should never be automatically converted into user tasks on the taskboard.\n */\nexport function isSubagentSession(\n sessionId: string,\n sessionMeta?: unknown,\n event?: { type: string; data?: unknown },\n): boolean {\n if (typeof sessionId === 'string') {\n if (sessionId.startsWith('subagent-') || sessionId.startsWith('child-') || sessionId.startsWith('delegate-')) {\n return true\n }\n }\n\n if (typeof sessionMeta === 'object' && sessionMeta !== null) {\n const s = sessionMeta as {\n header?: Record<string, unknown>\n meta?: Record<string, unknown>\n options?: Record<string, unknown>\n }\n\n const header = s.header\n const meta = s.meta\n const options = s.options\n\n // Check origin\n if (header?.origin === 'subagent' || meta?.origin === 'subagent') return true\n\n // Check parent session lineage\n if (\n header?.parentSession !== undefined\n || header?.parentSessionId !== undefined\n || meta?.parentSession !== undefined\n || meta?.parentSessionId !== undefined\n ) {\n return true\n }\n\n // Check delegation depth\n if (typeof header?.delegationDepth === 'number' && header.delegationDepth > 0) return true\n if (typeof meta?.delegationDepth === 'number' && meta.delegationDepth > 0) return true\n if (typeof options?.subagentDepth === 'number' && options.subagentDepth > 0) return true\n }\n\n if (event !== undefined) {\n if (event.type === 'subagent/descriptor' || event.type === 'subagent/start' || event.type === 'subagent/end') {\n return true\n }\n if (typeof event.data === 'object' && event.data !== null) {\n const d = event.data as Record<string, unknown>\n if (\n d.origin === 'subagent'\n || d.subagent === true\n || typeof d.subagentId === 'string'\n || typeof d.parentSession === 'string'\n ) {\n return true\n }\n }\n }\n\n return false\n}\n\n/**\n * Detect whether a session represents an active working conversation.\n * Inspects state, status, isWorking/isBusy methods, running flags, and active turns.\n */\nexport function isSessionActiveWorking(session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n const s = session as Record<string, unknown>\n\n // 1. Method checks\n if (typeof s.isWorking === 'function') {\n try { if (Boolean((s.isWorking as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.isWorking === true) {\n return true\n }\n\n if (typeof s.isBusy === 'function') {\n try { if (Boolean((s.isBusy as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.busy === true || s.isBusy === true) {\n return true\n }\n\n // 2. Boolean flags\n if (s.running === true || s.active === true || s.isGenerating === true || s.generating === true) {\n return true\n }\n\n // 3. State string\n if (typeof s.state === 'string') {\n const st = s.state.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 4. Status string\n if (typeof s.status === 'string') {\n const st = s.status.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'active' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 5. Active turn checks\n if (s.activeTurn !== undefined && s.activeTurn !== null && s.activeTurn !== false) {\n return true\n }\n if (s.currentTurn !== undefined && s.currentTurn !== null) {\n if (typeof s.currentTurn === 'object') {\n const ct = s.currentTurn as Record<string, unknown>\n if (ct.status === 'running' || ct.state === 'running' || ct.outcome === 'running' || ct.endedAt === undefined) {\n return true\n }\n } else {\n return true\n }\n }\n\n // 6. Turns list\n if (Array.isArray(s.turns) && s.turns.length > 0) {\n const lastTurn = s.turns[s.turns.length - 1]\n if (typeof lastTurn === 'object' && lastTurn !== null) {\n const lt = lastTurn as Record<string, unknown>\n if (lt.status === 'running' || lt.state === 'running' || lt.outcome === 'running' || (lt.startedAt !== undefined && lt.endedAt === undefined)) {\n return true\n }\n }\n }\n\n return false\n}\n\n/** Dependencies required by the external session sync service. */\nexport interface SessionSyncDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n events: EventsFace\n sessions?: {\n get?: (id: string) => unknown\n list?: () => unknown[]\n }\n now: () => number\n scanIntervalMs?: number\n}\n\n/** Default scan interval: 4s. */\nexport const DEFAULT_SCAN_INTERVAL_MS = 4000\n\n/**\n * Service that synchronizes external workspace sessions into the taskboard.\n */\nexport class ExternalSessionSyncService {\n private readonly unsubscribe: () => void\n private readonly ignoredSessions = new Set<string>()\n private scanTimer?: NodeJS.Timeout | number\n\n constructor(private readonly deps: SessionSyncDeps) {\n this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {\n return this.handleSessionEvent(sessionId, event, sessionMeta)\n })\n\n const interval = deps.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS\n if (interval > 0) {\n this.scanTimer = setInterval(() => {\n void this.scanActiveSessions()\n }, interval)\n }\n }\n\n /** Detach listener and clear scanner on teardown. */\n dispose(): void {\n this.unsubscribe()\n if (this.scanTimer !== undefined) {\n clearInterval(this.scanTimer as NodeJS.Timeout)\n this.scanTimer = undefined\n }\n }\n\n /**\n * Periodic active scan: checks whether external sessions linked to board tasks\n * are actively working, ensuring tasks in `in_review` / `todo` / `backlog`\n * automatically pull back to `in_progress`.\n */\n async scanActiveSessions(): Promise<void> {\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n if (this.deps.sessions === undefined) return\n\n const now = this.deps.now()\n const tasks = snapshot.tasks.filter(t => t.trashedAt === undefined)\n\n for (const task of tasks) {\n const sessionId = task.claimedBy ?? task.executions[task.executions.length - 1]?.sessionId\n if (sessionId === undefined || typeof sessionId !== 'string') continue\n if (this.ignoredSessions.has(sessionId) || sessionId.startsWith('session-taskboard-')) continue\n\n let session: unknown\n try {\n session = this.deps.sessions.get?.(sessionId)\n } catch { /* ignore */ }\n\n if (session === undefined && typeof this.deps.sessions.list === 'function') {\n try {\n const list = this.deps.sessions.list()\n session = list?.find(s => (s as { id?: string })?.id === sessionId)\n } catch { /* ignore */ }\n }\n\n if (session === undefined || session === null) continue\n if (isSubagentSession(sessionId, session)) {\n this.ignoredSessions.add(sessionId)\n continue\n }\n\n const isWorking = isSessionActiveWorking(session)\n if (isWorking) {\n if (task.status !== 'in_progress') {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const current = ledger.tasks.find(t => t.id === task.id)\n if (current === undefined || current.trashedAt !== undefined) return undefined\n current.status = 'in_progress'\n current.claimedBy = sessionId\n current.claimedAt = current.claimedAt ?? now\n current.updatedAt = now\n current.updatedBy = { kind: 'agent', sessionId }\n const hasRunning = current.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n current.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n }\n return [current]\n })\n }\n }\n }\n }\n\n private async handleSessionEvent(\n sessionId: string,\n event: { type: string; data?: unknown },\n sessionMeta?: {\n header?: { cwd?: string; origin?: string; parentSession?: string; parentSessionId?: string; delegationDepth?: number }\n meta?: { origin?: string; parentSession?: string; delegationDepth?: number }\n options?: { subagentDepth?: number }\n },\n ): Promise<void> {\n // 1. Check if already ignored\n if (this.ignoredSessions.has(sessionId)) return\n\n // 2. Ignore taskboard's internal execution sessions\n if (sessionId.startsWith('session-taskboard-')) {\n this.ignoredSessions.add(sessionId)\n return\n }\n\n // 3. Ignore subagent sessions (delegated children)\n if (isSubagentSession(sessionId, sessionMeta, event)) {\n this.ignoredSessions.add(sessionId)\n // If a task was previously created for this subagent before detection, clean it up\n await this.deps.store.mutate('task-deleted', (ledger) => {\n const idx = ledger.tasks.findIndex(\n t => t.claimedBy === sessionId && t.createdBy.kind === 'agent' && t.createdBy.sessionId === sessionId,\n )\n if (idx >= 0) {\n ledger.tasks.splice(idx, 1)\n return []\n }\n return undefined\n })\n return\n }\n\n // 4. Check if external session sync is enabled in board settings\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n\n const now = this.deps.now()\n\n if (event.type === 'turn/start') {\n await this.handleTurnStart(sessionId, sessionMeta?.header?.cwd, now)\n return\n }\n\n if (event.type === 'user/message') {\n await this.handleUserMessage(sessionId, event.data, now)\n return\n }\n\n if (\n event.type === 'turn/step'\n || event.type === 'turn/progress'\n || event.type === 'agent/step'\n || event.type === 'agent/thought'\n || event.type === 'agent/turn/start'\n ) {\n await this.ensureSessionInProgress(sessionId, now)\n return\n }\n\n if (event.type === 'session/title') {\n await this.handleSessionTitle(sessionId, event.data, now)\n return\n }\n\n if (event.type === 'turn/end') {\n await this.handleTurnEnd(sessionId, event.data, now)\n return\n }\n }\n\n private async ensureSessionInProgress(sessionId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = task.claimedAt ?? now\n changed = true\n }\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleTurnStart(sessionId: string, cwd: string | undefined, now: number): Promise<void> {\n // Resolve workspace\n let wsId: string | undefined\n if (cwd !== undefined && cwd.length > 0) {\n const resolved = await this.deps.workspaces.resolveByPath(cwd)\n wsId = resolved?.id\n }\n if (wsId === undefined) {\n wsId = this.deps.workspaces.list()[0]?.id ?? 'default'\n }\n\n await this.deps.store.mutate('task-created', (ledger) => {\n // Find existing task linked to this session\n const existing = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n\n if (existing !== undefined) {\n if (existing.trashedAt !== undefined) return undefined\n // If already in_progress and holding claim, ensure running execution\n if (existing.status === 'in_progress' && existing.claimedBy === sessionId) {\n const hasRunning = existing.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n return [existing]\n }\n return undefined\n }\n\n // Resumed or continued turn (e.g. from in_review or todo)\n existing.status = 'in_progress'\n existing.claimedBy = sessionId\n existing.claimedAt = now\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n return [existing]\n }\n\n // Create new task for this external session\n const shortId = sessionId.replace(/^session-/, '').slice(0, 8)\n const newTask: TaskRecord = {\n id: newTaskId(),\n title: `会话 ${shortId}`,\n description: '',\n prompt: '',\n workspaceId: wsId,\n urgency: 'normal',\n status: 'in_progress',\n blocked: false,\n execution: { mode: 'claim' },\n isolation: 'none',\n claimedBy: sessionId,\n claimedAt: now,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'agent', sessionId },\n updatedBy: { kind: 'agent', sessionId },\n comments: [],\n executions: [\n {\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n },\n ],\n }\n ledger.tasks.push(newTask)\n return [newTask]\n })\n }\n\n private async handleUserMessage(sessionId: string, msgData: unknown, now: number): Promise<void> {\n const text = extractUserMessageText(msgData)\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (text.trim().length > 0) {\n // If title is default placeholder \"会话 ...\", replace with prompt summary\n if (task.title.startsWith('会话 ') && task.title.length <= 16) {\n const derived = titleFromText(text)\n if (derived.length > 0) {\n task.title = normalizeTitle(derived)\n changed = true\n }\n }\n // If description is empty, record initial prompt\n if (task.description.length === 0) {\n task.description = text.slice(0, 2000)\n changed = true\n }\n }\n\n // If task was in in_review, todo, or backlog, user message resumes work -> move to in_progress\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = now\n changed = true\n }\n\n // Ensure running execution exists\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleSessionTitle(sessionId: string, titleData: unknown, now: number): Promise<void> {\n const rawTitle = typeof titleData === 'object' && titleData !== null && 'title' in titleData && typeof (titleData as { title: unknown }).title === 'string'\n ? (titleData as { title: string }).title\n : typeof titleData === 'string'\n ? titleData\n : ''\n if (rawTitle.trim().length === 0) return\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n task.title = normalizeTitle(rawTitle)\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n })\n }\n\n private async handleTurnEnd(sessionId: string, endData: unknown, now: number): Promise<void> {\n const reason = typeof endData === 'object' && endData !== null && 'reason' in endData\n ? (endData as { reason: unknown }).reason\n : endData\n\n // Check if error or failure\n let isFailure = false\n let errorMessage = ''\n if (typeof reason === 'object' && reason !== null) {\n const r = reason as Record<string, unknown>\n if (r.kind === 'error' || r.kind === 'failure') {\n isFailure = true\n errorMessage = typeof r.error === 'string' ? r.error : typeof r.message === 'string' ? r.message : 'turn error'\n } else if (r.kind === 'cancel') {\n isFailure = true\n errorMessage = 'cancelled'\n }\n } else if (typeof reason === 'string' && (reason.includes('error') || reason.includes('fail'))) {\n isFailure = true\n errorMessage = reason\n }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n // Settle running execution\n for (const exec of task.executions) {\n if (exec.sessionId === sessionId && exec.outcome === 'running') {\n exec.endedAt = now\n if (isFailure) {\n exec.outcome = 'failed'\n exec.error = errorMessage.slice(0, 500)\n } else {\n exec.outcome = 'succeeded'\n }\n }\n }\n\n delete task.claimedBy\n delete task.claimedAt\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n\n if (isFailure) {\n // Failed session hands back to todo with comment\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),\n systemKey: 'sys.sessionError',\n systemParams: { error: errorMessage.slice(0, 300) },\n version: 1,\n createdAt: now,\n })\n }\n } else {\n // Successful settlement automatically moves to in_review (待验收)\n if (task.status === 'in_progress') {\n task.status = 'in_review'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),\n systemKey: 'sys.sessionDone',\n version: 1,\n createdAt: now,\n })\n }\n }\n return [task]\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-sync.js","names":[],"sources":["../../src/host/session-sync.ts"],"sourcesContent":["/**\n * External workspace session synchronization service.\n *\n * When `settings.syncExternalSessions` is enabled (0.5.4):\n * - Listens to session lifecycle events from outside the taskboard.\n * - On `turn/start`: automatically captures or resumes the session on the board\n * (status: `in_progress`, claimedBy: sessionId).\n * - On `user/message` / `session/title`: enriches/updates task title & description.\n * - On `turn/end`: settles the execution (success -> `in_review` 待验收, failure -> `todo`).\n *\n * @module dsh-taskboard/host/session-sync\n */\nimport {\n defaultSyncExternalSessionsOf,\n newCommentId,\n newExecutionId,\n newTaskId,\n nextCronTime,\n normalizeBody,\n normalizeTitle,\n parseCron,\n spawnNextCycle,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport type { EventsFace } from './execution.ts'\nimport type { TaskStore } from './store.ts'\nimport type { WorkspaceFace } from './tools.ts'\n\n/** Extract text content from a user message event payload. */\nexport function extractUserMessageText(msg: unknown): string {\n if (typeof msg !== 'object' || msg === null) return ''\n const content = (msg as { content?: unknown }).content\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content\n .map(part => {\n if (typeof part === 'string') return part\n if (typeof part === 'object' && part !== null && 'text' in part && typeof (part as { text: unknown }).text === 'string') {\n return (part as { text: string }).text\n }\n return ''\n })\n .filter(Boolean)\n .join('\\n')\n }\n return ''\n}\n\n/** Extract a short one-line title from prompt text. */\nexport function titleFromText(text: string): string {\n const clean = text.trim().replace(/^#+\\s*/, '')\n const firstLine = clean.split('\\n')[0]?.trim() ?? ''\n return firstLine.slice(0, 50).trim()\n}\n\n/**\n * Detect whether a session represents a subagent child conversation.\n * Subagents are created by agent delegation (e.g. invoke_subagent / subagents service)\n * and should never be automatically converted into user tasks on the taskboard.\n */\nexport function isSubagentSession(\n sessionId: string,\n sessionMeta?: unknown,\n event?: { type: string; data?: unknown },\n): boolean {\n if (typeof sessionId === 'string') {\n if (sessionId.startsWith('subagent-') || sessionId.startsWith('child-') || sessionId.startsWith('delegate-')) {\n return true\n }\n }\n\n if (typeof sessionMeta === 'object' && sessionMeta !== null) {\n const s = sessionMeta as {\n header?: Record<string, unknown>\n meta?: Record<string, unknown>\n options?: Record<string, unknown>\n }\n\n const header = s.header\n const meta = s.meta\n const options = s.options\n\n // Check origin\n if (header?.origin === 'subagent' || meta?.origin === 'subagent') return true\n\n // Check parent session lineage\n if (\n header?.parentSession !== undefined\n || header?.parentSessionId !== undefined\n || meta?.parentSession !== undefined\n || meta?.parentSessionId !== undefined\n ) {\n return true\n }\n\n // Check delegation depth\n if (typeof header?.delegationDepth === 'number' && header.delegationDepth > 0) return true\n if (typeof meta?.delegationDepth === 'number' && meta.delegationDepth > 0) return true\n if (typeof options?.subagentDepth === 'number' && options.subagentDepth > 0) return true\n }\n\n if (event !== undefined) {\n if (event.type === 'subagent/descriptor' || event.type === 'subagent/start' || event.type === 'subagent/end') {\n return true\n }\n if (typeof event.data === 'object' && event.data !== null) {\n const d = event.data as Record<string, unknown>\n if (\n d.origin === 'subagent'\n || d.subagent === true\n || typeof d.subagentId === 'string'\n || typeof d.parentSession === 'string'\n ) {\n return true\n }\n }\n }\n\n return false\n}\n\n/**\n * Detect whether a session represents an active working conversation.\n * Inspects state, status, isWorking/isBusy methods, running flags, and active turns.\n */\nexport function isSessionActiveWorking(session: unknown): boolean {\n if (typeof session !== 'object' || session === null) return false\n const s = session as Record<string, unknown>\n\n // 1. Method checks\n if (typeof s.isWorking === 'function') {\n try { if (Boolean((s.isWorking as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.isWorking === true) {\n return true\n }\n\n if (typeof s.isBusy === 'function') {\n try { if (Boolean((s.isBusy as () => boolean)())) return true } catch { /* ignore */ }\n } else if (s.busy === true || s.isBusy === true) {\n return true\n }\n\n // 2. Boolean flags\n if (s.running === true || s.active === true || s.isGenerating === true || s.generating === true) {\n return true\n }\n\n // 3. State string\n if (typeof s.state === 'string') {\n const st = s.state.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 4. Status string\n if (typeof s.status === 'string') {\n const st = s.status.toLowerCase()\n if (st === 'running' || st === 'working' || st === 'busy' || st === 'active' || st === 'generating' || st === 'executing') {\n return true\n }\n }\n\n // 5. Active turn checks\n if (s.activeTurn !== undefined && s.activeTurn !== null && s.activeTurn !== false) {\n return true\n }\n if (s.currentTurn !== undefined && s.currentTurn !== null) {\n if (typeof s.currentTurn === 'object') {\n const ct = s.currentTurn as Record<string, unknown>\n if (ct.status === 'running' || ct.state === 'running' || ct.outcome === 'running' || ct.endedAt === undefined) {\n return true\n }\n } else {\n return true\n }\n }\n\n // 6. Turns list\n if (Array.isArray(s.turns) && s.turns.length > 0) {\n const lastTurn = s.turns[s.turns.length - 1]\n if (typeof lastTurn === 'object' && lastTurn !== null) {\n const lt = lastTurn as Record<string, unknown>\n if (lt.status === 'running' || lt.state === 'running' || lt.outcome === 'running' || (lt.startedAt !== undefined && lt.endedAt === undefined)) {\n return true\n }\n }\n }\n\n return false\n}\n\n/** Dependencies required by the external session sync service. */\nexport interface SessionSyncDeps {\n store: TaskStore\n workspaces: WorkspaceFace\n events: EventsFace\n sessions?: {\n get?: (id: string) => unknown\n list?: () => unknown[]\n }\n now: () => number\n scanIntervalMs?: number\n}\n\n/** Default scan interval: 4s. */\nexport const DEFAULT_SCAN_INTERVAL_MS = 4000\n\n/**\n * Service that synchronizes external workspace sessions into the taskboard.\n */\nexport class ExternalSessionSyncService {\n private readonly unsubscribe: () => void\n private readonly ignoredSessions = new Set<string>()\n private scanTimer?: NodeJS.Timeout | number\n\n constructor(private readonly deps: SessionSyncDeps) {\n this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {\n return this.handleSessionEvent(sessionId, event, sessionMeta)\n })\n\n const interval = deps.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS\n if (interval > 0) {\n this.scanTimer = setInterval(() => {\n void this.scanActiveSessions()\n }, interval)\n }\n }\n\n /** Detach listener and clear scanner on teardown. */\n dispose(): void {\n this.unsubscribe()\n if (this.scanTimer !== undefined) {\n clearInterval(this.scanTimer as NodeJS.Timeout)\n this.scanTimer = undefined\n }\n }\n\n /**\n * Periodic active scan: checks whether external sessions linked to board tasks\n * are actively working, ensuring tasks in `in_review` / `todo` / `backlog`\n * automatically pull back to `in_progress`.\n */\n async scanActiveSessions(): Promise<void> {\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n if (this.deps.sessions === undefined) return\n\n const now = this.deps.now()\n const tasks = snapshot.tasks.filter(t => t.trashedAt === undefined)\n\n for (const task of tasks) {\n const sessionId = task.claimedBy ?? task.executions[task.executions.length - 1]?.sessionId\n if (sessionId === undefined || typeof sessionId !== 'string') continue\n if (this.ignoredSessions.has(sessionId) || sessionId.startsWith('session-taskboard-')) continue\n\n let session: unknown\n try {\n session = this.deps.sessions.get?.(sessionId)\n } catch { /* ignore */ }\n\n if (session === undefined && typeof this.deps.sessions.list === 'function') {\n try {\n const list = this.deps.sessions.list()\n session = list?.find(s => (s as { id?: string })?.id === sessionId)\n } catch { /* ignore */ }\n }\n\n if (session === undefined || session === null) continue\n if (isSubagentSession(sessionId, session)) {\n this.ignoredSessions.add(sessionId)\n continue\n }\n\n const isWorking = isSessionActiveWorking(session)\n if (isWorking) {\n if (task.status !== 'in_progress') {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const current = ledger.tasks.find(t => t.id === task.id)\n if (current === undefined || current.trashedAt !== undefined) return undefined\n current.status = 'in_progress'\n current.claimedBy = sessionId\n current.claimedAt = current.claimedAt ?? now\n current.updatedAt = now\n current.updatedBy = { kind: 'agent', sessionId }\n const hasRunning = current.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n current.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n }\n return [current]\n })\n }\n }\n }\n }\n\n private async handleSessionEvent(\n sessionId: string,\n event: { type: string; data?: unknown },\n sessionMeta?: {\n header?: { cwd?: string; origin?: string; parentSession?: string; parentSessionId?: string; delegationDepth?: number }\n meta?: { origin?: string; parentSession?: string; delegationDepth?: number }\n options?: { subagentDepth?: number }\n },\n ): Promise<void> {\n // 1. Check if already ignored\n if (this.ignoredSessions.has(sessionId)) return\n\n // 2. Ignore taskboard's internal execution sessions\n if (sessionId.startsWith('session-taskboard-')) {\n this.ignoredSessions.add(sessionId)\n return\n }\n\n // 3. Ignore subagent sessions (delegated children)\n if (isSubagentSession(sessionId, sessionMeta, event)) {\n this.ignoredSessions.add(sessionId)\n // If a task was previously created for this subagent before detection, clean it up\n await this.deps.store.mutate('task-deleted', (ledger) => {\n const idx = ledger.tasks.findIndex(\n t => t.claimedBy === sessionId && t.createdBy.kind === 'agent' && t.createdBy.sessionId === sessionId,\n )\n if (idx >= 0) {\n ledger.tasks.splice(idx, 1)\n return []\n }\n return undefined\n })\n return\n }\n\n // 4. Check if external session sync is enabled in board settings\n const snapshot = this.deps.store.snapshot()\n if (!defaultSyncExternalSessionsOf(snapshot.settings)) return\n\n const now = this.deps.now()\n\n if (event.type === 'turn/start') {\n await this.handleTurnStart(sessionId, sessionMeta?.header?.cwd, now)\n return\n }\n\n if (event.type === 'user/message') {\n await this.handleUserMessage(sessionId, event.data, now)\n return\n }\n\n if (\n event.type === 'turn/step'\n || event.type === 'turn/progress'\n || event.type === 'agent/step'\n || event.type === 'agent/thought'\n || event.type === 'agent/turn/start'\n ) {\n await this.ensureSessionInProgress(sessionId, now)\n return\n }\n\n if (event.type === 'session/title') {\n await this.handleSessionTitle(sessionId, event.data, now)\n return\n }\n\n if (event.type === 'turn/end') {\n await this.handleTurnEnd(sessionId, event.data, now)\n return\n }\n }\n\n private async ensureSessionInProgress(sessionId: string, now: number): Promise<void> {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = task.claimedAt ?? now\n changed = true\n }\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleTurnStart(sessionId: string, cwd: string | undefined, now: number): Promise<void> {\n // Resolve workspace\n let wsId: string | undefined\n if (cwd !== undefined && cwd.length > 0) {\n const resolved = await this.deps.workspaces.resolveByPath(cwd)\n wsId = resolved?.id\n }\n if (wsId === undefined) {\n wsId = this.deps.workspaces.list()[0]?.id ?? 'default'\n }\n\n await this.deps.store.mutate('task-created', (ledger) => {\n // Find existing task linked to this session\n const existing = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n\n if (existing !== undefined) {\n if (existing.trashedAt !== undefined) return undefined\n // If already in_progress and holding claim, ensure running execution\n if (existing.status === 'in_progress' && existing.claimedBy === sessionId) {\n const hasRunning = existing.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n return [existing]\n }\n return undefined\n }\n\n // Resumed or continued turn (e.g. from in_review or todo)\n existing.status = 'in_progress'\n existing.claimedBy = sessionId\n existing.claimedAt = now\n existing.updatedAt = now\n existing.updatedBy = { kind: 'agent', sessionId }\n existing.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n return [existing]\n }\n\n // Create new task for this external session\n const shortId = sessionId.replace(/^session-/, '').slice(0, 8)\n const newTask: TaskRecord = {\n id: newTaskId(),\n title: `会话 ${shortId}`,\n description: '',\n prompt: '',\n workspaceId: wsId,\n urgency: 'normal',\n status: 'in_progress',\n blocked: false,\n execution: { mode: 'claim' },\n isolation: 'none',\n claimedBy: sessionId,\n claimedAt: now,\n version: 1,\n createdAt: now,\n updatedAt: now,\n createdBy: { kind: 'agent', sessionId },\n updatedBy: { kind: 'agent', sessionId },\n comments: [],\n executions: [\n {\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n },\n ],\n }\n ledger.tasks.push(newTask)\n return [newTask]\n })\n }\n\n private async handleUserMessage(sessionId: string, msgData: unknown, now: number): Promise<void> {\n const text = extractUserMessageText(msgData)\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n let changed = false\n if (text.trim().length > 0) {\n // If title is default placeholder \"会话 ...\", replace with prompt summary\n if (task.title.startsWith('会话 ') && task.title.length <= 16) {\n const derived = titleFromText(text)\n if (derived.length > 0) {\n task.title = normalizeTitle(derived)\n changed = true\n }\n }\n // If description is empty, record initial prompt\n if (task.description.length === 0) {\n task.description = text.slice(0, 2000)\n changed = true\n }\n }\n\n // If task was in in_review, todo, or backlog, user message resumes work -> move to in_progress\n if (task.status !== 'in_progress') {\n task.status = 'in_progress'\n task.claimedBy = sessionId\n task.claimedAt = now\n changed = true\n }\n\n // Ensure running execution exists\n const hasRunning = task.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')\n if (!hasRunning) {\n task.executions.push({\n id: newExecutionId(),\n sessionId,\n trigger: 'manual',\n startedAt: now,\n outcome: 'running',\n isolation: 'none',\n })\n changed = true\n }\n\n if (changed) {\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n }\n return undefined\n })\n }\n\n private async handleSessionTitle(sessionId: string, titleData: unknown, now: number): Promise<void> {\n const rawTitle = typeof titleData === 'object' && titleData !== null && 'title' in titleData && typeof (titleData as { title: unknown }).title === 'string'\n ? (titleData as { title: string }).title\n : typeof titleData === 'string'\n ? titleData\n : ''\n if (rawTitle.trim().length === 0) return\n\n await this.deps.store.mutate('task-updated', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n task.title = normalizeTitle(rawTitle)\n task.updatedAt = now\n task.updatedBy = { kind: 'user' }\n return [task]\n })\n }\n\n private async handleTurnEnd(sessionId: string, endData: unknown, now: number): Promise<void> {\n const reason = typeof endData === 'object' && endData !== null && 'reason' in endData\n ? (endData as { reason: unknown }).reason\n : endData\n\n // Check if error or failure\n let isFailure = false\n let errorMessage = ''\n if (typeof reason === 'object' && reason !== null) {\n const r = reason as Record<string, unknown>\n if (r.kind === 'error' || r.kind === 'failure') {\n isFailure = true\n errorMessage = typeof r.error === 'string' ? r.error : typeof r.message === 'string' ? r.message : 'turn error'\n } else if (r.kind === 'cancel') {\n isFailure = true\n errorMessage = 'cancelled'\n }\n } else if (typeof reason === 'string' && (reason.includes('error') || reason.includes('fail'))) {\n isFailure = true\n errorMessage = reason\n }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const task = ledger.tasks.find(\n t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),\n )\n if (task === undefined || task.trashedAt !== undefined) return undefined\n\n // Settle running execution\n let scheduledTrigger = false\n for (const exec of task.executions) {\n if (exec.sessionId === sessionId && exec.outcome === 'running') {\n scheduledTrigger = exec.trigger === 'scheduled'\n exec.endedAt = now\n if (isFailure) {\n exec.outcome = 'failed'\n exec.error = errorMessage.slice(0, 500)\n } else {\n exec.outcome = 'succeeded'\n }\n }\n }\n\n delete task.claimedBy\n delete task.claimedAt\n task.updatedAt = now\n task.updatedBy = { kind: 'agent', sessionId }\n\n if (isFailure) {\n // Failed session hands back to todo with comment\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),\n systemKey: 'sys.sessionError',\n systemParams: { error: errorMessage.slice(0, 300) },\n version: 1,\n createdAt: now,\n })\n }\n } else {\n // Successful settlement automatically moves to in_review (待验收)\n if (task.status === 'in_progress') {\n task.status = 'in_review'\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),\n systemKey: 'sys.sessionDone',\n version: 1,\n createdAt: now,\n })\n // Periodic scheduled success (定期执行): same handoff as the\n // execution service — the finished card stays in review while a\n // fresh todo card carries the cron onward.\n if (scheduledTrigger && task.execution.cron !== undefined) {\n const match = parseCron(task.execution.cron)\n const next = match === null ? undefined : nextCronTime(match, now) ?? undefined\n if (next !== undefined) {\n const successor = spawnNextCycle(task, undefined, now)\n task.execution = { mode: 'claim' }\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 定期任务本轮执行完毕,定时已由新待办卡 ${successor.id} 承接,请审查本卡后验收。`),\n systemKey: 'sys.periodicHandoff',\n systemParams: { nextTaskId: successor.id },\n version: 1,\n createdAt: now,\n })\n ledger.tasks.push(successor)\n return [task, successor]\n }\n task.execution = { mode: 'claim' }\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody('[系统] 定期表达式已无未来触发时间,本轮结束后定时停用;如需继续请重新设置。'),\n systemKey: 'sys.cronDead',\n version: 1,\n createdAt: now,\n })\n }\n }\n }\n return [task]\n })\n }\n}\n"],"mappings":"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"}
package/lib/host/tools.js CHANGED
@@ -1,4 +1,4 @@
1
- import { asIsolation, asStatus, asUrgency, canTransition, checklistFromTexts, defaultIsolationOf, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
1
+ import { asIsolation, asPermission, asStatus, asUrgency, canTransition, checklistFromTexts, defaultIsolationOf, defaultPermissionOf, effectivePrompt, isClaim, isClaimedBy, newCommentId, newTaskId, normalizeBody, normalizeExecution, normalizeExecutionReport, normalizeModel, normalizePrompt, normalizeTitle, summarize, syncClaim } from "../shared/protocol.js";
2
2
  import { defineTool } from "./sdk.js";
3
3
  import { archiveTaskSessions } from "./archive-sessions.js";
4
4
  //#region src/host/tools.ts
@@ -18,7 +18,7 @@ function taskDetail(t) {
18
18
  const lines = [
19
19
  `任务 ${t.id} 「${t.title}」`,
20
20
  `状态: ${t.status} (v${t.version}) · 紧急度: ${t.urgency} · 项目: ${t.workspaceId}${t.blocked ? " · 受阻" : ""}`,
21
- `执行方式: ${t.execution.mode}${t.execution.cron !== void 0 ? ` cron=${t.execution.cron}` : ""}`,
21
+ `执行方式: ${t.execution.mode}${t.execution.cron !== void 0 ? ` 定期 cron=${t.execution.cron}` : ""}${t.execution.runAt !== void 0 ? ` 定时(一次) runAt=${new Date(t.execution.runAt).toISOString()}` : ""}`,
22
22
  `隔离: ${t.isolation === "none" ? "关闭(原目录执行)" : "Git Worktree"}${t.branch !== void 0 ? `(分支 ${t.branch})` : ""}${t.branches !== void 0 ? `(多仓库镜像 ${Object.keys(t.branches).length + (t.branch !== void 0 ? 1 : 0)} 个仓库)` : ""}`
23
23
  ];
24
24
  const holder = isClaimedBy(t);
@@ -271,7 +271,7 @@ function registerTaskboardTools(ctx, deps) {
271
271
  })));
272
272
  disposers.push(register(defineTool({
273
273
  name: "taskboard_create",
274
- description: "Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). Optional: description, prompt (sent to a fresh session on execution), status (default todo), execution mode (claim|scheduled + cron), model {provider, model} to pin executions to a model. Do not track trivial requests as tasks.",
274
+ description: "Create a task on the board. Required: title, workspaceId (project), urgency (urgent/normal/relaxed). Optional: description, prompt (sent to a fresh session on execution), status (default todo), execution mode (claim | scheduled+cron 定期重复 | scheduled+runAt 定时一次), model {provider, model} to pin executions to a model. Do not track trivial requests as tasks.",
275
275
  parameters: {
276
276
  title: {
277
277
  type: "string",
@@ -303,7 +303,7 @@ function registerTaskboardTools(ctx, deps) {
303
303
  execution: {
304
304
  type: "object",
305
305
  additionalProperties: false,
306
- description: "Execution config: { mode: \"claim\" } (default) or { mode: \"scheduled\", cron: \"m h dom mon dow\" }.",
306
+ description: "Execution config: { mode: \"claim\" } (default) | { mode: \"scheduled\", cron } periodic (定期, repeats) | { mode: \"scheduled\", runAt } one-shot (定时, fires once).",
307
307
  properties: {
308
308
  mode: {
309
309
  type: "string",
@@ -311,7 +311,11 @@ function registerTaskboardTools(ctx, deps) {
311
311
  },
312
312
  cron: {
313
313
  type: "string",
314
- description: "Five-field cron expression (scheduled only)."
314
+ description: "Five-field cron expression (scheduled periodic only)."
315
+ },
316
+ runAt: {
317
+ type: "string",
318
+ description: "One-shot trigger time: epoch ms or ISO string (scheduled one-shot only; must be in the future)."
315
319
  }
316
320
  }
317
321
  },
@@ -338,6 +342,10 @@ function registerTaskboardTools(ctx, deps) {
338
342
  type: "string",
339
343
  description: "Code isolation for executions: \"worktree\" (each run gets a fresh git worktree on branch task/<标题>+<taskId>) or \"none\" (run in the project directory, zero git interaction). Omitted → the board default (看板设置 → 默认执行隔离; factory default \"none\")."
340
344
  },
345
+ permission: {
346
+ type: "string",
347
+ description: "Execution permission: \"read-only\", \"workspace-write\", or \"danger-full-access\". Omitted → the board default (看板设置 → 默认权限; factory default \"workspace-write\")."
348
+ },
341
349
  presetId: {
342
350
  type: "string",
343
351
  description: "Agent preset the execution session is composed from (its tool set / persona); default = the deployment default preset. Optional."
@@ -369,6 +377,7 @@ function registerTaskboardTools(ctx, deps) {
369
377
  const execution = normalizeExecution(args.execution ?? {}, deps.now());
370
378
  const model = args.model !== void 0 ? checkModel(deps, args.model) : void 0;
371
379
  const isolation = args.isolation === void 0 ? defaultIsolationOf(store.snapshot().settings) : asIsolation(args.isolation);
380
+ const permission = args.permission === void 0 ? defaultPermissionOf(store.snapshot().settings) : asPermission(args.permission);
372
381
  const presetId = args.presetId?.trim() || void 0;
373
382
  const checklistTexts = args.checklist?.map((c) => c.trim()).filter((c) => c.length > 0);
374
383
  const checklist = checklistTexts !== void 0 && checklistTexts.length > 0 ? checklistFromTexts(checklistTexts) : void 0;
@@ -385,6 +394,7 @@ function registerTaskboardTools(ctx, deps) {
385
394
  execution,
386
395
  model,
387
396
  isolation,
397
+ permission,
388
398
  ...presetId !== void 0 ? { presetId } : {},
389
399
  ...checklist !== void 0 ? { checklist } : {},
390
400
  version: 1,
@@ -407,7 +417,7 @@ function registerTaskboardTools(ctx, deps) {
407
417
  })));
408
418
  disposers.push(register(defineTool({
409
419
  name: "taskboard_update",
410
- description: "Update a task's title/description/prompt/urgency/blocked. Requires ifVersion (read first). The model and execution config are read-only through this tool (they belong to the task owner/user).",
420
+ description: "Update a task's title/description/prompt/urgency/blocked/permission. Requires ifVersion (read first). The model and execution config are read-only through this tool (they belong to the task owner/user).",
411
421
  parameters: {
412
422
  id: {
413
423
  type: "string",
@@ -438,6 +448,10 @@ function registerTaskboardTools(ctx, deps) {
438
448
  blocked: {
439
449
  type: "boolean",
440
450
  description: "Blocked marker (work cannot continue right now)."
451
+ },
452
+ permission: {
453
+ type: "string",
454
+ description: "Execution permission: read-only | workspace-write | danger-full-access."
441
455
  }
442
456
  },
443
457
  output: {
@@ -464,6 +478,7 @@ function registerTaskboardTools(ctx, deps) {
464
478
  if (args.prompt !== void 0) next.prompt = normalizePrompt(args.prompt);
465
479
  if (args.urgency !== void 0) next.urgency = asUrgency(args.urgency);
466
480
  if (args.blocked !== void 0) next.blocked = args.blocked;
481
+ if (args.permission !== void 0) next.permission = asPermission(args.permission);
467
482
  next.version = task.version + 1;
468
483
  next.updatedAt = deps.now();
469
484
  next.updatedBy = actor;