{"version":3,"file":"scheduled-runs.d.ts","sourceRoot":"","sources":["../../../../src/runs/background/scheduled-runs.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { AgentToolResult } from \"@lpb-work/pi-agent-core\";\nimport type { ExtensionContext } from \"@lpb-work/pi-coding-agent\";\nimport { writePrivateAtomicJson } from \"../../shared/atomic-json.ts\";\nimport { shortenPath } from \"../../shared/formatters.ts\";\nimport type { AsyncStatus, Details, ExtensionConfig } from \"../../shared/types.ts\";\nimport { reconcileAsyncRun } from \"./stale-run-reconciler.ts\";\nimport { previewSimpleWorkflowRun } from \"../../workflows/scripted-workflow.ts\";\nimport type { SubagentParamsLike } from \"../foreground/subagent-executor.ts\";\nimport { validateExecutionAcceptance } from \"../shared/acceptance.ts\";\nimport type { ResolvedSubagentCapabilityCeiling } from \"../shared/capability-ceiling.ts\";\n\nexport const SCHEDULED_RUN_ACTIONS = [\n\t\"schedule.create\",\n\t\"schedule.list\",\n\t\"schedule.show\",\n\t\"schedule.history\",\n\t\"schedule.pause\",\n\t\"schedule.resume\",\n\t\"schedule.run\",\n\t\"schedule.run-due\",\n\t\"schedule.delete\",\n] as const;\n\nconst MAX_TIMER_DELAY_MS = 2_147_483_647;\nconst DEFAULT_MAX_PENDING = 20;\nconst MAX_HISTORY = 100;\nconst STALE_LAUNCH_CLAIM_MS = 5 * 60_000;\nconst SCHEDULE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;\n\ntype ScheduledRunTimers = Pick<typeof globalThis, \"setTimeout\" | \"clearTimeout\">;\nexport type ScheduledRunAction = (typeof SCHEDULED_RUN_ACTIONS)[number];\nexport type ScheduleRunState = \"running\" | \"skipped\" | \"missed\" | \"completed\" | \"failed_launch\" | \"failed_run\";\nexport type ScheduleTrigger =\n\t| { kind: \"once\"; at: string; nextRunAt?: string }\n\t| { kind: \"interval\"; every: string; everyMs: number; anchorAt: string; nextRunAt: string };\nexport type ScheduleTarget = { workflowScript: string };\n\nexport interface ScheduleRecord {\n\tschemaVersion: 1;\n\tid: string;\n\tname: string;\n\tcwd: string;\n\ttrigger: ScheduleTrigger;\n\ttarget: ScheduleTarget;\n\toverlap: \"skip\";\n\tcatchUp: \"none\" | \"latest\";\n\ttimeoutMs?: number;\n\tpaused: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tactiveRunId?: string;\n\tlastRunId?: string;\n}\n\nexport interface ScheduleRunRecord {\n\tschemaVersion: 1;\n\tid: string;\n\tscheduleId: string;\n\tplannedAt: string;\n\tdueReason: \"timer\" | \"run-due\" | \"manual\";\n\tstate: ScheduleRunState;\n\tstartedAt?: string;\n\tupdatedAt?: string;\n\tcompletedAt?: string;\n\tasyncId?: string;\n\tasyncDir?: string;\n\terror?: string;\n}\n\ntype ScheduledRunManagerDeps = {\n\tconfig: ExtensionConfig;\n\tlaunch(params: SubagentParamsLike, ctx: ExtensionContext, signal: AbortSignal): Promise<AgentToolResult<Details>>;\n\tstoreRoot?: string;\n\tresultsDir?: string;\n\tnow?: () => number;\n\trandomId?: () => string;\n\tresolveCapabilityCeiling?: (sessionId: string) => ResolvedSubagentCapabilityCeiling | undefined;\n\ttimers?: ScheduledRunTimers;\n};\n\nfunction parseRunTimestamp(value: unknown, field: string, file: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\" || !Number.isFinite(Date.parse(value)))\n\t\tthrow new Error(`Schedule run record '${file}' has invalid ${field}.`);\n\treturn value;\n}\n\nfunction parseScheduleRun(value: unknown, file: string): ScheduleRunRecord {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(`Schedule run record '${file}' must be a JSON object.`);\n\tconst record = value as Partial<ScheduleRunRecord>;\n\tif (\n\t\trecord.schemaVersion !== 1 ||\n\t\ttypeof record.id !== \"string\" ||\n\t\ttypeof record.scheduleId !== \"string\" ||\n\t\ttypeof record.plannedAt !== \"string\" ||\n\t\ttypeof record.dueReason !== \"string\" ||\n\t\ttypeof record.state !== \"string\"\n\t)\n\t\tthrow new Error(`Schedule run record '${file}' has invalid required fields.`);\n\tif (record.dueReason !== \"timer\" && record.dueReason !== \"run-due\" && record.dueReason !== \"manual\")\n\t\tthrow new Error(`Schedule run record '${file}' has an unsupported dueReason.`);\n\tif (![\"running\", \"skipped\", \"missed\", \"completed\", \"failed_launch\", \"failed_run\"].includes(record.state))\n\t\tthrow new Error(`Schedule run record '${file}' has an unsupported state.`);\n\tparseRunTimestamp(record.plannedAt, \"plannedAt\", file);\n\tparseRunTimestamp(record.startedAt, \"startedAt\", file);\n\tparseRunTimestamp(record.updatedAt, \"updatedAt\", file);\n\tparseRunTimestamp(record.completedAt, \"completedAt\", file);\n\tfor (const [field, value] of Object.entries({\n\t\tasyncId: record.asyncId,\n\t\tasyncDir: record.asyncDir,\n\t\terror: record.error,\n\t})) {\n\t\tif (value !== undefined && typeof value !== \"string\")\n\t\t\tthrow new Error(`Schedule run record '${file}' has invalid ${field}.`);\n\t}\n\treturn record as ScheduleRunRecord;\n}\n\n\nexport function isScheduledRunAction(action: unknown): action is ScheduledRunAction {\n\treturn typeof action === \"string\" && (SCHEDULED_RUN_ACTIONS as readonly string[]).includes(action);\n}\n\nexport function scheduledRunsEnabled(config: ExtensionConfig): boolean {\n\treturn config.scheduledRuns?.enabled !== false;\n}\n\nexport function scheduledRunStorePath(cwd: string, _sessionId?: string, root?: string): string {\n\tif (!root) return path.join(path.resolve(cwd), \".pi-subagents\", \"schedules\");\n\tconst projectKey = createHash(\"sha256\").update(path.resolve(cwd)).digest(\"hex\").slice(0, 20);\n\treturn path.join(root, projectKey);\n}\n\nexport function parseScheduledRunTime(at: string, now = Date.now()): number {\n\tconst trimmed = at.trim();\n\tconst relative = trimmed.match(/^\\+(\\d+)(s|m|h|d)$/);\n\tif (relative) {\n\t\tconst amount = Number(relative[1]);\n\t\tif (!Number.isSafeInteger(amount) || amount < 1)\n\t\t\tthrow new Error(`Invalid at value \"${at}\". Relative delays must be positive, such as \"+10m\".`);\n\t\tconst unitMs = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[relative[2] as \"s\" | \"m\" | \"h\" | \"d\"];\n\t\tconst result = now + amount * unitMs;\n\t\tif (!Number.isSafeInteger(result)) throw new Error(`Invalid at value \"${at}\". Relative delay is too large.`);\n\t\treturn result;\n\t}\n\tconst iso = trimmed.match(/^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,3})?)?(Z|[+-]\\d{2}:\\d{2})$/);\n\tif (!iso)\n\t\tthrow new Error(\n\t\t\t`Invalid at value \"${at}\". Use a one-shot delay such as \"+10m\" or an ISO timestamp with timezone.`,\n\t\t);\n\tconst year = Number(iso[1]);\n\tconst month = Number(iso[2]);\n\tconst day = Number(iso[3]);\n\tconst hour = Number(iso[4]);\n\tconst minute = Number(iso[5]);\n\tconst second = iso[6] === undefined ? 0 : Number(iso[6]);\n\tconst zone = iso[7]!;\n\tconst offsetHour = zone === \"Z\" ? 0 : Number(zone.slice(1, 3));\n\tconst offsetMinute = zone === \"Z\" ? 0 : Number(zone.slice(4, 6));\n\tconst daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;\n\tconst parsed = Date.parse(trimmed);\n\tif (\n\t\tmonth < 1 ||\n\t\tmonth > 12 ||\n\t\tday < 1 ||\n\t\tday > daysInMonth ||\n\t\thour > 23 ||\n\t\tminute > 59 ||\n\t\tsecond > 59 ||\n\t\toffsetHour > 23 ||\n\t\toffsetMinute > 59 ||\n\t\t!Number.isFinite(parsed)\n\t)\n\t\tthrow new Error(`Invalid at value \"${at}\". Use a valid ISO timestamp.`);\n\tif (parsed <= now) throw new Error(`Scheduled time ${new Date(parsed).toISOString()} is in the past.`);\n\treturn parsed;\n}\n\nexport function parseScheduleInterval(every: string): number {\n\tconst match = every.trim().match(/^(\\d+)(m|h|d|w)$/);\n\tif (!match)\n\t\tthrow new Error(\n\t\t\t`Invalid every value \"${every}\". This first recurring slice supports fixed intervals such as \"30m\", \"6h\", \"2d\", or \"2w\".`,\n\t\t);\n\tconst amount = Number(match[1]);\n\tif (!Number.isSafeInteger(amount) || amount < 1)\n\t\tthrow new Error(`Invalid every value \"${every}\". Interval must be positive.`);\n\tconst unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[match[2] as \"m\" | \"h\" | \"d\" | \"w\"];\n\tconst result = amount * unitMs;\n\tif (!Number.isSafeInteger(result)) throw new Error(`Invalid every value \"${every}\". Interval is too large.`);\n\treturn result;\n}\n\nfunction timestamp(value: number): string {\n\treturn new Date(value).toISOString();\n}\n\nfunction validateScheduleId(id: string): string {\n\tif (!SCHEDULE_ID.test(id))\n\t\tthrow new Error(\"Schedule id must be 1-64 characters and contain only letters, numbers, '.', '_', or '-'.\");\n\treturn id;\n}\n\nfunction pathWithin(root: string, candidate: string): boolean {\n\tconst relative = path.relative(root, candidate);\n\treturn relative === \"\" || (relative !== \"..\" && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\nfunction assertScheduleRoot(root: string, projectCwd: string | undefined, create: boolean): void {\n\tif (!projectCwd) {\n\t\tif (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });\n\t\treturn;\n\t}\n\tconst projectPath = fs.realpathSync(projectCwd);\n\tlet existing = root;\n\twhile (!fs.existsSync(existing)) {\n\t\tconst parent = path.dirname(existing);\n\t\tif (parent === existing) break;\n\t\texisting = parent;\n\t}\n\tif (!pathWithin(projectPath, fs.realpathSync(existing)))\n\t\tthrow new Error(`Project schedule root '${root}' resolves outside the real project.`);\n\tif (!create) return;\n\tfs.mkdirSync(root, { recursive: true, mode: 0o700 });\n\tif (!pathWithin(projectPath, fs.realpathSync(root)))\n\t\tthrow new Error(`Project schedule root '${root}' resolves outside the real project.`);\n}\n\nfunction scheduleDir(root: string, id: string, create = false, projectCwd?: string): string {\n\tassertScheduleRoot(root, projectCwd, create);\n\tconst dir = path.join(root, validateScheduleId(id));\n\ttry {\n\t\tconst stat = fs.lstatSync(dir);\n\t\tif (stat.isSymbolicLink() || !stat.isDirectory())\n\t\t\tthrow new Error(`Schedule path '${dir}' must be a real directory.`);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n\t\tif (!create) return dir;\n\t\tfs.mkdirSync(dir, { mode: 0o700 });\n\t}\n\tconst rootPath = fs.realpathSync(root);\n\tconst dirPath = fs.realpathSync(dir);\n\tif (dirPath !== path.join(rootPath, id))\n\t\tthrow new Error(`Schedule path '${dir}' escapes the project schedule root.`);\n\treturn dir;\n}\n\nfunction readJson(file: string, label: string): unknown {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf-8\"));\n\t} catch (error) {\n\t\tthrow new Error(`Failed to read ${label} '${file}': ${error instanceof Error ? error.message : String(error)}`, {\n\t\t\tcause: error instanceof Error ? error : undefined,\n\t\t});\n\t}\n}\n\nfunction parseScheduleTarget(value: unknown, file: string): ScheduleTarget {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(`Schedule record '${file}' has invalid trigger or target.`);\n\tconst target = value as { workflowScript?: unknown; agent?: unknown; task?: unknown };\n\tif (typeof target.workflowScript === \"string\" && target.workflowScript.trim())\n\t\treturn { workflowScript: target.workflowScript.trim() };\n\tif (target.agent !== undefined || target.task !== undefined)\n\t\tthrow new Error(\n\t\t\t`Schedule record '${file}' uses a removed legacy agent target; recreate it with target.workflowScript.`,\n\t\t);\n\tthrow new Error(`Schedule record '${file}' requires a workflowScript target.`);\n}\n\nfunction parseSchedule(value: unknown, file: string): ScheduleRecord {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value))\n\t\tthrow new Error(`Schedule record '${file}' must be a JSON object.`);\n\tconst record = value as Partial<ScheduleRecord>;\n\tif (\n\t\trecord.schemaVersion !== 1 ||\n\t\ttypeof record.id !== \"string\" ||\n\t\ttypeof record.name !== \"string\" ||\n\t\ttypeof record.cwd !== \"string\" ||\n\t\ttypeof record.createdAt !== \"string\" ||\n\t\ttypeof record.updatedAt !== \"string\" ||\n\t\ttypeof record.paused !== \"boolean\"\n\t)\n\t\tthrow new Error(`Schedule record '${file}' has invalid required fields.`);\n\tvalidateScheduleId(record.id);\n\tif (!record.trigger || typeof record.trigger !== \"object\" || !record.target || typeof record.target !== \"object\")\n\t\tthrow new Error(`Schedule record '${file}' has invalid trigger or target.`);\n\tif (record.overlap !== \"skip\" || (record.catchUp !== \"none\" && record.catchUp !== \"latest\"))\n\t\tthrow new Error(`Schedule record '${file}' has unsupported policy fields.`);\n\tif (record.trigger.kind === \"once\") {\n\t\tif (\n\t\t\ttypeof record.trigger.at !== \"string\" ||\n\t\t\t(record.trigger.nextRunAt !== undefined && typeof record.trigger.nextRunAt !== \"string\")\n\t\t)\n\t\t\tthrow new Error(`Schedule record '${file}' has an invalid one-shot trigger.`);\n\t} else if (record.trigger.kind === \"interval\") {\n\t\tif (\n\t\t\ttypeof record.trigger.every !== \"string\" ||\n\t\t\ttypeof record.trigger.everyMs !== \"number\" ||\n\t\t\ttypeof record.trigger.anchorAt !== \"string\" ||\n\t\t\ttypeof record.trigger.nextRunAt !== \"string\"\n\t\t)\n\t\t\tthrow new Error(`Schedule record '${file}' has an invalid interval trigger.`);\n\t} else throw new Error(`Schedule record '${file}' has an unsupported trigger.`);\n\treturn { ...record, target: parseScheduleTarget(record.target, file) } as ScheduleRecord;\n}\n\nclass ScheduleStore {\n\treadonly root: string;\n\tprivate readonly projectCwd?: string;\n\n\tconstructor(root: string, projectCwd?: string) {\n\t\tthis.root = root;\n\t\tthis.projectCwd = projectCwd;\n\t}\n\n\tdirectory(id: string, create = false): string {\n\t\treturn scheduleDir(this.root, id, create, this.projectCwd);\n\t}\n\n\tids(): string[] {\n\t\tassertScheduleRoot(this.root, this.projectCwd, false);\n\t\tif (!fs.existsSync(this.root)) return [];\n\t\treturn fs\n\t\t\t.readdirSync(this.root, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() && SCHEDULE_ID.test(entry.name))\n\t\t\t.map((entry) => entry.name);\n\t}\n\n\tlist(): ScheduleRecord[] {\n\t\treturn this.ids().map((id) => this.get(id));\n\t}\n\n\tget(id: string): ScheduleRecord {\n\t\tconst file = path.join(scheduleDir(this.root, id, false, this.projectCwd), \"schedule.json\");\n\t\tif (!fs.existsSync(file)) throw new Error(`Schedule '${id}' not found.`);\n\t\treturn parseSchedule(readJson(file, \"schedule record\"), file);\n\t}\n\n\twrite(record: ScheduleRecord): void {\n\t\twritePrivateAtomicJson(\n\t\t\tpath.join(scheduleDir(this.root, record.id, true, this.projectCwd), \"schedule.json\"),\n\t\t\trecord,\n\t\t);\n\t}\n\n\tdelete(id: string): void {\n\t\tfs.rmSync(scheduleDir(this.root, id, false, this.projectCwd), { recursive: true, force: true });\n\t}\n\n\thistory(id: string): ScheduleRunRecord[] {\n\t\tconst file = path.join(scheduleDir(this.root, id, false, this.projectCwd), \"history.json\");\n\t\tif (!fs.existsSync(file)) return [];\n\t\tconst value = readJson(file, \"schedule history\") as { schemaVersion?: unknown; runs?: unknown };\n\t\tif (value?.schemaVersion !== 1 || !Array.isArray(value.runs))\n\t\t\tthrow new Error(`Schedule history '${file}' has invalid fields.`);\n\t\treturn value.runs.map((run, index) => parseScheduleRun(run, `${file} runs[${index}]`));\n\t}\n\n\twriteRun(schedule: ScheduleRecord, run: ScheduleRunRecord, event: string): void {\n\t\tconst dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);\n\t\twritePrivateAtomicJson(path.join(dir, \"runs\", `${run.id}.json`), run);\n\t\tconst runs = [run, ...this.history(schedule.id).filter((item) => item.id !== run.id)].slice(0, MAX_HISTORY);\n\t\twritePrivateAtomicJson(path.join(dir, \"history.json\"), { schemaVersion: 1, runs });\n\t\tfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\tfs.appendFileSync(\n\t\t\tpath.join(dir, \"events.jsonl\"),\n\t\t\t`${JSON.stringify({ schemaVersion: 1, timestamp: new Date().toISOString(), event, scheduleId: schedule.id, runId: run.id, state: run.state })}\\n`,\n\t\t\t{ encoding: \"utf-8\", mode: 0o600 },\n\t\t);\n\t}\n\n\tappendEvent(schedule: ScheduleRecord, event: string): void {\n\t\tconst dir = scheduleDir(this.root, schedule.id, true, this.projectCwd);\n\t\tfs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\tfs.appendFileSync(\n\t\t\tpath.join(dir, \"events.jsonl\"),\n\t\t\t`${JSON.stringify({ schemaVersion: 1, timestamp: new Date().toISOString(), event, scheduleId: schedule.id })}\\n`,\n\t\t\t{ encoding: \"utf-8\", mode: 0o600 },\n\t\t);\n\t}\n}\n\nfunction resolveMaxPending(config: ExtensionConfig): number {\n\tconst value = config.scheduledRuns?.maxPending;\n\treturn typeof value === \"number\" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_MAX_PENDING;\n}\n\nfunction nextAfter(trigger: ScheduleTrigger, plannedAt: number, now: number): string | undefined {\n\tif (trigger.kind === \"once\") return undefined;\n\tlet next = plannedAt + trigger.everyMs;\n\twhile (next <= now) next += trigger.everyMs;\n\treturn timestamp(next);\n}\n\nfunction nextRunAt(schedule: ScheduleRecord): number | undefined {\n\tconst value = schedule.trigger.nextRunAt;\n\tif (!value) return undefined;\n\tconst parsed = Date.parse(value);\n\tif (!Number.isFinite(parsed)) throw new Error(`Schedule '${schedule.id}' has invalid nextRunAt.`);\n\treturn parsed;\n}\n\nfunction duePlannedAt(schedule: ScheduleRecord, now: number): number | undefined {\n\tconst next = nextRunAt(schedule);\n\tif (next === undefined || next > now || schedule.catchUp !== \"latest\" || schedule.trigger.kind !== \"interval\")\n\t\treturn next;\n\treturn next + Math.floor((now - next) / schedule.trigger.everyMs) * schedule.trigger.everyMs;\n}\n\nfunction textResult(\n\ttext: string,\n\tschedules?: ScheduleRecord[],\n\truns?: ScheduleRunRecord[],\n\tisError = false,\n): AgentToolResult<Details> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text }],\n\t\t...(isError ? { isError: true } : {}),\n\t\tdetails: {\n\t\t\tmode: \"management\",\n\t\t\tresults: [],\n\t\t\tschedules: { ...(schedules ? { records: schedules } : {}), ...(runs ? { runs } : {}) },\n\t\t},\n\t};\n}\n\nfunction targetLabel(target: ScheduleTarget): string {\n\tconst preview = previewSimpleWorkflowRun(target.workflowScript);\n\treturn preview?.agent ? `workflowScript -> agent ${preview.agent}` : \"workflowScript (dynamic)\";\n}\n\nfunction sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget; error?: string } {\n\tif (params.tasks || params.chain)\n\t\treturn { error: \"Recurring schedules require workflowScript; legacy tasks and chain inputs are unsupported.\" };\n\tif (params.agent !== undefined || params.task !== undefined)\n\t\treturn {\n\t\t\terror: \"schedule.create requires workflowScript. Use workflowScript: \\\"return runs.run('main', { agent, task })\\\".\",\n\t\t};\n\tif (typeof params.workflowScript !== \"string\" || !params.workflowScript.trim())\n\t\treturn { error: \"schedule.create requires a non-empty workflowScript.\" };\n\tif (params.context === \"fork\") return { error: \"Scheduled runs require fresh context.\" };\n\tif (params.async === false) return { error: \"Scheduled runs are always async.\" };\n\tconst acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);\n\tif (acceptanceErrors.length) return { error: acceptanceErrors.join(\" \") };\n\treturn { target: { workflowScript: params.workflowScript.trim() } };\n}\n\nfunction executionParams(schedule: ScheduleRecord): SubagentParamsLike {\n\treturn {\n\t\t...schedule.target,\n\t\tasync: true,\n\t\tcontext: \"fresh\",\n\t\tcwd: schedule.cwd,\n\t\tmission: false,\n\t\t...(schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }),\n\t};\n}\n\nfunction snapshotContext(ctx: ExtensionContext, cwd: string): ExtensionContext {\n\tconst source = ctx.sessionManager;\n\tconst sessionId = source.getSessionId();\n\tconst sessionFile = source.getSessionFile();\n\tconst sessionManager = new Proxy(source, {\n\t\tget(target, property) {\n\t\t\tif (property === \"getSessionId\") return () => sessionId;\n\t\t\tif (property === \"getSessionFile\") return () => sessionFile;\n\t\t\tconst value = Reflect.get(target, property, target) as unknown;\n\t\t\treturn typeof value === \"function\" ? value.bind(target) : value;\n\t\t},\n\t});\n\treturn { ...ctx, cwd, sessionManager };\n}\n\nexport function listScheduledRunSummaries(cwd: string, root?: string): ScheduleRecord[] {\n\treturn new ScheduleStore(\n\t\tscheduledRunStorePath(cwd, undefined, root),\n\t\troot === undefined ? path.resolve(cwd) : undefined,\n\t).list();\n}\n\nexport class ScheduledRunManager {\n\tprivate store?: ScheduleStore;\n\tprivate readonly stores = new Map<string, ScheduleStore>();\n\tprivate readonly contexts = new Map<string, ExtensionContext>();\n\tprivate readonly timers = new Map<string, ReturnType<typeof setTimeout>>();\n\tprivate readonly now: () => number;\n\tprivate readonly randomId: () => string;\n\tprivate readonly timersApi: ScheduledRunTimers;\n\tprivate readonly deps: ScheduledRunManagerDeps;\n\n\tconstructor(deps: ScheduledRunManagerDeps) {\n\t\tthis.deps = deps;\n\t\tthis.now = deps.now ?? Date.now;\n\t\tthis.randomId = deps.randomId ?? (() => randomUUID().slice(0, 8));\n\t\tthis.timersApi = deps.timers ?? globalThis;\n\t}\n\n\tbindSession(ctx: ExtensionContext): void {\n\t\tif (!scheduledRunsEnabled(this.deps.config)) return;\n\t\tthis.selectProject(ctx.cwd, ctx);\n\t}\n\n\tstop(): void {\n\t\tthis.stopTimers();\n\t\tthis.store = undefined;\n\t\tthis.stores.clear();\n\t\tthis.contexts.clear();\n\t}\n\n\tasync handleToolCall(params: SubagentParamsLike, ctx: ExtensionContext): Promise<AgentToolResult<Details>> {\n\t\ttry {\n\t\t\tif (!scheduledRunsEnabled(this.deps.config))\n\t\t\t\treturn textResult(\n\t\t\t\t\t\"Scheduled runs are disabled by scheduledRuns.enabled=false.\",\n\t\t\t\t\tundefined,\n\t\t\t\t\tundefined,\n\t\t\t\t\ttrue,\n\t\t\t\t);\n\t\t\tthis.selectProject(params.cwd ?? ctx.cwd, ctx);\n\t\t\tswitch (params.action) {\n\t\t\t\tcase \"schedule.create\":\n\t\t\t\t\treturn this.create(params, ctx);\n\t\t\t\tcase \"schedule.list\":\n\t\t\t\t\treturn this.list();\n\t\t\t\tcase \"schedule.show\":\n\t\t\t\t\treturn this.show(params);\n\t\t\t\tcase \"schedule.history\":\n\t\t\t\t\treturn this.history(params);\n\t\t\t\tcase \"schedule.pause\":\n\t\t\t\t\treturn this.pause(params, true);\n\t\t\t\tcase \"schedule.resume\":\n\t\t\t\t\treturn this.pause(params, false);\n\t\t\t\tcase \"schedule.run\":\n\t\t\t\t\treturn await this.runManual(params);\n\t\t\t\tcase \"schedule.run-due\":\n\t\t\t\t\treturn await this.runDue();\n\t\t\t\tcase \"schedule.delete\":\n\t\t\t\t\treturn this.remove(params);\n\t\t\t\tdefault:\n\t\t\t\t\treturn textResult(`Unknown schedule action: ${params.action}`, undefined, undefined, true);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn textResult(error instanceof Error ? error.message : String(error), undefined, undefined, true);\n\t\t}\n\t}\n\n\thandleAsyncCompletion(payload: unknown): void {\n\t\tif (!payload || typeof payload !== \"object\") return;\n\t\tconst data = payload as { id?: unknown; runId?: unknown; success?: unknown; state?: unknown; summary?: unknown };\n\t\tconst asyncId = typeof data.runId === \"string\" ? data.runId : typeof data.id === \"string\" ? data.id : undefined;\n\t\tif (!asyncId) return;\n\t\tfor (const store of this.stores.values()) {\n\t\t\tlet ids: string[];\n\t\t\ttry {\n\t\t\t\tids = store.ids();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`Failed to inspect schedule store '${store.root}' during async completion:`, error);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const id of ids) {\n\t\t\t\ttry {\n\t\t\t\t\tconst schedule = store.get(id);\n\t\t\t\t\tconst run = store.history(id).find((item) => item.asyncId === asyncId && item.state === \"running\");\n\t\t\t\t\tif (!run) continue;\n\t\t\t\t\tthis.finishRun(\n\t\t\t\t\t\tstore,\n\t\t\t\t\t\tschedule,\n\t\t\t\t\t\trun,\n\t\t\t\t\t\tdata.success === true,\n\t\t\t\t\t\ttypeof data.summary === \"string\" ? data.summary : undefined,\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`Failed to inspect schedule '${id}' in '${store.root}' during async completion:`, error);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate create(params: SubagentParamsLike, ctx: ExtensionContext): AgentToolResult<Details> {\n\t\tconst store = this.requireStore();\n\t\tconst target = sanitizeTarget(params);\n\t\tif (target.error) return textResult(target.error, undefined, undefined, true);\n\t\tconst at = params.at?.trim();\n\t\tconst every = params.every?.trim();\n\t\tif (Boolean(at) === Boolean(every))\n\t\t\treturn textResult(\"schedule.create requires exactly one trigger: at or every.\", undefined, undefined, true);\n\t\tif (params.overlap !== undefined && params.overlap !== \"skip\")\n\t\t\treturn textResult(\"This first recurring slice supports overlap='skip' only.\", undefined, undefined, true);\n\t\tif (params.catchUp !== undefined && params.catchUp !== \"none\" && params.catchUp !== \"latest\")\n\t\t\treturn textResult(\"catchUp must be 'none' or 'latest'.\", undefined, undefined, true);\n\t\tif (\n\t\t\tparams.missionId !== undefined ||\n\t\t\tparams.mission !== undefined ||\n\t\t\tparams.missionUpdate !== undefined ||\n\t\t\tparams.missionStatus !== undefined ||\n\t\t\tparams.missionScope !== undefined\n\t\t)\n\t\t\treturn textResult(\n\t\t\t\t\"Mission attachment is deferred from this first schedule slice.\",\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t);\n\t\tif (\n\t\t\tparams.on !== undefined ||\n\t\t\tparams.timezone !== undefined ||\n\t\t\tevery === \"day\" ||\n\t\t\tevery === \"week\" ||\n\t\t\tevery === \"month\" ||\n\t\t\tevery === \"year\"\n\t\t)\n\t\t\treturn textResult(\n\t\t\t\t\"Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.\",\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t);\n\t\tconst sessionId = ctx.sessionManager.getSessionId() ?? \"unknown\";\n\t\tif (this.deps.resolveCapabilityCeiling?.(sessionId))\n\t\t\treturn textResult(\n\t\t\t\t\"Cannot persist a schedule while a capability ceiling is active.\",\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t);\n\t\tif (store.list().length >= resolveMaxPending(this.deps.config))\n\t\t\treturn textResult(\n\t\t\t\t`Schedule limit reached (${resolveMaxPending(this.deps.config)}).`,\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t);\n\t\tconst id = validateScheduleId(params.id?.trim() || this.randomId());\n\t\tif (store.list().some((item) => item.id === id))\n\t\t\treturn textResult(`Schedule '${id}' already exists.`, undefined, undefined, true);\n\t\tconst now = this.now();\n\t\tlet trigger: ScheduleTrigger;\n\t\tif (at) {\n\t\t\tconst planned = parseScheduledRunTime(at, now);\n\t\t\ttrigger = { kind: \"once\", at, nextRunAt: timestamp(planned) };\n\t\t} else {\n\t\t\tconst everyMs = parseScheduleInterval(every!);\n\t\t\ttrigger = {\n\t\t\t\tkind: \"interval\",\n\t\t\t\tevery: every!,\n\t\t\t\teveryMs,\n\t\t\t\tanchorAt: timestamp(now),\n\t\t\t\tnextRunAt: timestamp(now + everyMs),\n\t\t\t};\n\t\t}\n\t\tconst schedule: ScheduleRecord = {\n\t\t\tschemaVersion: 1,\n\t\t\tid,\n\t\t\tname: params.name?.trim() || params.scheduleName?.trim() || targetLabel(target.target!),\n\t\t\tcwd: path.resolve(params.cwd ?? ctx.cwd),\n\t\t\ttrigger,\n\t\t\ttarget: target.target!,\n\t\t\toverlap: \"skip\",\n\t\t\tcatchUp: params.catchUp ?? \"latest\",\n\t\t\t...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),\n\t\t\tpaused: false,\n\t\t\tcreatedAt: timestamp(now),\n\t\t\tupdatedAt: timestamp(now),\n\t\t};\n\t\tstore.write(schedule);\n\t\tstore.appendEvent(schedule, \"schedule.created\");\n\t\tthis.arm(schedule, store);\n\t\treturn textResult(\n\t\t\t`Created schedule ${id}.\\nName: ${schedule.name}\\nTrigger: ${at ? `at ${at}` : `every ${every}`}\\nNext: ${schedule.trigger.nextRunAt}\\nTarget: ${targetLabel(schedule.target)}`,\n\t\t\t[schedule],\n\t\t);\n\t}\n\n\tprivate list(): AgentToolResult<Details> {\n\t\tconst schedules = this.requireStore()\n\t\t\t.list()\n\t\t\t.sort((a, b) => (a.trigger.nextRunAt ?? \"\").localeCompare(b.trigger.nextRunAt ?? \"\"));\n\t\tif (!schedules.length) return textResult(\"No project schedules.\", []);\n\t\treturn textResult(\n\t\t\t[\n\t\t\t\t`Project schedules: ${schedules.length}`,\n\t\t\t\t...schedules.map(\n\t\t\t\t\t(item) =>\n\t\t\t\t\t\t`- ${item.id} | ${item.paused ? \"paused\" : item.activeRunId ? \"running\" : \"scheduled\"} | ${item.trigger.nextRunAt ?? \"no next run\"} | ${item.name}`,\n\t\t\t\t),\n\t\t\t].join(\"\\n\"),\n\t\t\tschedules,\n\t\t);\n\t}\n\n\tprivate show(params: SubagentParamsLike): AgentToolResult<Details> {\n\t\tconst schedule = this.resolve(params);\n\t\treturn textResult(\n\t\t\t[\n\t\t\t\t`Schedule: ${schedule.id}`,\n\t\t\t\t`Name: ${schedule.name}`,\n\t\t\t\t`State: ${schedule.paused ? \"paused\" : schedule.activeRunId ? \"running\" : \"scheduled\"}`,\n\t\t\t\t`Target: ${targetLabel(schedule.target)}`,\n\t\t\t\t`CWD: ${shortenPath(schedule.cwd)}`,\n\t\t\t\t`Next: ${schedule.trigger.nextRunAt ?? \"none\"}`,\n\t\t\t\t`Catch up: ${schedule.catchUp}`,\n\t\t\t\tschedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined,\n\t\t\t]\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\"\\n\"),\n\t\t\t[schedule],\n\t\t);\n\t}\n\n\tprivate history(params: SubagentParamsLike): AgentToolResult<Details> {\n\t\tconst schedule = this.resolve(params);\n\t\tconst runs = this.requireStore().history(schedule.id);\n\t\treturn textResult(\n\t\t\truns.length\n\t\t\t\t? [\n\t\t\t\t\t\t`Schedule history: ${schedule.id}`,\n\t\t\t\t\t\t...runs.map(\n\t\t\t\t\t\t\t(run) =>\n\t\t\t\t\t\t\t\t`- ${run.id} | ${run.state} | ${run.plannedAt}${run.asyncId ? ` | async ${run.asyncId}` : \"\"}`,\n\t\t\t\t\t\t),\n\t\t\t\t\t].join(\"\\n\")\n\t\t\t\t: `No runs recorded for schedule ${schedule.id}.`,\n\t\t\t[schedule],\n\t\t\truns,\n\t\t);\n\t}\n\n\tprivate pause(params: SubagentParamsLike, paused: boolean): AgentToolResult<Details> {\n\t\tconst schedule = this.resolve(params);\n\t\tif (schedule.paused === paused)\n\t\t\treturn textResult(`Schedule ${schedule.id} is already ${paused ? \"paused\" : \"active\"}.`, [schedule]);\n\t\tschedule.paused = paused;\n\t\tschedule.updatedAt = timestamp(this.now());\n\t\tthis.requireStore().write(schedule);\n\t\tthis.requireStore().appendEvent(schedule, paused ? \"schedule.paused\" : \"schedule.resumed\");\n\t\tconst store = this.requireStore();\n\t\tif (paused) this.clearTimer(store, schedule.id);\n\t\telse this.restoreOne(store, schedule);\n\t\treturn textResult(`${paused ? \"Paused\" : \"Resumed\"} schedule ${schedule.id}.`, [schedule]);\n\t}\n\n\tprivate async runManual(params: SubagentParamsLike): Promise<AgentToolResult<Details>> {\n\t\tconst store = this.requireStore();\n\t\tconst schedule = this.resolve(params);\n\t\tconst run = await this.launch(store, schedule, this.now(), \"manual\", false);\n\t\treturn textResult(\n\t\t\t`Manual schedule run ${run.id}: ${run.state}${run.asyncId ? ` (async ${run.asyncId})` : \"\"}.`,\n\t\t\t[store.get(schedule.id)],\n\t\t\t[run],\n\t\t\trun.state === \"failed_launch\",\n\t\t);\n\t}\n\n\tprivate async runDue(): Promise<AgentToolResult<Details>> {\n\t\tconst store = this.requireStore();\n\t\tconst due = store\n\t\t\t.list()\n\t\t\t.filter(\n\t\t\t\t(schedule) => !schedule.paused && nextRunAt(schedule) !== undefined && nextRunAt(schedule)! <= this.now(),\n\t\t\t);\n\t\tconst runs: ScheduleRunRecord[] = [];\n\t\tfor (const schedule of due) {\n\t\t\tconst planned = duePlannedAt(schedule, this.now())!;\n\t\t\tif (!schedule.activeRunId && schedule.catchUp === \"none\" && planned < this.now())\n\t\t\t\truns.push(this.recordMissed(store, schedule, planned, \"run-due\"));\n\t\t\telse runs.push(await this.launch(store, schedule, planned, \"run-due\", true));\n\t\t}\n\t\treturn textResult(\n\t\t\truns.length ? `Processed ${runs.length} due schedule(s).` : \"No schedules are due.\",\n\t\t\tstore.list(),\n\t\t\truns,\n\t\t);\n\t}\n\n\tprivate remove(params: SubagentParamsLike): AgentToolResult<Details> {\n\t\tconst schedule = this.resolve(params);\n\t\tif (schedule.activeRunId)\n\t\t\treturn textResult(\n\t\t\t\t`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`,\n\t\t\t\t[schedule],\n\t\t\t\tundefined,\n\t\t\t\ttrue,\n\t\t\t);\n\t\tconst store = this.requireStore();\n\t\tthis.clearTimer(store, schedule.id);\n\t\tstore.appendEvent(schedule, \"schedule.deleted\");\n\t\tstore.delete(schedule.id);\n\t\treturn textResult(`Deleted schedule ${schedule.id}.`);\n\t}\n\n\tprivate restore(store: ScheduleStore): void {\n\t\tfor (const schedule of store.list()) this.restoreOne(store, schedule);\n\t}\n\n\tprivate restoreOne(store: ScheduleStore, schedule: ScheduleRecord): void {\n\t\tif (schedule.activeRunId) {\n\t\t\tconst run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);\n\t\t\tconst startedAt = run?.startedAt ? Date.parse(run.startedAt) : Number.NaN;\n\t\t\tlet asyncStatus: AsyncStatus | null | undefined;\n\t\t\tif (run?.state === \"running\" && run.asyncDir) {\n\t\t\t\tconst reconciliation = reconcileAsyncRun(run.asyncDir, {\n\t\t\t\t\tresultsDir: this.deps.resultsDir,\n\t\t\t\t\tnow: this.now,\n\t\t\t\t});\n\t\t\t\tasyncStatus = reconciliation.status;\n\t\t\t\tif (asyncStatus && [\"complete\", \"failed\", \"stopped\", \"rejected\"].includes(asyncStatus.state))\n\t\t\t\t\tthis.finishRun(\n\t\t\t\t\t\tstore,\n\t\t\t\t\t\tschedule,\n\t\t\t\t\t\trun,\n\t\t\t\t\t\tasyncStatus.state === \"complete\",\n\t\t\t\t\t\ttypeof asyncStatus.error === \"string\" ? asyncStatus.error : undefined,\n\t\t\t\t\t);\n\t\t\t}\n\t\t\tconst staleAsyncRecord =\n\t\t\t\trun?.state === \"running\" &&\n\t\t\t\t!!run.asyncId &&\n\t\t\t\tNumber.isFinite(startedAt) &&\n\t\t\t\tstartedAt + STALE_LAUNCH_CLAIM_MS <= this.now() &&\n\t\t\t\t(!run.asyncDir ||\n\t\t\t\t\tasyncStatus === null ||\n\t\t\t\t\t(asyncStatus?.state === \"running\" && typeof asyncStatus.pid !== \"number\"));\n\t\t\tif (\n\t\t\t\tschedule.activeRunId &&\n\t\t\t\t(!run ||\n\t\t\t\t\trun.state !== \"running\" ||\n\t\t\t\t\t(!run.asyncId && Number.isFinite(startedAt) && startedAt + STALE_LAUNCH_CLAIM_MS <= this.now()) ||\n\t\t\t\t\tstaleAsyncRecord)\n\t\t\t) {\n\t\t\t\tif (run?.state === \"running\") {\n\t\t\t\t\trun.state = run.asyncId ? \"failed_run\" : \"failed_launch\";\n\t\t\t\t\trun.completedAt = timestamp(this.now());\n\t\t\t\t\trun.updatedAt = run.completedAt;\n\t\t\t\t\trun.error = run.asyncId\n\t\t\t\t\t\t? \"Recovered a stale async run record without a live status.\"\n\t\t\t\t\t\t: \"Recovered a stale launch claim before an async run was attached.\";\n\t\t\t\t\tstore.writeRun(schedule, run, \"schedule.run.failed\");\n\t\t\t\t}\n\t\t\t\tschedule.activeRunId = undefined;\n\t\t\t\tschedule.updatedAt = timestamp(this.now());\n\t\t\t\tstore.write(schedule);\n\t\t\t\tfs.rmSync(path.join(store.directory(schedule.id), \"active.lock\"), { force: true });\n\t\t\t}\n\t\t}\n\t\tif (schedule.paused) return;\n\t\tconst next = nextRunAt(schedule);\n\t\tif (next === undefined) return;\n\t\tif (!schedule.activeRunId && next < this.now() && schedule.catchUp === \"none\")\n\t\t\tthis.recordMissed(store, schedule, next, \"timer\");\n\t\tthis.arm(schedule, store);\n\t}\n\n\tprivate arm(schedule: ScheduleRecord, store: ScheduleStore): void {\n\t\tthis.clearTimer(store, schedule.id);\n\t\tif (schedule.paused) return;\n\t\tconst next = nextRunAt(schedule);\n\t\tif (next === undefined) return;\n\t\tconst timer = this.timersApi.setTimeout(\n\t\t\t() => void this.fire(store, schedule.id),\n\t\t\tMath.min(Math.max(0, next - this.now()), MAX_TIMER_DELAY_MS),\n\t\t);\n\t\ttimer.unref?.();\n\t\tthis.timers.set(this.timerKey(store, schedule.id), timer);\n\t}\n\n\tprivate async fire(store: ScheduleStore, id: string): Promise<void> {\n\t\tthis.clearTimer(store, id);\n\t\tconst schedule = store.get(id);\n\t\tconst planned = duePlannedAt(schedule, this.now());\n\t\tif (planned === undefined || schedule.paused) return;\n\t\tif (planned > this.now()) return this.arm(schedule, store);\n\t\tawait this.launch(store, schedule, planned, \"timer\", true);\n\t}\n\n\tprivate async launch(\n\t\tstore: ScheduleStore,\n\t\tschedule: ScheduleRecord,\n\t\tplanned: number,\n\t\tdueReason: ScheduleRunRecord[\"dueReason\"],\n\t\tadvance: boolean,\n\t): Promise<ScheduleRunRecord> {\n\t\tconst now = this.now();\n\t\tconst run: ScheduleRunRecord = {\n\t\t\tschemaVersion: 1,\n\t\t\tid: this.randomId(),\n\t\t\tscheduleId: schedule.id,\n\t\t\tplannedAt: timestamp(planned),\n\t\t\tdueReason,\n\t\t\tstate: \"running\",\n\t\t\tstartedAt: timestamp(now),\n\t\t\tupdatedAt: timestamp(now),\n\t\t};\n\t\tif (schedule.activeRunId) {\n\t\t\trun.state = \"skipped\";\n\t\t\trun.completedAt = timestamp(now);\n\t\t\trun.updatedAt = run.completedAt;\n\t\t\tif (advance) {\n\t\t\t\tschedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);\n\t\t\t\tschedule.updatedAt = timestamp(now);\n\t\t\t\tstore.write(schedule);\n\t\t\t}\n\t\t\tstore.writeRun(schedule, run, \"schedule.skipped_overlap\");\n\t\t\tthis.arm(schedule, store);\n\t\t\treturn run;\n\t\t}\n\t\tconst lockPath = path.join(store.directory(schedule.id, true), \"active.lock\");\n\t\tfs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n\t\tlet lock: number;\n\t\ttry {\n\t\t\tlock = fs.openSync(lockPath, \"wx\", 0o600);\n\t\t\tfs.writeFileSync(lock, run.id, \"utf-8\");\n\t\t\tfs.closeSync(lock);\n\t\t} catch (error) {\n\t\t\tif ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\t\t\trun.state = \"skipped\";\n\t\t\trun.completedAt = timestamp(now);\n\t\t\trun.updatedAt = run.completedAt;\n\t\t\tif (advance) {\n\t\t\t\tschedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);\n\t\t\t\tschedule.updatedAt = timestamp(now);\n\t\t\t\tstore.write(schedule);\n\t\t\t}\n\t\t\tstore.writeRun(schedule, run, \"schedule.skipped_overlap\");\n\t\t\tthis.arm(schedule, store);\n\t\t\treturn run;\n\t\t}\n\t\tschedule.activeRunId = run.id;\n\t\tschedule.lastRunId = run.id;\n\t\tif (advance) schedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);\n\t\tschedule.updatedAt = timestamp(now);\n\t\tstore.write(schedule);\n\t\tstore.writeRun(schedule, run, \"schedule.run.started\");\n\t\ttry {\n\t\t\tconst result = await this.deps.launch(\n\t\t\t\texecutionParams(schedule),\n\t\t\t\tthis.requireContext(store),\n\t\t\t\tnew AbortController().signal,\n\t\t\t);\n\t\t\tconst asyncId = result.details?.asyncId ?? result.details?.runId;\n\t\t\tif (result.isError || !asyncId)\n\t\t\t\tthrow new Error(result.content.find((item) => item.type === \"text\")?.text ?? \"Scheduled launch failed.\");\n\t\t\trun.asyncId = asyncId;\n\t\t\trun.asyncDir = result.details?.asyncDir;\n\t\t\trun.updatedAt = timestamp(this.now());\n\t\t\tstore.writeRun(schedule, run, \"schedule.run.attached_async\");\n\t\t\tthis.arm(schedule, store);\n\t\t\treturn run;\n\t\t} catch (error) {\n\t\t\trun.state = \"failed_launch\";\n\t\t\trun.completedAt = timestamp(this.now());\n\t\t\trun.updatedAt = run.completedAt;\n\t\t\trun.error = error instanceof Error ? error.message : String(error);\n\t\t\tschedule.activeRunId = undefined;\n\t\t\tschedule.updatedAt = timestamp(this.now());\n\t\t\tstore.write(schedule);\n\t\t\tstore.writeRun(schedule, run, \"schedule.run.failed\");\n\t\t\tfs.rmSync(lockPath, { force: true });\n\t\t\tthis.arm(schedule, store);\n\t\t\treturn run;\n\t\t}\n\t}\n\n\tprivate finishRun(\n\t\tstore: ScheduleStore,\n\t\tschedule: ScheduleRecord,\n\t\trun: ScheduleRunRecord,\n\t\tsuccess: boolean,\n\t\terror?: string,\n\t): void {\n\t\tconst now = this.now();\n\t\tconst next = nextRunAt(schedule);\n\t\tif (next !== undefined && next <= now) {\n\t\t\tconst planned = duePlannedAt(schedule, now)!;\n\t\t\tconst skipped: ScheduleRunRecord = {\n\t\t\t\tschemaVersion: 1,\n\t\t\t\tid: this.randomId(),\n\t\t\t\tscheduleId: schedule.id,\n\t\t\t\tplannedAt: timestamp(planned),\n\t\t\t\tdueReason: \"timer\",\n\t\t\t\tstate: \"skipped\",\n\t\t\t\tcompletedAt: timestamp(now),\n\t\t\t\tupdatedAt: timestamp(now),\n\t\t\t};\n\t\t\tschedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, now);\n\t\t\tstore.writeRun(schedule, skipped, \"schedule.skipped_overlap\");\n\t\t}\n\t\trun.state = success ? \"completed\" : \"failed_run\";\n\t\trun.completedAt = timestamp(now);\n\t\trun.updatedAt = run.completedAt;\n\t\tif (!success && error) run.error = error;\n\t\tschedule.activeRunId = undefined;\n\t\tschedule.updatedAt = timestamp(now);\n\t\tstore.write(schedule);\n\t\tfs.rmSync(path.join(store.directory(schedule.id), \"active.lock\"), { force: true });\n\t\tstore.writeRun(schedule, run, success ? \"schedule.run.completed\" : \"schedule.run.failed\");\n\t\tthis.arm(schedule, store);\n\t}\n\n\tprivate recordMissed(\n\t\tstore: ScheduleStore,\n\t\tschedule: ScheduleRecord,\n\t\tplanned: number,\n\t\tdueReason: ScheduleRunRecord[\"dueReason\"],\n\t): ScheduleRunRecord {\n\t\tconst run: ScheduleRunRecord = {\n\t\t\tschemaVersion: 1,\n\t\t\tid: this.randomId(),\n\t\t\tscheduleId: schedule.id,\n\t\t\tplannedAt: timestamp(planned),\n\t\t\tdueReason,\n\t\t\tstate: \"missed\",\n\t\t\tcompletedAt: timestamp(this.now()),\n\t\t\tupdatedAt: timestamp(this.now()),\n\t\t};\n\t\tschedule.trigger.nextRunAt = nextAfter(schedule.trigger, planned, this.now());\n\t\tschedule.updatedAt = timestamp(this.now());\n\t\tstore.write(schedule);\n\t\tstore.writeRun(schedule, run, \"schedule.missed\");\n\t\treturn run;\n\t}\n\n\tprivate selectProject(cwd: string, ctx: ExtensionContext): void {\n\t\tconst projectCwd = path.resolve(cwd);\n\t\tconst root = scheduledRunStorePath(projectCwd, undefined, this.deps.storeRoot);\n\t\tif (path.resolve(ctx.cwd) === projectCwd) this.contexts.set(root, snapshotContext(ctx, projectCwd));\n\t\telse if (!this.contexts.has(root))\n\t\t\tthrow new Error(`Cannot use project '${projectCwd}' until that project has been opened in this runtime.`);\n\t\tlet store = this.stores.get(root);\n\t\tif (!store) {\n\t\t\tstore = new ScheduleStore(root, this.deps.storeRoot === undefined ? projectCwd : undefined);\n\t\t\tthis.stores.set(root, store);\n\t\t\tthis.restore(store);\n\t\t}\n\t\tthis.store = store;\n\t}\n\n\tprivate resolve(params: SubagentParamsLike): ScheduleRecord {\n\t\tconst id = params.id?.trim();\n\t\tif (!id) throw new Error(`${params.action} requires id.`);\n\t\treturn this.requireStore().get(id);\n\t}\n\n\tprivate requireStore(): ScheduleStore {\n\t\tif (!this.store) throw new Error(\"Schedule store is unavailable.\");\n\t\treturn this.store;\n\t}\n\n\tprivate requireContext(store: ScheduleStore): ExtensionContext {\n\t\tconst ctx = this.contexts.get(store.root);\n\t\tif (!ctx) throw new Error(\"Schedule runtime context is unavailable.\");\n\t\treturn ctx;\n\t}\n\n\tprivate timerKey(store: ScheduleStore, id: string): string {\n\t\treturn `${store.root}\\0${id}`;\n\t}\n\n\tprivate clearTimer(store: ScheduleStore, id: string): void {\n\t\tconst key = this.timerKey(store, id);\n\t\tconst timer = this.timers.get(key);\n\t\tif (!timer) return;\n\t\tthis.timersApi.clearTimeout(timer);\n\t\tthis.timers.delete(key);\n\t}\n\n\tprivate stopTimers(): void {\n\t\tfor (const timer of this.timers.values()) this.timersApi.clearTimeout(timer);\n\t\tthis.timers.clear();\n\t}\n}\n\nexport function createScheduledRunManager(deps: ScheduledRunManagerDeps): ScheduledRunManager {\n\treturn new ScheduledRunManager(deps);\n}\n"]}