@roll-agent/runtime 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/agent-session.js +1 -1
- package/dist/engine/system-prompt.js +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/scheduler/calendar.d.ts +66 -0
- package/dist/scheduler/calendar.js +1 -0
- package/dist/scheduler/database-version.d.ts +6 -0
- package/dist/scheduler/database-version.js +1 -0
- package/dist/scheduler/read-only-sqlite.d.ts +4 -0
- package/dist/scheduler/read-only-sqlite.js +1 -0
- package/dist/scheduler/schedule-store.d.ts +4 -0
- package/dist/scheduler/schedule-store.js +1 -1
- package/dist/scheduler/trigger.d.ts +160 -1
- package/dist/scheduler/trigger.js +1 -1
- package/dist/scheduler/types.d.ts +2 -0
- package/dist/tool-bridge/invalid-schedule-retry.d.ts +6 -0
- package/dist/tool-bridge/invalid-schedule-retry.js +1 -0
- package/dist/tool-bridge/schedule-create-input.d.ts +91 -0
- package/dist/tool-bridge/schedule-create-input.js +1 -0
- package/dist/tool-bridge/schedule-tool.js +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{isAbsolute as i,resolve as r}from"node:path";import{randomUUID as s}from"node:crypto";import{DatabaseSync as d}from"node:sqlite";import{expandTilde as a}from"../store/thread-store.js";import{SCHEDULER_LIMITS as o}from"./limits.js";import{TRIGGER_KINDS as u,computeNextRunAtMs as c,parseTriggerJson as l,triggerSpecSchema as h}from"./trigger.js";import{CANCEL_INVOCATION_OUTCOMES as E,COMPLETE_INVOCATION_OUTCOMES as p,EXECUTOR_LIVENESS as _,INVOCATION_FAILURE_OUTCOMES as T,INVOCATION_LIVE_STATUSES as m,INVOCATION_MODES as v,INVOCATION_STATUSES as N,INVOCATION_TERMINAL_STATUSES as R,INVOCATION_TREE_LIVENESS as L,TRACKED_LEADER_STATES as S,SCHEDULE_STATUSES as g,SCHEDULE_STORE_ERROR_CODES as f,ScheduleStoreError as I}from"./types.js";const A=8,x=15e3,w=R.map(()=>"?").join(", "),D={dead:"dead",notDead:"not-dead",deferred:"deferred"},O={valid:"valid",invalid:"invalid"};function M(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(A)}`)}function C(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>A)throw M(t.user_version);const n=e.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name IN ('schedules', 'invocations')").all(),i=new Set(n.map(e=>e.name));if(t.user_version<1||!i.has("schedules")||!i.has("invocations"))throw new Error("not a valid authoritative scheduler database")}function b(e){if(!H(e.mode))throw new I(f.invalid,"Invalid reference mode");return{invocationId:e.invocation_id,attempt:e.attempt,scheduleId:e.schedule_id,threadId:e.thread_id,threadsDir:e.threads_dir,name:e.name,cwd:e.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:e.created_at}}function U(e){if(!Number.isSafeInteger(e.expectedAttempt)||e.expectedAttempt<1||0===e.threadId.trim().length||!i(e.threadsDir))throw new I(f.invalid,"会话关联需要有效的 attempt、会话 ID 和绝对存储路径")}const y=Object.values(v),k=Object.values(N);function H(e){return y.includes(e)}function F(e){return k.includes(e)}function W(e){try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}function P(e){if(null===e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(B):[]}catch{return[]}}function B(e){return"number"==typeof e&&Number.isInteger(e)&&e>0}function G(e){return e===S.alive||e===S.exited||e===S.unknown}function X(e){return B(e)?{pgid:e,leaderState:S.unknown}:"object"==typeof e&&null!==e&&"pgid"in e&&B(e.pgid)&&"leaderState"in e&&G(e.leaderState)?"startToken"in e?"string"==typeof e.startToken&&e.startToken.length>0?{pgid:e.pgid,leaderState:e.leaderState,startToken:e.startToken}:void 0:{pgid:e.pgid,leaderState:e.leaderState}:void 0}function $(e){if(null===e)return{kind:O.valid,groups:[]};try{const t=JSON.parse(e);if(!Array.isArray(t))return{kind:O.invalid};const n=new Map;for(const e of t){const t=X(e);if(void 0===t)return{kind:O.invalid};n.set(t.pgid,t)}return{kind:O.valid,groups:[...n.values()]}}catch{return{kind:O.invalid}}}function q(e){return new I(f.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function Y(e,t){const n=$(e);if(n.kind===O.invalid)throw q(t);return n.groups}function j(e){const t=new Map;for(const n of e)!Number.isInteger(n.pgid)||n.pgid<=0||t.set(n.pgid,void 0!==n.startToken&&n.startToken.length>0?{pgid:n.pgid,leaderState:n.leaderState,startToken:n.startToken}:{pgid:n.pgid,leaderState:n.leaderState});return JSON.stringify([...t.values()])}function K(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const V="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",J=[N.claimed,N.running,N.retry],z=J;function Q(e){return null!==e.executor_pid&&null!==e.executor_start_token?{pid:e.executor_pid,startToken:e.executor_start_token}:void 0}function Z(e){if(!H(e.mode)||!F(e.status))throw new I(f.invalid,`invocation ${e.id} 的 mode/status 非法: ${e.mode}/${e.status}`);const t=Y(e.tree_tracked_pgids,e.id);return{id:e.id,scheduleId:e.schedule_id,mode:e.mode,status:e.status,scheduledForMs:e.scheduled_for,attempt:e.attempt,maxAttempts:e.max_attempts,executor:Q(e),claimedBy:e.claimed_by??void 0,leaseUntilMs:e.lease_until??void 0,retryAtMs:e.retry_at??void 0,threadId:e.thread_id??void 0,outputExcerpt:e.output_excerpt??void 0,error:e.error??void 0,pendingActions:W(e.pending_actions_json),treeTrackedPgids:t.map(e=>e.pgid),treeTrackedGroups:t,treeUnsettled:1===e.tree_unsettled,treeSurvivorPids:P(e.tree_survivor_pids),createdAtMs:e.created_at,startedAtMs:e.started_at??void 0,finishedAtMs:e.finished_at??void 0}}function ee(e){return Object.values(g).some(t=>t===e)}function te(e,t){if(!ee(e.status))throw new I(f.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);const n=ie(e.max_rounds??void 0),i=e.rounds_started??0;if("number"!=typeof i||!Number.isSafeInteger(i)||i<0)throw new I(f.invalid,`schedule ${e.id} 的 roundsStarted 非法`);const r=void 0===n?void 0:t.prepare("SELECT * FROM invocations WHERE schedule_id = ? AND mode = ? ORDER BY scheduled_for DESC, rowid DESC LIMIT 1").get(e.id,v.scheduled),s=void 0===r?void 0:{status:Z(r).status,treeUnsettled:1===r.tree_unsettled||null!==r.tree_tracked_pgids&&"[]"!==r.tree_tracked_pgids.trim()};return{id:e.id,name:e.name,prompt:e.prompt,cwd:e.cwd,trigger:l(e.trigger_json),status:e.status,authorityDigest:e.authority_digest??void 0,maxRunMs:ne(e.max_run_ms),maxRounds:n,roundsStarted:i,...void 0===s?{}:{lastScheduledRun:s},nextRunAtMs:e.next_run_at??void 0,lastRunAtMs:e.last_run_at??void 0,lastError:e.last_error??void 0,createdAtMs:e.created_at,updatedAtMs:e.updated_at}}function ne(e){if(null!=e){if("number"!=typeof e||!Number.isInteger(e)||e<o.minMaxRunMs||e>o.maxRunCeilingMs)throw new I(f.invalid,`maxRunMs 必须是 ${String(o.minMaxRunMs)}..${String(o.maxRunCeilingMs)} 的整数毫秒`);return e}}function ie(e){if(void 0!==e){if("number"!=typeof e||!Number.isSafeInteger(e)||e<1)throw new I(f.invalid,"maxRounds 必须是正安全整数");return e}}function re(e){const t=e.name.trim();if(0===t.length||t.length>o.maxNameChars)throw new I(f.invalid,`name 必须为 1..${String(o.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>o.maxPromptChars)throw new I(f.invalid,`prompt 必须为 1..${String(o.maxPromptChars)} 个字符`);if(!i(e.cwd))throw new I(f.invalid,"cwd 必须是绝对路径");const r=h.safeParse(e.trigger);if(!r.success)throw new I(f.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:r.data,maxRunMs:ne(e.maxRunMs),maxRounds:ie(e.maxRounds)}}export class ScheduleStore{db;maxSchedules;claimLeaseMs;retryBudget;retryBackoffMs;executorLiveness;treeLiveness;maxLivenessProbesPerClaim;livenessProbeDeferralMs;invocationRetentionPerSchedule;invocationRetentionMs;constructor(i,s={}){this.maxSchedules=s.maxSchedules??50,this.claimLeaseMs=s.claimLeaseMs??o.claimLeaseMs,this.retryBudget=s.retryBudget??o.retryBudget,this.retryBackoffMs=s.retryBackoffMs??o.retryBackoffMs,this.executorLiveness=s.executorLiveness??(()=>_.unknown),this.treeLiveness=s.treeLiveness,this.maxLivenessProbesPerClaim=s.maxLivenessProbesPerClaim??o.maxLivenessProbesPerClaim,this.livenessProbeDeferralMs=s.livenessProbeDeferralMs??o.livenessProbeDeferralMs,this.invocationRetentionPerSchedule=s.invocationRetentionPerSchedule??o.invocationRetentionPerSchedule,this.invocationRetentionMs=s.invocationRetentionMs??o.invocationRetentionMs;const u=a(i);if(!0===s.requireExistingDatabase&&!t(u))throw new Error("authoritative scheduler database does not exist");t(u)||n(u,{recursive:!0,mode:448}),"win32"!==process.platform&&e(u,448);const c=r(u,"schedules.db");if(!0===s.requireExistingDatabase&&!t(c))throw new Error("authoritative scheduler database does not exist");if(this.db=new d(c),!0===s.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(x)}`),C(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{e(c,384)}catch(e){throw this.db.close(),e}try{this.init()}catch(e){throw this.db.close(),e}}init(){this.db.exec(`PRAGMA busy_timeout = ${String(x)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>A)throw M(e.user_version);const t=e.user_version<7;t&&this.db.exec("PRAGMA foreign_keys = OFF;");try{this.transaction(()=>{const e=this.db.prepare("PRAGMA user_version").get().user_version;if(e>A)throw M(e);const t=e<7&&void 0!==this.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'schedules'").get();if(this.db.exec(`CREATE TABLE IF NOT EXISTS schedules (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'running', 'retry', 'completed', 'needs_confirmation', 'failed')),\n scheduled_for INTEGER NOT NULL,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT ${String(o.retryBudget)},\n executor_pid INTEGER,\n executor_start_token TEXT,\n executor_probed_at INTEGER,\n claimed_by TEXT,\n ownership_token TEXT,\n lease_until INTEGER,\n retry_at INTEGER,\n thread_id TEXT,\n output_excerpt TEXT,\n error TEXT,\n pending_actions_json TEXT NOT NULL DEFAULT '[]',\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER,\n tree_tracked_pgids TEXT,\n tree_unsettled INTEGER NOT NULL DEFAULT 0,\n tree_survivor_pids TEXT,\n UNIQUE (schedule_id, mode, scheduled_for)\n );\n CREATE INDEX IF NOT EXISTS idx_schedules_due\n ON schedules (next_run_at) WHERE status = 'active' AND next_run_at IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_invocations_live\n ON invocations (schedule_id) WHERE status IN ('pending', 'claimed', 'running', 'retry');\n CREATE TABLE IF NOT EXISTS schedule_thread_refs (\n invocation_id TEXT NOT NULL,\n attempt INTEGER NOT NULL CHECK (attempt > 0),\n schedule_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n threads_dir TEXT NOT NULL,\n name TEXT NOT NULL,\n cwd TEXT NOT NULL,\n scheduled_for INTEGER NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n created_at INTEGER NOT NULL,\n PRIMARY KEY (invocation_id, attempt)\n );\n CREATE INDEX IF NOT EXISTS idx_schedule_thread_refs_history\n ON schedule_thread_refs (schedule_id, scheduled_for DESC, invocation_id, attempt DESC);`),e<7&&this.addMissingColumns(),t&&this.migrateRoundsInTransaction(),this.db.exec("CREATE TABLE IF NOT EXISTS schedule_extensions (\n request_id TEXT PRIMARY KEY CHECK (length(request_id) BETWEEN 1 AND 128),\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n expected_max_rounds INTEGER NOT NULL CHECK (typeof(expected_max_rounds) = 'integer' AND expected_max_rounds BETWEEN 1 AND 9007199254740991),\n additional_rounds INTEGER NOT NULL CHECK (typeof(additional_rounds) = 'integer' AND additional_rounds BETWEEN 1 AND 9007199254740991),\n authority_digest TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE (schedule_id, expected_max_rounds)\n );"),t&&0!==this.db.prepare("PRAGMA foreign_key_check").all().length)throw new I(f.invalid,"scheduler 迁移失败:外键关联不完整");this.db.exec(`PRAGMA user_version = ${String(A)};`)})}finally{t&&this.db.exec("PRAGMA foreign_keys = ON;")}}migrateRoundsInTransaction(){this.db.exec("CREATE TABLE schedules_v7 (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n INSERT INTO schedules_v7 (rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at)\n SELECT rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at FROM schedules;\n DROP TABLE schedules;\n ALTER TABLE schedules_v7 RENAME TO schedules;\n CREATE INDEX idx_schedules_due ON schedules (next_run_at)\n WHERE status = 'active' AND next_run_at IS NOT NULL;")}addMissingColumns(){const e=[{table:"schedules",column:"authority_digest",definition:"TEXT"},{table:"schedules",column:"max_run_ms",definition:"INTEGER"},{table:"invocations",column:"max_attempts",definition:`INTEGER NOT NULL DEFAULT ${String(o.retryBudget)}`},{table:"invocations",column:"executor_pid",definition:"INTEGER"},{table:"invocations",column:"executor_start_token",definition:"TEXT"},{table:"invocations",column:"executor_probed_at",definition:"INTEGER"},{table:"invocations",column:"tree_tracked_pgids",definition:"TEXT"},{table:"invocations",column:"tree_unsettled",definition:"INTEGER NOT NULL DEFAULT 0"},{table:"invocations",column:"tree_survivor_pids",definition:"TEXT"}];for(const t of e){this.db.prepare(`PRAGMA table_info(${t.table})`).all().map(e=>e.name).includes(t.column)||this.db.exec(`ALTER TABLE ${t.table} ADD COLUMN ${t.column} ${t.definition}`)}this.clampOversizedIntervals()}clampOversizedIntervals(){const e=this.db.prepare("SELECT id, trigger_json FROM schedules").all();for(const t of e){let e;try{e=JSON.parse(t.trigger_json)}catch{continue}"object"!=typeof e||null===e||!("kind"in e)||e.kind!==u.interval||!("everyMs"in e)||"number"!=typeof e.everyMs||e.everyMs<=o.maxIntervalMs||this.db.prepare("UPDATE schedules SET trigger_json = ?, status = ?, last_error = ?, updated_at = ?\n WHERE id = ?").run(JSON.stringify({kind:u.interval,everyMs:o.maxIntervalMs}),g.paused,`原间隔 ${String(e.everyMs)} ms 超过上限 365 天,已按 365 天登记并暂停;确认后 roll schedule resume`,Date.now(),t.id)}}transaction(e){this.db.exec("BEGIN IMMEDIATE");try{const t=e();return this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}createSchedule(e,t=Date.now()){const n=re(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t))}createScheduleIdempotent(e,t=Date.now()){const n=re(e),i=String(o.maxRunMs);return this.transaction(()=>{const r=this.db.prepare(`SELECT * FROM schedules\n WHERE status = ? AND prompt = ? AND cwd = ? AND trigger_json = ?\n AND COALESCE(max_run_ms, ${i}) = COALESCE(?, ${i})\n AND max_rounds IS ?`).get(g.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??null,n.maxRounds??null);if(void 0!==r){const n=e.authorityDigest??null;return(r.authority_digest??null)!==n?(this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(n,t,r.id),{created:!1,reauthorized:!0,schedule:this.requireSchedule(r.id)}):{created:!1,reauthorized:!1,schedule:te(r,this.db)}}return{created:!0,reauthorized:!1,schedule:this.insertScheduleInTransaction(n,e,t)}})}insertScheduleInTransaction(e,t,n){if(this.db.prepare("SELECT COUNT(*) AS count FROM schedules").get().count>=this.maxSchedules)throw new I(f.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);const i=s(),r=!0===t.fireImmediately?n:c(e.trigger,n);return this.db.prepare("INSERT INTO schedules\n (id, name, prompt, cwd, trigger_json, status, authority_digest, max_run_ms,\n next_run_at, created_at, updated_at, max_rounds)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),g.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n,e.maxRounds??null),this.requireSchedule(i)}setAuthorityDigest(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes}resumeSchedule(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(i?.status===g.completed)throw new I(f.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");const r=this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(g.active,t,n,e);return this.reconcileRoundsInTransaction(e,n),1===r.changes})}getScheduleExtension(e){const t=this.db.prepare("SELECT * FROM schedule_extensions WHERE request_id = ?").get(e);return void 0===t?void 0:{requestId:t.request_id,scheduleId:t.schedule_id,expectedMaxRounds:t.expected_max_rounds,additionalRounds:t.additional_rounds,authorityDigest:t.authority_digest,createdAtMs:t.created_at}}extendSchedule(e,t=Date.now()){const n=ie(e.additionalRounds),i=ie(e.expectedMaxRounds);if(void 0===n||void 0===i||!Number.isSafeInteger(i+n))throw new I(f.invalid,"追加轮数及原上限必须为正安全整数,追加后的总轮数不得溢出");if("string"!=typeof e.requestId||0===e.requestId.trim().length||e.requestId.length>128)throw new I(f.invalid,"requestId 必须为 1..128 字符的非空请求标识;重试时复用原标识");if("string"!=typeof e.authorityDigest||0===e.authorityDigest.trim().length)throw new I(f.invalid,"追加轮数必须重新核验工作区权限");return this.transaction(()=>{const r=this.getScheduleExtension(e.requestId);if(void 0!==r){if(r.scheduleId!==e.scheduleId||r.expectedMaxRounds!==i||r.additionalRounds!==n)throw new I(f.invalid,"requestId 已用于不同的追加请求;请检查原请求参数");return{extended:!1,schedule:this.requireSchedule(e.scheduleId)}}const s=this.requireSchedule(e.scheduleId);if(void 0===s.maxRounds)throw new I(f.invalid,"不限轮数的任务不需要追加额度");if(s.maxRounds!==i)throw new I(f.invalid,"任务轮数上限已变化,追加请求已过期;请刷新任务并核对原请求结果");if(s.status!==g.completed||s.roundsStarted!==s.maxRounds)throw new I(f.invalid,"仅能为已结束且轮数用尽的有限任务追加轮数");if(void 0!==this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND\n (status IN ('pending', 'claimed', 'running', 'retry') OR tree_unsettled = 1\n OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]')) LIMIT 1").get(s.id))throw new I(f.invalid,"任务仍有待执行、运行中或未清场的记录,暂不能追加轮数;请先完成运行收尾");const d=c(s.trigger,t);if(!Number.isSafeInteger(t)||!Number.isSafeInteger(d)||!Number.isFinite(new Date(d).getTime()))throw new I(f.invalid,"追加时间或下次执行时间超出有效范围");return this.db.prepare("INSERT INTO schedule_extensions (request_id, schedule_id, expected_max_rounds,\n additional_rounds, authority_digest, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(e.requestId,s.id,i,n,e.authorityDigest,t),this.db.prepare("UPDATE schedules SET max_rounds = ?, authority_digest = ?, status = ?,\n next_run_at = ?, updated_at = ? WHERE id = ?").run(i+n,e.authorityDigest,g.active,d,t,s.id),{extended:!0,schedule:this.requireSchedule(s.id)}})}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?te(t,this.db):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(e=>te(e,this.db))}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(void 0===i)return!1;if(i.status===g.completed){if(t===g.active)throw new I(f.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");return!0}if(t===g.completed)throw new I(f.invalid,"结束状态由调度器在轮数用尽且运行结算后设置");if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===g.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,v.scheduled,N.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return this.reconcileRoundsInTransaction(e,n),!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(N.running).map(Z)}listActiveWorkerInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status IN (?, ?)\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC").all(N.claimed,N.running).map(Z)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${V}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...J).map(Z)}prepareWorkerShutdown(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT id FROM invocations WHERE claimed_by = ? AND status = ?").all(e,N.claimed);for(const e of i)this.finishInvocationAsFailedInTransaction(e.id,t,n);return this.db.prepare("SELECT * FROM invocations WHERE claimed_by = ? AND status = ?\n ORDER BY started_at ASC, created_at ASC").all(e,N.running).map(e=>{if(null===e.ownership_token)throw new I(f.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new I(f.invalid,`running invocation ${e.id} 无法加载 worker claim`);return t})})}removeSchedule(e,t={}){return this.transaction(()=>{if(!0!==t.abandon){const t=this.db.prepare(`SELECT id, status FROM invocations WHERE schedule_id = ? AND ${V} LIMIT 1`).get(e,...J);if(void 0!==t)throw new I(f.invalid,`定时任务 ${e} 仍有未结束的运行(invocation ${t.id},${t.status});先 cancel --kill 清场,或显式 --abandon 放弃追踪`)}return 1===this.db.prepare("DELETE FROM schedules WHERE id = ?").run(e).changes})}enqueueManualInvocation(e,t=Date.now(),n={}){const i=Math.max(1,Math.floor(n.maxAttempts??this.retryBudget));return this.transaction(()=>{this.requireSchedule(e);const n=s();for(let r=0;r<1e3;r+=1){if(1===this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, created_at)\n VALUES (?, ?, ?, ?, ?, 0, ?, ?)").run(n,e,v.manual,N.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new I(f.invalid,`schedule ${e} 的手动触发入队失败`)})}claimPendingInvocation(e,t,n=Date.now()){return this.transaction(()=>{const i=s();return 1===this.db.prepare(`UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?, attempt = 1,\n retry_at = NULL\n WHERE id = ? AND status = ?\n AND NOT EXISTS (\n SELECT 1 FROM invocations o\n WHERE o.schedule_id = (SELECT schedule_id FROM invocations WHERE id = ?)\n AND o.id != ? AND ${V})`).run(N.claimed,t,i,n+this.claimLeaseMs,e,N.pending,e,e,...J).changes?this.loadClaim(e,i):void 0})}findLiveRun(e){const t=this.db.prepare(`SELECT * FROM invocations WHERE schedule_id = ? AND ${V}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...J);return t?Z(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,N.pending).changes}cancelInvocation(e,t,n=Date.now(),i={}){return this.transaction(()=>{const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return this.cancelInvocationInTransaction(r,t,n,i)})}finalizeCancellation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id),n=this.revisionMismatch(t,{...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}});if(void 0!==n)return n;if(!0!==e.abandon&&void 0!==t&&$(t.tree_tracked_pgids).kind===O.invalid)return E.treeUnsettled;const i=void 0===t?void 0:this.executorBlocksCancel(t,!0===e.abandon);if(void 0!==i)return i;if(void 0!==e.tree&&void 0!==t&&z.includes(t.status)){if(!this.writeInvocationTreeInTransaction({id:e.id,unsettled:e.tree.unsettled,...void 0!==e.tree.trackedGroups?{trackedGroups:e.tree.trackedGroups}:{},...void 0!==e.tree.survivorPids?{survivorPids:e.tree.survivorPids}:{},expectedAttempt:e.expectedAttempt}))return E.ownershipChanged}const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);return this.cancelInvocationInTransaction(r,e.reason,e.nowMs,{...!0===e.abandon?{abandon:!0}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}},{executorChecked:!0})})}executorBlocksCancel(e,t){if(e.status!==N.running||t)return;const n=Q(e),i=void 0===n?_.unknown:this.executorLiveness(n);return i===_.alive||i===_.descendants?E.executorAlive:i===_.unknown?E.executorUnknown:void 0}revisionMismatch(e,t){return void 0!==e&&F(e.status)?void 0!==t.expectedOwnershipToken&&e.ownership_token!==t.expectedOwnershipToken||void 0!==t.expectedAttempt&&e.attempt!==t.expectedAttempt||void 0!==t.expectedClaimedBy&&(e.claimed_by??void 0)!==t.expectedClaimedBy?E.ownershipChanged:void 0:E.notFound}cancelInvocationInTransaction(e,t,n,i,r={}){const s=this.revisionMismatch(e,i);if(void 0!==s)return s;if(void 0===e||!F(e.status))return E.notFound;if(!m.includes(e.status))return E.terminal;const d=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==d)return d;const a=!0===i.abandon;return!a&&this.treeBlocksTerminal(e)?E.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:a})?E.cancelled:E.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===N.retry?"retry_at":"lease_until";this.db.prepare(`UPDATE invocations SET ${n} = ? WHERE id = ?`).run(t+this.claimLeaseMs,e.id)}pauseInvalidScheduleInTransaction(e,t,n){this.db.prepare("UPDATE schedules SET status = ?, last_error = ?, updated_at = ? WHERE id = ?").run(g.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return te(e,this.db)}catch(n){return void this.pauseInvalidScheduleInTransaction(e.id,n,t)}}loadScheduleForClaimInTransaction(e,t){const n=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return void 0===n?void 0:this.decodeScheduleForClaimInTransaction(n,t)}hasOtherLiveRunInTransaction(e,t){return void 0!==this.db.prepare(`SELECT 1 AS present FROM invocations\n WHERE schedule_id = ? AND id != ? AND ${V} LIMIT 1`).get(e,t,...J)}claimDue(e){const t=e.heldInvocationIds??new Set;return this.transaction(()=>{const n=[],i=this.db.prepare("SELECT i.*, s.status AS schedule_status FROM invocations i\n JOIN schedules s ON s.id = i.schedule_id\n WHERE i.status = ?\n OR (i.status = ? AND i.retry_at IS NOT NULL AND i.retry_at <= ?)\n OR (i.status IN (?, ?) AND i.lease_until IS NOT NULL AND i.lease_until <= ?)\n ORDER BY i.scheduled_for ASC, i.created_at ASC").all(N.pending,N.retry,e.nowMs,N.claimed,N.running,e.nowMs),r=[],d=i.filter(e=>e.status===N.running&&!t.has(e.id)&&void 0!==Q(e));d.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const a=new Map;let o=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of d){const n=Q(t);void 0!==n&&(o<=0?a.set(t.id,D.deferred):(o-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),a.set(t.id,this.executorLiveness(n)===_.dead?D.dead:D.notDead)))}for(const n of i){const i=n.status===N.claimed||n.status===N.running,s=a.get(n.id);!i||!t.has(n.id)&&s!==D.notDead?s!==D.deferred?r.push(n):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.livenessProbeDeferralMs,n.id):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.claimLeaseMs,n.id)}if(e.limit<=0)return n;for(const t of r){if(n.length>=e.limit)break;if($(t.tree_tracked_pgids).kind===O.invalid){this.deferLiveRowInTransaction(t,e.nowMs);continue}const i=this.loadScheduleForClaimInTransaction(t.schedule_id,e.nowMs);if(void 0===i){t.status!==N.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===v.scheduled&&i.status===g.paused){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.finishInvocationAsFailedInTransaction(t.id,"任务已暂停,放弃本次运行",e.nowMs);continue}if(this.hasOtherLiveRunInTransaction(t.schedule_id,t.id))continue;const r=t.status===N.pending?1:t.attempt+1;if(r>t.max_attempts){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.markTerminalFailureInTransaction(t,`重试预算耗尽(共 ${String(t.max_attempts)} 次尝试):${t.error??"exec 未成功完成"}`,e.nowMs);continue}const d=s();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(N.claimed,e.workerId,d,e.nowMs+this.claimLeaseMs,r,t.id);const a=this.loadClaim(t.id,d);void 0!==a&&n.push(a)}if(n.length>=e.limit)return n;const u=this.db.prepare("SELECT s.* FROM schedules s\n WHERE s.status = ? AND s.next_run_at IS NOT NULL AND s.next_run_at <= ?\n AND (s.max_rounds IS NULL OR s.rounds_started < s.max_rounds)\n AND NOT EXISTS (\n SELECT 1 FROM invocations i\n WHERE i.schedule_id = s.id AND i.status IN (?, ?, ?, ?))\n ORDER BY s.next_run_at ASC, s.created_at ASC\n LIMIT ?").all(g.active,e.nowMs,N.pending,N.claimed,N.running,N.retry,this.maxSchedules);for(const t of u){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=s(),d=s(),a=this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, claimed_by,\n ownership_token, lease_until, created_at)\n VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)").run(r,t.id,v.scheduled,N.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,d,e.nowMs+this.claimLeaseMs,e.nowMs),o=Number(a.changes);if(this.db.prepare("UPDATE schedules SET rounds_started = rounds_started + ?,\n next_run_at = CASE WHEN max_rounds IS NOT NULL AND rounds_started + ? >= max_rounds\n THEN NULL ELSE ? END, updated_at = ? WHERE id = ?").run(o,o,c(i.trigger,e.nowMs),e.nowMs,t.id),1===a.changes){const e=this.loadClaim(r,d);void 0!==e&&n.push(e)}}return n})}beginInvocation(e,t,n=Date.now(),i){return 1===this.db.prepare("UPDATE invocations\n SET status = ?, started_at = ?, lease_until = ?, executor_pid = ?,\n executor_start_token = ?, executor_probed_at = NULL\n WHERE id = ? AND ownership_token = ? AND status = ?").run(N.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,N.claimed).changes?this.loadClaim(e,t):void 0}renewLease(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE invocations SET lease_until = ?\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n+this.claimLeaseMs,e,t,N.claimed,N.running).changes}completeInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.ownership_token!==e.ownershipToken||t.status!==N.claimed&&t.status!==N.running)return p.lostClaim;if(this.treeBlocksTerminal(t))return p.treeUnsettled;return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, thread_id = ?, output_excerpt = ?, pending_actions_json = ?,\n error = NULL, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(e.status,e.threadId??null,e.outputExcerpt??null,JSON.stringify(e.pendingActions??[]),e.nowMs,e.id,e.ownershipToken,N.claimed,N.running).changes?p.lostClaim:(this.db.prepare("UPDATE schedules SET last_run_at = ?, last_error = NULL, updated_at = ?\n WHERE id = (SELECT schedule_id FROM invocations WHERE id = ?)").run(e.nowMs,e.nowMs,e.id),t.mode===v.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),p.written)})}reclassifyTimedOutInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.attempt!==e.expectedAttempt||null!==t.finished_at&&t.finished_at<e.timedOutAtMs||t.status!==N.completed&&t.status!==N.needsConfirmation)return T.lostClaim;if(this.treeBlocksTerminal(t))return T.treeUnsettled;if(t.attempt>=t.max_attempts){return 1!==this.db.prepare("UPDATE invocations\n SET thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(e.id,e.expectedAttempt,N.completed,N.needsConfirmation).changes?T.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===v.scheduled&&this.getSchedule(t.schedule_id)?.status===g.paused?T.terminalPaused:T.terminal)}return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, finished_at = NULL,\n claimed_by = NULL, ownership_token = NULL, lease_until = NULL,\n thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(N.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,N.completed,N.needsConfirmation).changes?T.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),t.mode===v.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),T.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?j(t):null,i=K(e.unsettled?e.survivorPids??[]:[]),r=e.unsettled?1:0,s=e.error??null;if(void 0!==e.ownershipToken){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n,r,i,s,e.id,e.ownershipToken,N.claimed,N.running).changes}if(void 0!==e.expectedAttempt){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND attempt = ? AND status IN (?, ?, ?)").run(n,r,i,s,e.id,e.expectedAttempt,N.claimed,N.running,N.retry).changes}return!1}failInvocation(e,t,n,i=Date.now(),r={}){return this.transaction(()=>{const s=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0===s||s.ownership_token!==t||s.status!==N.claimed&&s.status!==N.running?T.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?T.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===v.scheduled&&this.getSchedule(s.schedule_id)?.status===g.paused?T.terminalPaused:T.terminal):(this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL\n WHERE id = ?").run(N.retry,i+this.retryBackoffMs,n,e),T.retryScheduled)})}registerThreadReference(e,t=Date.now()){return U(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations\n WHERE id = ? AND attempt = ? AND ownership_token = ? AND status = ?").get(e.invocationId,e.expectedAttempt,e.ownershipToken,N.running);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}backfillThreadReference(e,t=Date.now()){return U(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations WHERE id = ? AND attempt = ? AND thread_id = ?").get(e.invocationId,e.expectedAttempt,e.threadId);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}insertThreadReference(e,t,n){const i=this.db.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? AND attempt = ?").get(t.invocationId,t.expectedAttempt);if(void 0!==i){if(i.thread_id!==t.threadId||i.threads_dir!==t.threadsDir)throw new I(f.invalid,`invocation ${t.invocationId} attempt ${String(t.expectedAttempt)} 已关联其他会话`);return b(i)}const r=this.getSchedule(e.schedule_id);if(void 0!==r){if(!H(e.mode))throw new I(f.invalid,"Invalid invocation mode");return this.db.prepare("INSERT INTO schedule_thread_refs\n (invocation_id, attempt, schedule_id, thread_id, threads_dir, name, cwd,\n scheduled_for, mode, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(t.invocationId,t.expectedAttempt,r.id,t.threadId,t.threadsDir,r.name,r.cwd,e.scheduled_for,e.mode,n),{invocationId:t.invocationId,attempt:t.expectedAttempt,scheduleId:r.id,threadId:t.threadId,threadsDir:t.threadsDir,name:r.name,cwd:r.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:n}}}getInvocation(e){const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return t?Z(t):void 0}listInvocations(e,t=20){if(t<=0)return[];return this.db.prepare("SELECT * FROM invocations WHERE schedule_id = ?\n ORDER BY created_at DESC, rowid DESC LIMIT ?").all(e,t).map(Z)}pruneInvocations(e=Date.now()){return this.transaction(()=>{const t=this.db.prepare(`DELETE FROM invocations\n WHERE status IN (${w})\n AND finished_at IS NOT NULL AND finished_at < ?`).run(...R,e-this.invocationRetentionMs),n=this.db.prepare(`DELETE FROM invocations WHERE id IN (\n SELECT id FROM (\n SELECT id, ROW_NUMBER() OVER (\n PARTITION BY schedule_id\n ORDER BY COALESCE(finished_at, created_at) DESC, rowid DESC) AS rn\n FROM invocations WHERE status IN (${w}))\n WHERE rn > ?)`).run(...R,this.invocationRetentionPerSchedule);return Number(t.changes)+Number(n.changes)})}nextWakeAtMs(){return this.db.prepare("SELECT MIN(t) AS wake FROM (\n SELECT MIN(next_run_at) AS t FROM schedules\n WHERE status = ? AND next_run_at IS NOT NULL\n AND (max_rounds IS NULL OR rounds_started < max_rounds)\n UNION ALL SELECT MIN(retry_at) FROM invocations WHERE status = ?\n UNION ALL SELECT MIN(lease_until) FROM invocations WHERE status IN (?, ?)\n UNION ALL SELECT MIN(scheduled_for) FROM invocations WHERE status = ?)").get(g.active,N.retry,N.claimed,N.running,N.pending).wake??void 0}treeBlocksTerminal(e){const t=$(e.tree_tracked_pgids);return t.kind===O.invalid||(1===e.tree_unsettled||0!==t.groups.length)&&(void 0===this.treeLiveness||this.treeLiveness(Z(e))!==L.settled)}reconcileRoundsInTransaction(e,t){this.db.prepare("UPDATE schedules\n SET status = CASE\n WHEN NOT EXISTS (SELECT 1 FROM invocations i WHERE i.schedule_id = schedules.id AND i.mode = 'scheduled'\n AND (i.status IN ('pending', 'claimed', 'running', 'retry') OR i.tree_unsettled = 1\n OR (i.tree_tracked_pgids IS NOT NULL AND trim(i.tree_tracked_pgids) != '[]')))\n THEN 'completed'\n WHEN status = 'completed' THEN 'active'\n ELSE status END,\n next_run_at = NULL, updated_at = ?\n WHERE id = ? AND max_rounds IS NOT NULL AND rounds_started >= max_rounds").run(t,e)}finishInvocationAsFailedInTransaction(e,t,n,i={}){const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0!==r&&((!0===i.force||!this.treeBlocksTerminal(r))&&(this.db.prepare("UPDATE invocations\n SET status = ?, error = ?, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ?").run(N.failed,t,n,e),r.mode===v.scheduled&&this.reconcileRoundsInTransaction(r.schedule_id,n),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===v.scheduled?this.db.prepare("UPDATE schedules SET status = CASE WHEN status = 'completed' THEN status ELSE ? END, last_error = ?, updated_at = ? WHERE id = ?").run(g.paused,t,n,e.schedule_id):this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(t,n,e.schedule_id))}loadClaim(e,t){const n=this.getInvocation(e);if(void 0===n)return;const i=this.getSchedule(n.scheduleId);return void 0!==i?{invocation:n,schedule:i,ownershipToken:t}:void 0}requireInvocation(e){const t=this.getInvocation(e);if(void 0===t)throw new I(f.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new I(f.notFound,`schedule ${e} 不存在`);return t}}export const SCHEDULE_LEDGER_READ_STATUSES={ok:"ok",empty:"empty",migrationRequired:"migration-required"};export function readScheduleLedger(e){const n=r(a(e),"schedules.db");if(!t(n))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=new d(n,{readOnly:!0});try{i.exec(`PRAGMA busy_timeout = ${String(x)}; BEGIN;`);const e=i.prepare("PRAGMA user_version").get();if(![5,6,7,A].includes(e.user_version))return{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version,schedules:[]};const t=i.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all();return{status:SCHEDULE_LEDGER_READ_STATUSES.ok,schedules:t.map(e=>te(e,i))}}finally{i.close()}}function se(e,n){const i=r(a(e),"schedules.db");if(!t(i))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty};const s=new d(i,{readOnly:!0});try{s.exec(`PRAGMA busy_timeout = ${String(x)}; BEGIN;`);const e=s.prepare("PRAGMA user_version").get();return[5,6,7,A].includes(e.user_version)?{status:SCHEDULE_LEDGER_READ_STATUSES.ok,value:n(s,e.user_version>=6)}:{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version}}finally{s.close()}}function de(e,t,n){const i=e.prepare("SELECT * FROM invocations WHERE id = ?").get(t),r=n?e.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? ORDER BY attempt ASC").all(t).map(b):[],s=void 0===i?void 0:Z(i),d=s??r[0];return void 0===d?void 0:{invocationId:t,scheduleId:d.scheduleId,scheduledForMs:d.scheduledForMs,mode:d.mode,invocation:s,references:r}}function ae(e){return"SELECT id, schedule_id, scheduled_for FROM invocations"+(e?" UNION SELECT invocation_id AS id, schedule_id, scheduled_for FROM schedule_thread_refs":"")}function oe(e,t){const n=e.prepare("SELECT * FROM schedules").all().map(t=>te(t,e)),i=new Map(n.map(e=>[e.id,e])),r=e.prepare("SELECT id FROM schedules"+(t?" UNION SELECT schedule_id AS id FROM schedule_thread_refs":"")).all(),s=[];for(const{id:n}of r){const r=e.prepare(`SELECT id FROM (${ae(t)})\n WHERE schedule_id = ? ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(n),d=void 0===r?void 0:de(e,r.id,t),a=i.get(n),o=d?.references.at(-1),u=a??o;void 0!==u&&s.push({scheduleId:n,name:u.name,cwd:u.cwd,schedule:a,latestRun:d})}return s.sort((e,t)=>Number(void 0===e.schedule)-Number(void 0===t.schedule)||(t.latestRun?.scheduledForMs??t.schedule?.createdAtMs??0)-(e.latestRun?.scheduledForMs??e.schedule?.createdAtMs??0)||e.scheduleId.localeCompare(t.scheduleId))}export function readScheduleHistory(e,t={}){const n=t.limit??20,i=t.offset??0;if(!Number.isSafeInteger(n)||n<1||n>100||!Number.isSafeInteger(i)||i<0)throw new RangeError("History limit must be 1..100 and offset must be nonnegative integers");const r=se(e,(e,r)=>{const s=void 0===t.scheduleId?"":"WHERE schedule_id = ?",d=e.prepare(`SELECT id FROM (${ae(r)}) ${s}\n ORDER BY scheduled_for DESC, id DESC LIMIT ? OFFSET ?`),a=void 0===t.scheduleId?[n+1,i]:[t.scheduleId,n+1,i],o=d.all(...a),u=[];for(const{id:t}of o.slice(0,n)){const n=de(e,t,r);void 0!==n&&u.push(n)}return{tasks:oe(e,r),runs:u,hasMore:o.length>n}});return{status:r.status,...void 0===r.schemaVersion?{}:{schemaVersion:r.schemaVersion},tasks:r.value?.tasks??[],runs:r.value?.runs??[],hasMore:r.value?.hasMore??!1}}export function readScheduleRun(e,t){const n=se(e,(e,n)=>de(e,t,n));return{status:n.status,...void 0===n.schemaVersion?{}:{schemaVersion:n.schemaVersion},run:n.value}}
|
|
1
|
+
import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{isAbsolute as i,resolve as r}from"node:path";import{randomUUID as s}from"node:crypto";import{DatabaseSync as d}from"node:sqlite";import{expandTilde as a}from"../store/thread-store.js";import{SCHEDULER_LIMITS as o}from"./limits.js";import{openReadOnlySchedulerDatabase as u,enableSchedulerQueryOnly as c}from"./read-only-sqlite.js";import{SCHEDULER_SCHEMA_VERSION as l,registerSchedulerWriter as h,installSchedulerWriterGuards as E}from"./database-version.js";import{TRIGGER_KINDS as p,computeNextRunAtMs as _,computeFirstRunAtMs as m,parseTriggerJson as T,triggerSpecSchema as v}from"./trigger.js";import{CANCEL_INVOCATION_OUTCOMES as N,COMPLETE_INVOCATION_OUTCOMES as R,EXECUTOR_LIVENESS as L,INVOCATION_FAILURE_OUTCOMES as S,INVOCATION_LIVE_STATUSES as f,INVOCATION_MODES as g,INVOCATION_STATUSES as A,INVOCATION_TERMINAL_STATUSES as I,INVOCATION_TREE_LIVENESS as w,TRACKED_LEADER_STATES as x,SCHEDULE_STATUSES as D,SCHEDULE_STORE_ERROR_CODES as O,ScheduleStoreError as M}from"./types.js";const C=15e3,b=I.map(()=>"?").join(", "),y={dead:"dead",notDead:"not-dead",deferred:"deferred"},U={valid:"valid",invalid:"invalid"};function k(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(l)}`)}function H(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>l)throw k(t.user_version);const n=e.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name IN ('schedules', 'invocations')").all(),i=new Set(n.map(e=>e.name));if(t.user_version<1||!i.has("schedules")||!i.has("invocations"))throw new Error("not a valid authoritative scheduler database")}function F(e){if(!G(e.mode))throw new M(O.invalid,"Invalid reference mode");return{invocationId:e.invocation_id,attempt:e.attempt,scheduleId:e.schedule_id,threadId:e.thread_id,threadsDir:e.threads_dir,name:e.name,cwd:e.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:e.created_at}}function W(e){if(!Number.isSafeInteger(e.expectedAttempt)||e.expectedAttempt<1||0===e.threadId.trim().length||!i(e.threadsDir))throw new M(O.invalid,"会话关联需要有效的 attempt、会话 ID 和绝对存储路径")}const P=Object.values(g),B=Object.values(A);function G(e){return P.includes(e)}function X(e){return B.includes(e)}function $(e){try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}function q(e){if(null===e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(j):[]}catch{return[]}}function j(e){return"number"==typeof e&&Number.isInteger(e)&&e>0}function Y(e){return e===x.alive||e===x.exited||e===x.unknown}function K(e){return j(e)?{pgid:e,leaderState:x.unknown}:"object"==typeof e&&null!==e&&"pgid"in e&&j(e.pgid)&&"leaderState"in e&&Y(e.leaderState)?"startToken"in e?"string"==typeof e.startToken&&e.startToken.length>0?{pgid:e.pgid,leaderState:e.leaderState,startToken:e.startToken}:void 0:{pgid:e.pgid,leaderState:e.leaderState}:void 0}function V(e){if(null===e)return{kind:U.valid,groups:[]};try{const t=JSON.parse(e);if(!Array.isArray(t))return{kind:U.invalid};const n=new Map;for(const e of t){const t=K(e);if(void 0===t)return{kind:U.invalid};n.set(t.pgid,t)}return{kind:U.valid,groups:[...n.values()]}}catch{return{kind:U.invalid}}}function J(e){return new M(O.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function z(e,t){const n=V(e);if(n.kind===U.invalid)throw J(t);return n.groups}function Q(e){const t=new Map;for(const n of e)!Number.isInteger(n.pgid)||n.pgid<=0||t.set(n.pgid,void 0!==n.startToken&&n.startToken.length>0?{pgid:n.pgid,leaderState:n.leaderState,startToken:n.startToken}:{pgid:n.pgid,leaderState:n.leaderState});return JSON.stringify([...t.values()])}function Z(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const ee="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",te=[A.claimed,A.running,A.retry],ne=te;function ie(e){return null!==e.executor_pid&&null!==e.executor_start_token?{pid:e.executor_pid,startToken:e.executor_start_token}:void 0}function re(e){if(!G(e.mode)||!X(e.status))throw new M(O.invalid,`invocation ${e.id} 的 mode/status 非法: ${e.mode}/${e.status}`);const t=z(e.tree_tracked_pgids,e.id);return{id:e.id,scheduleId:e.schedule_id,mode:e.mode,status:e.status,scheduledForMs:e.scheduled_for,attempt:e.attempt,maxAttempts:e.max_attempts,executor:ie(e),claimedBy:e.claimed_by??void 0,leaseUntilMs:e.lease_until??void 0,retryAtMs:e.retry_at??void 0,threadId:e.thread_id??void 0,outputExcerpt:e.output_excerpt??void 0,error:e.error??void 0,pendingActions:$(e.pending_actions_json),treeTrackedPgids:t.map(e=>e.pgid),treeTrackedGroups:t,treeUnsettled:1===e.tree_unsettled,treeSurvivorPids:q(e.tree_survivor_pids),createdAtMs:e.created_at,startedAtMs:e.started_at??void 0,finishedAtMs:e.finished_at??void 0}}function se(e){return Object.values(D).some(t=>t===e)}function de(e,t){if(!se(e.status))throw new M(O.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);const n=oe(e.max_rounds??void 0),i=e.rounds_started??0;if("number"!=typeof i||!Number.isSafeInteger(i)||i<0)throw new M(O.invalid,`schedule ${e.id} 的 roundsStarted 非法`);const r=void 0===n?void 0:t.prepare("SELECT * FROM invocations WHERE schedule_id = ? AND mode = ? ORDER BY scheduled_for DESC, rowid DESC LIMIT 1").get(e.id,g.scheduled),s=void 0===r?void 0:{status:re(r).status,treeUnsettled:1===r.tree_unsettled||null!==r.tree_tracked_pgids&&"[]"!==r.tree_tracked_pgids.trim()};return{id:e.id,name:e.name,prompt:e.prompt,cwd:e.cwd,trigger:T(e.trigger_json),status:e.status,authorityDigest:e.authority_digest??void 0,maxRunMs:ae(e.max_run_ms),maxRounds:n,roundsStarted:i,...void 0===s?{}:{lastScheduledRun:s},nextRunAtMs:e.next_run_at??void 0,lastRunAtMs:e.last_run_at??void 0,lastError:e.last_error??void 0,createdAtMs:e.created_at,updatedAtMs:e.updated_at}}function ae(e){if(null!=e){if("number"!=typeof e||!Number.isInteger(e)||e<o.minMaxRunMs||e>o.maxRunCeilingMs)throw new M(O.invalid,`maxRunMs 必须是 ${String(o.minMaxRunMs)}..${String(o.maxRunCeilingMs)} 的整数毫秒`);return e}}function oe(e){if(void 0!==e){if("number"!=typeof e||!Number.isSafeInteger(e)||e<1)throw new M(O.invalid,"maxRounds 必须是正安全整数");return e}}function ue(e){const t=e.name.trim();if(0===t.length||t.length>o.maxNameChars)throw new M(O.invalid,`name 必须为 1..${String(o.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>o.maxPromptChars)throw new M(O.invalid,`prompt 必须为 1..${String(o.maxPromptChars)} 个字符`);if(!i(e.cwd))throw new M(O.invalid,"cwd 必须是绝对路径");const r=v.safeParse(e.trigger);if(!r.success)throw new M(O.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:r.data,maxRunMs:ae(e.maxRunMs),maxRounds:oe(e.maxRounds)}}export class ScheduleStore{db;maxSchedules;claimLeaseMs;retryBudget;retryBackoffMs;executorLiveness;treeLiveness;maxLivenessProbesPerClaim;livenessProbeDeferralMs;invocationRetentionPerSchedule;invocationRetentionMs;constructor(i,s={}){this.maxSchedules=s.maxSchedules??50,this.claimLeaseMs=s.claimLeaseMs??o.claimLeaseMs,this.retryBudget=s.retryBudget??o.retryBudget,this.retryBackoffMs=s.retryBackoffMs??o.retryBackoffMs,this.executorLiveness=s.executorLiveness??(()=>L.unknown),this.treeLiveness=s.treeLiveness,this.maxLivenessProbesPerClaim=s.maxLivenessProbesPerClaim??o.maxLivenessProbesPerClaim,this.livenessProbeDeferralMs=s.livenessProbeDeferralMs??o.livenessProbeDeferralMs,this.invocationRetentionPerSchedule=s.invocationRetentionPerSchedule??o.invocationRetentionPerSchedule,this.invocationRetentionMs=s.invocationRetentionMs??o.invocationRetentionMs;const h=a(i);if(!0===s.readOnly){const e=r(h,"schedules.db"),n=t(e);if(!n&&!0===s.requireExistingDatabase)throw new Error("authoritative scheduler database does not exist");this.db=n?u(e):new d(":memory:");try{if(n){const e=this.db.prepare("PRAGMA user_version").get()?.user_version;if(![5,6,7,8,l].includes(Number(e)))throw new Error(`scheduler schema v${String(e)} 不支持只读查询;请停止旧 daemon 后由新版 daemon 初始化迁移`)}else this.init(),c(this.db)}catch(e){throw this.db.close(),e}return}if(!0===s.requireExistingDatabase&&!t(h))throw new Error("authoritative scheduler database does not exist");t(h)||n(h,{recursive:!0,mode:448}),"win32"!==process.platform&&e(h,448);const E=r(h,"schedules.db");if(!0===s.requireExistingDatabase&&!t(E))throw new Error("authoritative scheduler database does not exist");if(this.db=new d(E),!0===s.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(C)}`),H(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{e(E,384)}catch(e){throw this.db.close(),e}try{this.init()}catch(e){throw this.db.close(),e}}init(){this.db.exec(`PRAGMA busy_timeout = ${String(C)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`),h(this.db);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>l)throw k(e.user_version);const t=e.user_version<7;t&&this.db.exec("PRAGMA foreign_keys = OFF;");try{this.transaction(()=>{const e=this.db.prepare("PRAGMA user_version").get().user_version;if(e>l)throw k(e);const t=e<7&&void 0!==this.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'schedules'").get();if(this.db.exec(`CREATE TABLE IF NOT EXISTS schedules (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'running', 'retry', 'completed', 'needs_confirmation', 'failed')),\n scheduled_for INTEGER NOT NULL,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT ${String(o.retryBudget)},\n executor_pid INTEGER,\n executor_start_token TEXT,\n executor_probed_at INTEGER,\n claimed_by TEXT,\n ownership_token TEXT,\n lease_until INTEGER,\n retry_at INTEGER,\n thread_id TEXT,\n output_excerpt TEXT,\n error TEXT,\n pending_actions_json TEXT NOT NULL DEFAULT '[]',\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER,\n tree_tracked_pgids TEXT,\n tree_unsettled INTEGER NOT NULL DEFAULT 0,\n tree_survivor_pids TEXT,\n UNIQUE (schedule_id, mode, scheduled_for)\n );\n CREATE INDEX IF NOT EXISTS idx_schedules_due\n ON schedules (next_run_at) WHERE status = 'active' AND next_run_at IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_invocations_live\n ON invocations (schedule_id) WHERE status IN ('pending', 'claimed', 'running', 'retry');\n CREATE TABLE IF NOT EXISTS schedule_thread_refs (\n invocation_id TEXT NOT NULL,\n attempt INTEGER NOT NULL CHECK (attempt > 0),\n schedule_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n threads_dir TEXT NOT NULL,\n name TEXT NOT NULL,\n cwd TEXT NOT NULL,\n scheduled_for INTEGER NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n created_at INTEGER NOT NULL,\n PRIMARY KEY (invocation_id, attempt)\n );\n CREATE INDEX IF NOT EXISTS idx_schedule_thread_refs_history\n ON schedule_thread_refs (schedule_id, scheduled_for DESC, invocation_id, attempt DESC);`),e<7&&this.addMissingColumns(),t&&this.migrateRoundsInTransaction(),this.db.exec("CREATE TABLE IF NOT EXISTS schedule_extensions (\n request_id TEXT PRIMARY KEY CHECK (length(request_id) BETWEEN 1 AND 128),\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n expected_max_rounds INTEGER NOT NULL CHECK (typeof(expected_max_rounds) = 'integer' AND expected_max_rounds BETWEEN 1 AND 9007199254740991),\n additional_rounds INTEGER NOT NULL CHECK (typeof(additional_rounds) = 'integer' AND additional_rounds BETWEEN 1 AND 9007199254740991),\n authority_digest TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE (schedule_id, expected_max_rounds)\n );"),t&&0!==this.db.prepare("PRAGMA foreign_key_check").all().length)throw new M(O.invalid,"scheduler 迁移失败:外键关联不完整");E(this.db),e!==l&&this.db.exec(`PRAGMA user_version = ${String(l)};`)})}finally{t&&this.db.exec("PRAGMA foreign_keys = ON;")}}migrateRoundsInTransaction(){this.db.exec("CREATE TABLE schedules_v7 (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n INSERT INTO schedules_v7 (rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at)\n SELECT rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at FROM schedules;\n DROP TABLE schedules;\n ALTER TABLE schedules_v7 RENAME TO schedules;\n CREATE INDEX idx_schedules_due ON schedules (next_run_at)\n WHERE status = 'active' AND next_run_at IS NOT NULL;")}addMissingColumns(){const e=[{table:"schedules",column:"authority_digest",definition:"TEXT"},{table:"schedules",column:"max_run_ms",definition:"INTEGER"},{table:"invocations",column:"max_attempts",definition:`INTEGER NOT NULL DEFAULT ${String(o.retryBudget)}`},{table:"invocations",column:"executor_pid",definition:"INTEGER"},{table:"invocations",column:"executor_start_token",definition:"TEXT"},{table:"invocations",column:"executor_probed_at",definition:"INTEGER"},{table:"invocations",column:"tree_tracked_pgids",definition:"TEXT"},{table:"invocations",column:"tree_unsettled",definition:"INTEGER NOT NULL DEFAULT 0"},{table:"invocations",column:"tree_survivor_pids",definition:"TEXT"}];for(const t of e){this.db.prepare(`PRAGMA table_info(${t.table})`).all().map(e=>e.name).includes(t.column)||this.db.exec(`ALTER TABLE ${t.table} ADD COLUMN ${t.column} ${t.definition}`)}this.clampOversizedIntervals()}clampOversizedIntervals(){const e=this.db.prepare("SELECT id, trigger_json FROM schedules").all();for(const t of e){let e;try{e=JSON.parse(t.trigger_json)}catch{continue}"object"!=typeof e||null===e||!("kind"in e)||e.kind!==p.interval||!("everyMs"in e)||"number"!=typeof e.everyMs||e.everyMs<=o.maxIntervalMs||this.db.prepare("UPDATE schedules SET trigger_json = ?, status = ?, last_error = ?, updated_at = ?\n WHERE id = ?").run(JSON.stringify({kind:p.interval,everyMs:o.maxIntervalMs}),D.paused,`原间隔 ${String(e.everyMs)} ms 超过上限 365 天,已按 365 天登记并暂停;确认后 roll schedule resume`,Date.now(),t.id)}}transaction(e){this.db.exec("BEGIN IMMEDIATE");try{const t=e();return this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}createSchedule(e,t){const n=ue(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t??Date.now()))}createScheduleIdempotent(e,t){const n=ue(e),i=String(o.maxRunMs);return this.transaction(()=>{const r=t??Date.now(),s=this.db.prepare(`SELECT * FROM schedules\n WHERE status = ? AND prompt = ? AND cwd = ? AND trigger_json = ?\n AND COALESCE(max_run_ms, ${i}) = COALESCE(?, ${i})\n AND max_rounds IS ?`).get(D.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??null,n.maxRounds??null);if(void 0!==s){const t=e.authorityDigest??null;return(s.authority_digest??null)!==t?(this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,r,s.id),{created:!1,reauthorized:!0,schedule:this.requireSchedule(s.id)}):{created:!1,reauthorized:!1,schedule:de(s,this.db)}}return{created:!0,reauthorized:!1,schedule:this.insertScheduleInTransaction(n,e,r)}})}insertScheduleInTransaction(e,t,n){if(this.db.prepare("SELECT COUNT(*) AS count FROM schedules").get().count>=this.maxSchedules)throw new M(O.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);if(!0===t.fireImmediately&&("interval"!==e.trigger.kind||void 0!==e.trigger.startAtMs))throw new M(O.invalid,"立即触发不能与日历规则或 startAt 同时使用");const i=s(),r=!0===t.fireImmediately?n:m(e.trigger,n);if(void 0!==t.expectedFirstRunAtMs&&r!==t.expectedFirstRunAtMs)throw new M(O.invalid,"首次运行时间在确认后已变化,请重新发起确认");return this.db.prepare("INSERT INTO schedules\n (id, name, prompt, cwd, trigger_json, status, authority_digest, max_run_ms,\n next_run_at, created_at, updated_at, max_rounds)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),D.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n,e.maxRounds??null),this.requireSchedule(i)}setAuthorityDigest(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes}resumeSchedule(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(i?.status===D.completed)throw new M(O.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");const r=this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(D.active,t,n,e);return this.reconcileRoundsInTransaction(e,n),1===r.changes})}getScheduleExtension(e){const t=this.db.prepare("SELECT * FROM schedule_extensions WHERE request_id = ?").get(e);return void 0===t?void 0:{requestId:t.request_id,scheduleId:t.schedule_id,expectedMaxRounds:t.expected_max_rounds,additionalRounds:t.additional_rounds,authorityDigest:t.authority_digest,createdAtMs:t.created_at}}extendSchedule(e,t=Date.now()){const n=oe(e.additionalRounds),i=oe(e.expectedMaxRounds);if(void 0===n||void 0===i||!Number.isSafeInteger(i+n))throw new M(O.invalid,"追加轮数及原上限必须为正安全整数,追加后的总轮数不得溢出");if("string"!=typeof e.requestId||0===e.requestId.trim().length||e.requestId.length>128)throw new M(O.invalid,"requestId 必须为 1..128 字符的非空请求标识;重试时复用原标识");if("string"!=typeof e.authorityDigest||0===e.authorityDigest.trim().length)throw new M(O.invalid,"追加轮数必须重新核验工作区权限");return this.transaction(()=>{const r=this.getScheduleExtension(e.requestId);if(void 0!==r){if(r.scheduleId!==e.scheduleId||r.expectedMaxRounds!==i||r.additionalRounds!==n)throw new M(O.invalid,"requestId 已用于不同的追加请求;请检查原请求参数");return{extended:!1,schedule:this.requireSchedule(e.scheduleId)}}const s=this.requireSchedule(e.scheduleId);if(void 0===s.maxRounds)throw new M(O.invalid,"不限轮数的任务不需要追加额度");if(s.maxRounds!==i)throw new M(O.invalid,"任务轮数上限已变化,追加请求已过期;请刷新任务并核对原请求结果");if(s.status!==D.completed||s.roundsStarted!==s.maxRounds)throw new M(O.invalid,"仅能为已结束且轮数用尽的有限任务追加轮数");if(void 0!==this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND\n (status IN ('pending', 'claimed', 'running', 'retry') OR tree_unsettled = 1\n OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]')) LIMIT 1").get(s.id))throw new M(O.invalid,"任务仍有待执行、运行中或未清场的记录,暂不能追加轮数;请先完成运行收尾");const d=_(s.trigger,t);if(!Number.isSafeInteger(t)||!Number.isSafeInteger(d)||!Number.isFinite(new Date(d).getTime()))throw new M(O.invalid,"追加时间或下次执行时间超出有效范围");return this.db.prepare("INSERT INTO schedule_extensions (request_id, schedule_id, expected_max_rounds,\n additional_rounds, authority_digest, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(e.requestId,s.id,i,n,e.authorityDigest,t),this.db.prepare("UPDATE schedules SET max_rounds = ?, authority_digest = ?, status = ?,\n next_run_at = ?, updated_at = ? WHERE id = ?").run(i+n,e.authorityDigest,D.active,d,t,s.id),{extended:!0,schedule:this.requireSchedule(s.id)}})}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?de(t,this.db):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(e=>de(e,this.db))}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(void 0===i)return!1;if(i.status===D.completed){if(t===D.active)throw new M(O.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");return!0}if(t===D.completed)throw new M(O.invalid,"结束状态由调度器在轮数用尽且运行结算后设置");if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===D.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,g.scheduled,A.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return this.reconcileRoundsInTransaction(e,n),!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(A.running).map(re)}listActiveWorkerInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status IN (?, ?)\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC").all(A.claimed,A.running).map(re)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${ee}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...te).map(re)}prepareWorkerShutdown(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT id FROM invocations WHERE claimed_by = ? AND status = ?").all(e,A.claimed);for(const e of i)this.finishInvocationAsFailedInTransaction(e.id,t,n);return this.db.prepare("SELECT * FROM invocations WHERE claimed_by = ? AND status = ?\n ORDER BY started_at ASC, created_at ASC").all(e,A.running).map(e=>{if(null===e.ownership_token)throw new M(O.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new M(O.invalid,`running invocation ${e.id} 无法加载 worker claim`);return t})})}removeSchedule(e,t={}){return this.transaction(()=>{if(!0!==t.abandon){const t=this.db.prepare(`SELECT id, status FROM invocations WHERE schedule_id = ? AND ${ee} LIMIT 1`).get(e,...te);if(void 0!==t)throw new M(O.invalid,`定时任务 ${e} 仍有未结束的运行(invocation ${t.id},${t.status});先 cancel --kill 清场,或显式 --abandon 放弃追踪`)}return 1===this.db.prepare("DELETE FROM schedules WHERE id = ?").run(e).changes})}enqueueManualInvocation(e,t=Date.now(),n={}){const i=Math.max(1,Math.floor(n.maxAttempts??this.retryBudget));return this.transaction(()=>{this.requireSchedule(e);const n=s();for(let r=0;r<1e3;r+=1){if(1===this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, created_at)\n VALUES (?, ?, ?, ?, ?, 0, ?, ?)").run(n,e,g.manual,A.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new M(O.invalid,`schedule ${e} 的手动触发入队失败`)})}claimPendingInvocation(e,t,n=Date.now()){return this.transaction(()=>{const i=s();return 1===this.db.prepare(`UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?, attempt = 1,\n retry_at = NULL\n WHERE id = ? AND status = ?\n AND NOT EXISTS (\n SELECT 1 FROM invocations o\n WHERE o.schedule_id = (SELECT schedule_id FROM invocations WHERE id = ?)\n AND o.id != ? AND ${ee})`).run(A.claimed,t,i,n+this.claimLeaseMs,e,A.pending,e,e,...te).changes?this.loadClaim(e,i):void 0})}findLiveRun(e){const t=this.db.prepare(`SELECT * FROM invocations WHERE schedule_id = ? AND ${ee}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...te);return t?re(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,A.pending).changes}cancelInvocation(e,t,n=Date.now(),i={}){return this.transaction(()=>{const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return this.cancelInvocationInTransaction(r,t,n,i)})}finalizeCancellation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id),n=this.revisionMismatch(t,{...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}});if(void 0!==n)return n;if(!0!==e.abandon&&void 0!==t&&V(t.tree_tracked_pgids).kind===U.invalid)return N.treeUnsettled;const i=void 0===t?void 0:this.executorBlocksCancel(t,!0===e.abandon);if(void 0!==i)return i;if(void 0!==e.tree&&void 0!==t&&ne.includes(t.status)){if(!this.writeInvocationTreeInTransaction({id:e.id,unsettled:e.tree.unsettled,...void 0!==e.tree.trackedGroups?{trackedGroups:e.tree.trackedGroups}:{},...void 0!==e.tree.survivorPids?{survivorPids:e.tree.survivorPids}:{},expectedAttempt:e.expectedAttempt}))return N.ownershipChanged}const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);return this.cancelInvocationInTransaction(r,e.reason,e.nowMs,{...!0===e.abandon?{abandon:!0}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}},{executorChecked:!0})})}executorBlocksCancel(e,t){if(e.status!==A.running||t)return;const n=ie(e),i=void 0===n?L.unknown:this.executorLiveness(n);return i===L.alive||i===L.descendants?N.executorAlive:i===L.unknown?N.executorUnknown:void 0}revisionMismatch(e,t){return void 0!==e&&X(e.status)?void 0!==t.expectedOwnershipToken&&e.ownership_token!==t.expectedOwnershipToken||void 0!==t.expectedAttempt&&e.attempt!==t.expectedAttempt||void 0!==t.expectedClaimedBy&&(e.claimed_by??void 0)!==t.expectedClaimedBy?N.ownershipChanged:void 0:N.notFound}cancelInvocationInTransaction(e,t,n,i,r={}){const s=this.revisionMismatch(e,i);if(void 0!==s)return s;if(void 0===e||!X(e.status))return N.notFound;if(!f.includes(e.status))return N.terminal;const d=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==d)return d;const a=!0===i.abandon;return!a&&this.treeBlocksTerminal(e)?N.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:a})?N.cancelled:N.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===A.retry?"retry_at":"lease_until";this.db.prepare(`UPDATE invocations SET ${n} = ? WHERE id = ?`).run(t+this.claimLeaseMs,e.id)}pauseInvalidScheduleInTransaction(e,t,n){this.db.prepare("UPDATE schedules SET status = ?, last_error = ?, updated_at = ? WHERE id = ?").run(D.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return de(e,this.db)}catch(n){return void this.pauseInvalidScheduleInTransaction(e.id,n,t)}}loadScheduleForClaimInTransaction(e,t){const n=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return void 0===n?void 0:this.decodeScheduleForClaimInTransaction(n,t)}hasOtherLiveRunInTransaction(e,t){return void 0!==this.db.prepare(`SELECT 1 AS present FROM invocations\n WHERE schedule_id = ? AND id != ? AND ${ee} LIMIT 1`).get(e,t,...te)}claimDue(e){const t=e.heldInvocationIds??new Set;return this.transaction(()=>{const n=[],i=this.db.prepare("SELECT i.*, s.status AS schedule_status FROM invocations i\n JOIN schedules s ON s.id = i.schedule_id\n WHERE i.status = ?\n OR (i.status = ? AND i.retry_at IS NOT NULL AND i.retry_at <= ?)\n OR (i.status IN (?, ?) AND i.lease_until IS NOT NULL AND i.lease_until <= ?)\n ORDER BY i.scheduled_for ASC, i.created_at ASC").all(A.pending,A.retry,e.nowMs,A.claimed,A.running,e.nowMs),r=[],d=i.filter(e=>e.status===A.running&&!t.has(e.id)&&void 0!==ie(e));d.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const a=new Map;let o=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of d){const n=ie(t);void 0!==n&&(o<=0?a.set(t.id,y.deferred):(o-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),a.set(t.id,this.executorLiveness(n)===L.dead?y.dead:y.notDead)))}for(const n of i){const i=n.status===A.claimed||n.status===A.running,s=a.get(n.id);!i||!t.has(n.id)&&s!==y.notDead?s!==y.deferred?r.push(n):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.livenessProbeDeferralMs,n.id):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.claimLeaseMs,n.id)}if(e.limit<=0)return n;for(const t of r){if(n.length>=e.limit)break;if(V(t.tree_tracked_pgids).kind===U.invalid){this.deferLiveRowInTransaction(t,e.nowMs);continue}const i=this.loadScheduleForClaimInTransaction(t.schedule_id,e.nowMs);if(void 0===i){t.status!==A.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===g.scheduled&&i.status===D.paused){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.finishInvocationAsFailedInTransaction(t.id,"任务已暂停,放弃本次运行",e.nowMs);continue}if(this.hasOtherLiveRunInTransaction(t.schedule_id,t.id))continue;const r=t.status===A.pending?1:t.attempt+1;if(r>t.max_attempts){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.markTerminalFailureInTransaction(t,`重试预算耗尽(共 ${String(t.max_attempts)} 次尝试):${t.error??"exec 未成功完成"}`,e.nowMs);continue}const d=s();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(A.claimed,e.workerId,d,e.nowMs+this.claimLeaseMs,r,t.id);const a=this.loadClaim(t.id,d);void 0!==a&&n.push(a)}if(n.length>=e.limit)return n;const u=this.db.prepare("SELECT s.* FROM schedules s\n WHERE s.status = ? AND s.next_run_at IS NOT NULL AND s.next_run_at <= ?\n AND (s.max_rounds IS NULL OR s.rounds_started < s.max_rounds)\n AND NOT EXISTS (\n SELECT 1 FROM invocations i\n WHERE i.schedule_id = s.id AND i.status IN (?, ?, ?, ?))\n ORDER BY s.next_run_at ASC, s.created_at ASC\n LIMIT ?").all(D.active,e.nowMs,A.pending,A.claimed,A.running,A.retry,this.maxSchedules);for(const t of u){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=s(),d=s(),a=this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, claimed_by,\n ownership_token, lease_until, created_at)\n VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)").run(r,t.id,g.scheduled,A.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,d,e.nowMs+this.claimLeaseMs,e.nowMs),o=Number(a.changes);if(this.db.prepare("UPDATE schedules SET rounds_started = rounds_started + ?,\n next_run_at = CASE WHEN max_rounds IS NOT NULL AND rounds_started + ? >= max_rounds\n THEN NULL ELSE ? END, updated_at = ? WHERE id = ?").run(o,o,_(i.trigger,e.nowMs),e.nowMs,t.id),1===a.changes){const e=this.loadClaim(r,d);void 0!==e&&n.push(e)}}return n})}beginInvocation(e,t,n=Date.now(),i){return 1===this.db.prepare("UPDATE invocations\n SET status = ?, started_at = ?, lease_until = ?, executor_pid = ?,\n executor_start_token = ?, executor_probed_at = NULL\n WHERE id = ? AND ownership_token = ? AND status = ?").run(A.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,A.claimed).changes?this.loadClaim(e,t):void 0}renewLease(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE invocations SET lease_until = ?\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n+this.claimLeaseMs,e,t,A.claimed,A.running).changes}completeInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.ownership_token!==e.ownershipToken||t.status!==A.claimed&&t.status!==A.running)return R.lostClaim;if(this.treeBlocksTerminal(t))return R.treeUnsettled;return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, thread_id = ?, output_excerpt = ?, pending_actions_json = ?,\n error = NULL, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(e.status,e.threadId??null,e.outputExcerpt??null,JSON.stringify(e.pendingActions??[]),e.nowMs,e.id,e.ownershipToken,A.claimed,A.running).changes?R.lostClaim:(this.db.prepare("UPDATE schedules SET last_run_at = ?, last_error = NULL, updated_at = ?\n WHERE id = (SELECT schedule_id FROM invocations WHERE id = ?)").run(e.nowMs,e.nowMs,e.id),t.mode===g.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),R.written)})}reclassifyTimedOutInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.attempt!==e.expectedAttempt||null!==t.finished_at&&t.finished_at<e.timedOutAtMs||t.status!==A.completed&&t.status!==A.needsConfirmation)return S.lostClaim;if(this.treeBlocksTerminal(t))return S.treeUnsettled;if(t.attempt>=t.max_attempts){return 1!==this.db.prepare("UPDATE invocations\n SET thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(e.id,e.expectedAttempt,A.completed,A.needsConfirmation).changes?S.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===g.scheduled&&this.getSchedule(t.schedule_id)?.status===D.paused?S.terminalPaused:S.terminal)}return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, finished_at = NULL,\n claimed_by = NULL, ownership_token = NULL, lease_until = NULL,\n thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(A.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,A.completed,A.needsConfirmation).changes?S.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),t.mode===g.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),S.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?Q(t):null,i=Z(e.unsettled?e.survivorPids??[]:[]),r=e.unsettled?1:0,s=e.error??null;if(void 0!==e.ownershipToken){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n,r,i,s,e.id,e.ownershipToken,A.claimed,A.running).changes}if(void 0!==e.expectedAttempt){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND attempt = ? AND status IN (?, ?, ?)").run(n,r,i,s,e.id,e.expectedAttempt,A.claimed,A.running,A.retry).changes}return!1}failInvocation(e,t,n,i=Date.now(),r={}){return this.transaction(()=>{const s=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0===s||s.ownership_token!==t||s.status!==A.claimed&&s.status!==A.running?S.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?S.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===g.scheduled&&this.getSchedule(s.schedule_id)?.status===D.paused?S.terminalPaused:S.terminal):(this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL\n WHERE id = ?").run(A.retry,i+this.retryBackoffMs,n,e),S.retryScheduled)})}registerThreadReference(e,t=Date.now()){return W(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations\n WHERE id = ? AND attempt = ? AND ownership_token = ? AND status = ?").get(e.invocationId,e.expectedAttempt,e.ownershipToken,A.running);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}backfillThreadReference(e,t=Date.now()){return W(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations WHERE id = ? AND attempt = ? AND thread_id = ?").get(e.invocationId,e.expectedAttempt,e.threadId);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}insertThreadReference(e,t,n){const i=this.db.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? AND attempt = ?").get(t.invocationId,t.expectedAttempt);if(void 0!==i){if(i.thread_id!==t.threadId||i.threads_dir!==t.threadsDir)throw new M(O.invalid,`invocation ${t.invocationId} attempt ${String(t.expectedAttempt)} 已关联其他会话`);return F(i)}const r=this.getSchedule(e.schedule_id);if(void 0!==r){if(!G(e.mode))throw new M(O.invalid,"Invalid invocation mode");return this.db.prepare("INSERT INTO schedule_thread_refs\n (invocation_id, attempt, schedule_id, thread_id, threads_dir, name, cwd,\n scheduled_for, mode, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(t.invocationId,t.expectedAttempt,r.id,t.threadId,t.threadsDir,r.name,r.cwd,e.scheduled_for,e.mode,n),{invocationId:t.invocationId,attempt:t.expectedAttempt,scheduleId:r.id,threadId:t.threadId,threadsDir:t.threadsDir,name:r.name,cwd:r.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:n}}}getInvocation(e){const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return t?re(t):void 0}listInvocations(e,t=20){if(t<=0)return[];return this.db.prepare("SELECT * FROM invocations WHERE schedule_id = ?\n ORDER BY created_at DESC, rowid DESC LIMIT ?").all(e,t).map(re)}pruneInvocations(e=Date.now()){return this.transaction(()=>{const t=this.db.prepare(`DELETE FROM invocations\n WHERE status IN (${b})\n AND finished_at IS NOT NULL AND finished_at < ?`).run(...I,e-this.invocationRetentionMs),n=this.db.prepare(`DELETE FROM invocations WHERE id IN (\n SELECT id FROM (\n SELECT id, ROW_NUMBER() OVER (\n PARTITION BY schedule_id\n ORDER BY COALESCE(finished_at, created_at) DESC, rowid DESC) AS rn\n FROM invocations WHERE status IN (${b}))\n WHERE rn > ?)`).run(...I,this.invocationRetentionPerSchedule);return Number(t.changes)+Number(n.changes)})}nextWakeAtMs(){return this.db.prepare("SELECT MIN(t) AS wake FROM (\n SELECT MIN(next_run_at) AS t FROM schedules\n WHERE status = ? AND next_run_at IS NOT NULL\n AND (max_rounds IS NULL OR rounds_started < max_rounds)\n UNION ALL SELECT MIN(retry_at) FROM invocations WHERE status = ?\n UNION ALL SELECT MIN(lease_until) FROM invocations WHERE status IN (?, ?)\n UNION ALL SELECT MIN(scheduled_for) FROM invocations WHERE status = ?)").get(D.active,A.retry,A.claimed,A.running,A.pending).wake??void 0}treeBlocksTerminal(e){const t=V(e.tree_tracked_pgids);return t.kind===U.invalid||(1===e.tree_unsettled||0!==t.groups.length)&&(void 0===this.treeLiveness||this.treeLiveness(re(e))!==w.settled)}reconcileRoundsInTransaction(e,t){this.db.prepare("UPDATE schedules\n SET status = CASE\n WHEN NOT EXISTS (SELECT 1 FROM invocations i WHERE i.schedule_id = schedules.id AND i.mode = 'scheduled'\n AND (i.status IN ('pending', 'claimed', 'running', 'retry') OR i.tree_unsettled = 1\n OR (i.tree_tracked_pgids IS NOT NULL AND trim(i.tree_tracked_pgids) != '[]')))\n THEN 'completed'\n WHEN status = 'completed' THEN 'active'\n ELSE status END,\n next_run_at = NULL, updated_at = ?\n WHERE id = ? AND max_rounds IS NOT NULL AND rounds_started >= max_rounds").run(t,e)}finishInvocationAsFailedInTransaction(e,t,n,i={}){const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0!==r&&((!0===i.force||!this.treeBlocksTerminal(r))&&(this.db.prepare("UPDATE invocations\n SET status = ?, error = ?, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ?").run(A.failed,t,n,e),r.mode===g.scheduled&&this.reconcileRoundsInTransaction(r.schedule_id,n),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===g.scheduled?this.db.prepare("UPDATE schedules SET status = CASE WHEN status = 'completed' THEN status ELSE ? END, last_error = ?, updated_at = ? WHERE id = ?").run(D.paused,t,n,e.schedule_id):this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(t,n,e.schedule_id))}loadClaim(e,t){const n=this.getInvocation(e);if(void 0===n)return;const i=this.getSchedule(n.scheduleId);return void 0!==i?{invocation:n,schedule:i,ownershipToken:t}:void 0}requireInvocation(e){const t=this.getInvocation(e);if(void 0===t)throw new M(O.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new M(O.notFound,`schedule ${e} 不存在`);return t}}export const SCHEDULE_LEDGER_READ_STATUSES={ok:"ok",empty:"empty",migrationRequired:"migration-required"};export function readScheduleLedger(e){const n=r(a(e),"schedules.db");if(!t(n))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=u(n);let s=!1;try{i.exec(`PRAGMA busy_timeout = ${String(C)}; BEGIN;`),s=!0;const e=i.prepare("PRAGMA user_version").get();if(![5,6,7,8,l].includes(e.user_version))return{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version,schedules:[]};const t=i.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all();return{status:SCHEDULE_LEDGER_READ_STATUSES.ok,schedules:t.map(e=>de(e,i))}}finally{try{s&&i.exec("ROLLBACK")}finally{i.close()}}}function ce(e,n){const i=r(a(e),"schedules.db");if(!t(i))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty};const s=u(i);let d=!1;try{s.exec(`PRAGMA busy_timeout = ${String(C)}; BEGIN;`),d=!0;const e=s.prepare("PRAGMA user_version").get();return[5,6,7,8,l].includes(e.user_version)?{status:SCHEDULE_LEDGER_READ_STATUSES.ok,value:n(s,e.user_version>=6)}:{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version}}finally{try{d&&s.exec("ROLLBACK")}finally{s.close()}}}export function readScheduleOccupancy(e){const t=ce(e,e=>e.prepare(`SELECT * FROM invocations WHERE ${ee}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...te).map(re));if(t.status===SCHEDULE_LEDGER_READ_STATUSES.migrationRequired)throw new Error(`scheduler occupancy 无法读取 schema v${String(t.schemaVersion)},拒绝把未知占用当成空闲`);return t.value??[]}function le(e,t,n){const i=e.prepare("SELECT * FROM invocations WHERE id = ?").get(t),r=n?e.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? ORDER BY attempt ASC").all(t).map(F):[],s=void 0===i?void 0:re(i),d=s??r[0];return void 0===d?void 0:{invocationId:t,scheduleId:d.scheduleId,scheduledForMs:d.scheduledForMs,mode:d.mode,invocation:s,references:r}}function he(e){return"SELECT id, schedule_id, scheduled_for FROM invocations"+(e?" UNION SELECT invocation_id AS id, schedule_id, scheduled_for FROM schedule_thread_refs":"")}function Ee(e,t){const n=e.prepare("SELECT * FROM schedules").all().map(t=>de(t,e)),i=new Map(n.map(e=>[e.id,e])),r=e.prepare("SELECT id FROM schedules"+(t?" UNION SELECT schedule_id AS id FROM schedule_thread_refs":"")).all(),s=[];for(const{id:n}of r){const r=e.prepare(`SELECT id FROM (${he(t)})\n WHERE schedule_id = ? ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(n),d=void 0===r?void 0:le(e,r.id,t),a=i.get(n),o=d?.references.at(-1),u=a??o;void 0!==u&&s.push({scheduleId:n,name:u.name,cwd:u.cwd,schedule:a,latestRun:d})}return s.sort((e,t)=>Number(void 0===e.schedule)-Number(void 0===t.schedule)||(t.latestRun?.scheduledForMs??t.schedule?.createdAtMs??0)-(e.latestRun?.scheduledForMs??e.schedule?.createdAtMs??0)||e.scheduleId.localeCompare(t.scheduleId))}export function readScheduleHistory(e,t={}){const n=t.limit??20,i=t.offset??0;if(!Number.isSafeInteger(n)||n<1||n>100||!Number.isSafeInteger(i)||i<0)throw new RangeError("History limit must be 1..100 and offset must be nonnegative integers");const r=ce(e,(e,r)=>{const s=void 0===t.scheduleId?"":"WHERE schedule_id = ?",d=e.prepare(`SELECT id FROM (${he(r)}) ${s}\n ORDER BY scheduled_for DESC, id DESC LIMIT ? OFFSET ?`),a=void 0===t.scheduleId?[n+1,i]:[t.scheduleId,n+1,i],o=d.all(...a),u=[];for(const{id:t}of o.slice(0,n)){const n=le(e,t,r);void 0!==n&&u.push(n)}return{tasks:Ee(e,r),runs:u,hasMore:o.length>n}});return{status:r.status,...void 0===r.schemaVersion?{}:{schemaVersion:r.schemaVersion},tasks:r.value?.tasks??[],runs:r.value?.runs??[],hasMore:r.value?.hasMore??!1}}export function readScheduleRun(e,t){const n=ce(e,(e,n)=>le(e,t,n));return{status:n.status,...void 0===n.schemaVersion?{}:{schemaVersion:n.schemaVersion},run:n.value}}
|
|
@@ -1,29 +1,187 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const TRIGGER_KINDS: {
|
|
3
3
|
readonly interval: "interval";
|
|
4
|
+
readonly calendar: "calendar";
|
|
4
5
|
};
|
|
5
6
|
export type TriggerKind = (typeof TRIGGER_KINDS)[keyof typeof TRIGGER_KINDS];
|
|
6
7
|
export declare const intervalTriggerSchema: z.ZodObject<{
|
|
7
8
|
kind: z.ZodLiteral<"interval">;
|
|
8
9
|
everyMs: z.ZodNumber;
|
|
10
|
+
startAtMs: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
timeZone: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
9
12
|
}, "strict", z.ZodTypeAny, {
|
|
10
13
|
kind: "interval";
|
|
11
14
|
everyMs: number;
|
|
15
|
+
timeZone?: string | undefined;
|
|
16
|
+
startAtMs?: number | undefined;
|
|
12
17
|
}, {
|
|
13
18
|
kind: "interval";
|
|
14
19
|
everyMs: number;
|
|
20
|
+
timeZone?: string | undefined;
|
|
21
|
+
startAtMs?: number | undefined;
|
|
15
22
|
}>;
|
|
16
23
|
export declare const triggerSpecSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
17
24
|
kind: z.ZodLiteral<"interval">;
|
|
18
25
|
everyMs: z.ZodNumber;
|
|
26
|
+
startAtMs: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
timeZone: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
19
28
|
}, "strict", z.ZodTypeAny, {
|
|
20
29
|
kind: "interval";
|
|
21
30
|
everyMs: number;
|
|
31
|
+
timeZone?: string | undefined;
|
|
32
|
+
startAtMs?: number | undefined;
|
|
22
33
|
}, {
|
|
23
34
|
kind: "interval";
|
|
24
35
|
everyMs: number;
|
|
36
|
+
timeZone?: string | undefined;
|
|
37
|
+
startAtMs?: number | undefined;
|
|
38
|
+
}>, z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"calendar">;
|
|
40
|
+
calendar: z.ZodDiscriminatedUnion<"frequency", [z.ZodObject<{
|
|
41
|
+
time: z.ZodString;
|
|
42
|
+
timeZone: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
43
|
+
frequency: z.ZodLiteral<"daily">;
|
|
44
|
+
}, "strict", z.ZodTypeAny, {
|
|
45
|
+
time: string;
|
|
46
|
+
frequency: "daily";
|
|
47
|
+
timeZone: string;
|
|
48
|
+
}, {
|
|
49
|
+
time: string;
|
|
50
|
+
frequency: "daily";
|
|
51
|
+
timeZone: string;
|
|
52
|
+
}>, z.ZodObject<{
|
|
53
|
+
weekdays: z.ZodEffects<z.ZodArray<z.ZodNumber, "many">, number[], number[]>;
|
|
54
|
+
time: z.ZodString;
|
|
55
|
+
timeZone: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
56
|
+
frequency: z.ZodLiteral<"weekly">;
|
|
57
|
+
}, "strict", z.ZodTypeAny, {
|
|
58
|
+
time: string;
|
|
59
|
+
frequency: "weekly";
|
|
60
|
+
timeZone: string;
|
|
61
|
+
weekdays: number[];
|
|
62
|
+
}, {
|
|
63
|
+
time: string;
|
|
64
|
+
frequency: "weekly";
|
|
65
|
+
timeZone: string;
|
|
66
|
+
weekdays: number[];
|
|
67
|
+
}>]>;
|
|
68
|
+
startAtMs: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
}, "strict", z.ZodTypeAny, {
|
|
70
|
+
kind: "calendar";
|
|
71
|
+
calendar: {
|
|
72
|
+
time: string;
|
|
73
|
+
frequency: "daily";
|
|
74
|
+
timeZone: string;
|
|
75
|
+
} | {
|
|
76
|
+
time: string;
|
|
77
|
+
frequency: "weekly";
|
|
78
|
+
timeZone: string;
|
|
79
|
+
weekdays: number[];
|
|
80
|
+
};
|
|
81
|
+
startAtMs?: number | undefined;
|
|
82
|
+
}, {
|
|
83
|
+
kind: "calendar";
|
|
84
|
+
calendar: {
|
|
85
|
+
time: string;
|
|
86
|
+
frequency: "daily";
|
|
87
|
+
timeZone: string;
|
|
88
|
+
} | {
|
|
89
|
+
time: string;
|
|
90
|
+
frequency: "weekly";
|
|
91
|
+
timeZone: string;
|
|
92
|
+
weekdays: number[];
|
|
93
|
+
};
|
|
94
|
+
startAtMs?: number | undefined;
|
|
25
95
|
}>]>;
|
|
26
96
|
export type TriggerSpec = z.infer<typeof triggerSpecSchema>;
|
|
97
|
+
export declare const scheduleTimingInputSchema: z.ZodEffects<z.ZodObject<{
|
|
98
|
+
every: z.ZodOptional<z.ZodString>;
|
|
99
|
+
calendar: z.ZodOptional<z.ZodDiscriminatedUnion<"frequency", [z.ZodObject<{
|
|
100
|
+
time: z.ZodString;
|
|
101
|
+
frequency: z.ZodLiteral<"daily">;
|
|
102
|
+
} & {
|
|
103
|
+
timeZone: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
104
|
+
}, "strict", z.ZodTypeAny, {
|
|
105
|
+
time: string;
|
|
106
|
+
frequency: "daily";
|
|
107
|
+
timeZone?: string | undefined;
|
|
108
|
+
}, {
|
|
109
|
+
time: string;
|
|
110
|
+
frequency: "daily";
|
|
111
|
+
timeZone?: string | undefined;
|
|
112
|
+
}>, z.ZodObject<{
|
|
113
|
+
weekdays: z.ZodEffects<z.ZodArray<z.ZodNumber, "many">, number[], number[]>;
|
|
114
|
+
time: z.ZodString;
|
|
115
|
+
frequency: z.ZodLiteral<"weekly">;
|
|
116
|
+
} & {
|
|
117
|
+
timeZone: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
118
|
+
}, "strict", z.ZodTypeAny, {
|
|
119
|
+
time: string;
|
|
120
|
+
frequency: "weekly";
|
|
121
|
+
weekdays: number[];
|
|
122
|
+
timeZone?: string | undefined;
|
|
123
|
+
}, {
|
|
124
|
+
time: string;
|
|
125
|
+
frequency: "weekly";
|
|
126
|
+
weekdays: number[];
|
|
127
|
+
timeZone?: string | undefined;
|
|
128
|
+
}>]>>;
|
|
129
|
+
startAt: z.ZodOptional<z.ZodString>;
|
|
130
|
+
}, "strict", z.ZodTypeAny, {
|
|
131
|
+
every?: string | undefined;
|
|
132
|
+
calendar?: {
|
|
133
|
+
time: string;
|
|
134
|
+
frequency: "daily";
|
|
135
|
+
timeZone?: string | undefined;
|
|
136
|
+
} | {
|
|
137
|
+
time: string;
|
|
138
|
+
frequency: "weekly";
|
|
139
|
+
weekdays: number[];
|
|
140
|
+
timeZone?: string | undefined;
|
|
141
|
+
} | undefined;
|
|
142
|
+
startAt?: string | undefined;
|
|
143
|
+
}, {
|
|
144
|
+
every?: string | undefined;
|
|
145
|
+
calendar?: {
|
|
146
|
+
time: string;
|
|
147
|
+
frequency: "daily";
|
|
148
|
+
timeZone?: string | undefined;
|
|
149
|
+
} | {
|
|
150
|
+
time: string;
|
|
151
|
+
frequency: "weekly";
|
|
152
|
+
weekdays: number[];
|
|
153
|
+
timeZone?: string | undefined;
|
|
154
|
+
} | undefined;
|
|
155
|
+
startAt?: string | undefined;
|
|
156
|
+
}>, {
|
|
157
|
+
every?: string | undefined;
|
|
158
|
+
calendar?: {
|
|
159
|
+
time: string;
|
|
160
|
+
frequency: "daily";
|
|
161
|
+
timeZone?: string | undefined;
|
|
162
|
+
} | {
|
|
163
|
+
time: string;
|
|
164
|
+
frequency: "weekly";
|
|
165
|
+
weekdays: number[];
|
|
166
|
+
timeZone?: string | undefined;
|
|
167
|
+
} | undefined;
|
|
168
|
+
startAt?: string | undefined;
|
|
169
|
+
}, {
|
|
170
|
+
every?: string | undefined;
|
|
171
|
+
calendar?: {
|
|
172
|
+
time: string;
|
|
173
|
+
frequency: "daily";
|
|
174
|
+
timeZone?: string | undefined;
|
|
175
|
+
} | {
|
|
176
|
+
time: string;
|
|
177
|
+
frequency: "weekly";
|
|
178
|
+
weekdays: number[];
|
|
179
|
+
timeZone?: string | undefined;
|
|
180
|
+
} | undefined;
|
|
181
|
+
startAt?: string | undefined;
|
|
182
|
+
}>;
|
|
183
|
+
export type ScheduleTimingInput = z.input<typeof scheduleTimingInputSchema>;
|
|
184
|
+
export declare function createScheduleTrigger(input: ScheduleTimingInput, nowMs?: number): TriggerSpec;
|
|
27
185
|
export declare class ScheduleTriggerError extends Error {
|
|
28
186
|
readonly code: "schedule_trigger_invalid";
|
|
29
187
|
constructor(message: string);
|
|
@@ -32,7 +190,8 @@ export declare function formatDuration(ms: number): string;
|
|
|
32
190
|
export declare function formatInterval(ms: number): string;
|
|
33
191
|
export declare function parseIntervalText(text: string): number;
|
|
34
192
|
export declare function parseMaxRunText(text: string): number;
|
|
35
|
-
export declare function createIntervalTrigger(text: string):
|
|
193
|
+
export declare function createIntervalTrigger(text: string): z.infer<typeof intervalTriggerSchema>;
|
|
36
194
|
export declare function describeTrigger(trigger: TriggerSpec): string;
|
|
37
195
|
export declare function computeNextRunAtMs(trigger: TriggerSpec, nowMs: number): number;
|
|
196
|
+
export declare function computeFirstRunAtMs(trigger: TriggerSpec, nowMs: number): number;
|
|
38
197
|
export declare function parseTriggerJson(json: string): TriggerSpec;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as r}from"zod";import{SCHEDULER_LIMITS as e}from"./limits.js";export const TRIGGER_KINDS={interval:"interval"};export const intervalTriggerSchema=r.object({kind:r.literal(TRIGGER_KINDS.interval),everyMs:r.number().int().min(e.minIntervalMs).max(e.maxIntervalMs)}).strict();export const triggerSpecSchema=r.discriminatedUnion("kind",[intervalTriggerSchema]);export class ScheduleTriggerError extends Error{code="schedule_trigger_invalid";constructor(r){super(r),this.name="ScheduleTriggerError"}}const
|
|
1
|
+
import{z as r}from"zod";import{SCHEDULER_LIMITS as e}from"./limits.js";import{calendarScheduleSchema as t,calendarScheduleInputSchema as n,describeCalendar as a,nextCalendarRunAtMs as i,parseLocalStartAt as s,systemTimeZone as o,timeZoneSchema as c}from"./calendar.js";export const TRIGGER_KINDS={interval:"interval",calendar:"calendar"};export const intervalTriggerSchema=r.object({kind:r.literal(TRIGGER_KINDS.interval),everyMs:r.number().int().min(e.minIntervalMs).max(e.maxIntervalMs),startAtMs:r.number().int().min(0).max(2534022144e5).optional(),timeZone:c.optional()}).strict();const l=r.object({kind:r.literal(TRIGGER_KINDS.calendar),calendar:t,startAtMs:r.number().int().min(0).max(2534022144e5).optional()}).strict();export const triggerSpecSchema=r.discriminatedUnion("kind",[intervalTriggerSchema,l]);export const scheduleTimingInputSchema=r.object({every:r.string().trim().min(1).optional(),calendar:n.optional(),startAt:r.string().trim().min(1).optional()}).strict().refine(r=>void 0===r.every!=(void 0===r.calendar),{message:"every 与 calendar 必须且只能提供一个"});export function createScheduleTrigger(e,t=Date.now()){const n=scheduleTimingInputSchema.safeParse(e);if(!n.success)throw new ScheduleTriggerError(n.error.issues[0]?.message??"定时参数无效");const a=n.data,i=a.calendar?.timeZone??o();let c;if(void 0!==a.startAt){try{c=/(?:Z|[+-]\d{2}:\d{2})$/u.test(a.startAt)?Date.parse(r.string().datetime({offset:!0}).parse(a.startAt)):s(a.startAt,i)}catch(r){throw new ScheduleTriggerError(`startAt 无效:${r instanceof Error?r.message:String(r)}`)}if(!Number.isFinite(c)||c<=t)throw new ScheduleTriggerError("startAt 必须是未来时间")}const l=void 0===c?{}:{startAtMs:c},m=void 0!==a.calendar?{kind:TRIGGER_KINDS.calendar,calendar:{...a.calendar,timeZone:i},...l}:{...createIntervalTrigger(a.every??""),...l,...void 0===c?{}:{timeZone:i}},d=triggerSpecSchema.safeParse(m);if(!d.success)throw new ScheduleTriggerError(d.error.issues[0]?.message??"定时参数无效");return d.data}export class ScheduleTriggerError extends Error{code="schedule_trigger_invalid";constructor(r){super(r),this.name="ScheduleTriggerError"}}const m={s:1e3,m:6e4,h:36e5,d:864e5},d=/^(\d{1,9})([smhd])$/u;function u(r){return Object.hasOwn(m,r)}export function formatDuration(r){return r%m.d===0?`${String(r/m.d)} 天`:r%m.h===0?`${String(r/m.h)} 小时`:r%m.m===0?`${String(r/m.m)} 分钟`:`${String(Math.round(r/m.s))} 秒`}export function formatInterval(r){return`每 ${formatDuration(r)}`}function g(r,e){const t=r.trim(),n=d.exec(t),a=n?.[1],i=n?.[2];if(void 0===a||void 0===i||!u(i))throw new ScheduleTriggerError(`无法识别的${e.label} "${t}":格式为 <数字><s|m|h|d>,例如 30m、2h、1d`);const s=Number.parseInt(a,10);if(s<=0)throw new ScheduleTriggerError(`${e.label}必须大于 0`);const o=s*m[i];if(o<e.minMs)throw new ScheduleTriggerError(`${e.label}不能小于 ${String(e.minMs/m.s)} 秒(收到 ${t})`);if(o>e.maxMs)throw new ScheduleTriggerError(`${e.label}不能大于 ${formatDuration(e.maxMs)}(收到 ${t})`);return o}export function parseIntervalText(r){return g(r,{label:"间隔",minMs:e.minIntervalMs,maxMs:e.maxIntervalMs})}export function parseMaxRunText(r){return g(r,{label:"单次运行上限",minMs:e.minMaxRunMs,maxMs:e.maxRunCeilingMs})}export function createIntervalTrigger(r){return{kind:TRIGGER_KINDS.interval,everyMs:parseIntervalText(r)}}export function describeTrigger(r){const e=r.kind===TRIGGER_KINDS.interval?formatInterval(r.everyMs):a(r.calendar);if(void 0===r.startAtMs)return e;const t=r.kind===TRIGGER_KINDS.calendar?r.calendar.timeZone:r.timeZone??"UTC";return`${e},从 ${new Date(r.startAtMs).toLocaleString("zh-CN",{timeZone:t,hour12:!1})}(${t})开始`}export function computeNextRunAtMs(r,e){return r.kind===TRIGGER_KINDS.interval?Math.max(e+r.everyMs,r.startAtMs??-1/0):i(r.calendar,Math.max(e,(r.startAtMs??0)-1))}export function computeFirstRunAtMs(r,e){if(void 0!==r.startAtMs&&r.startAtMs<=e)throw new ScheduleTriggerError("startAt 必须是未来时间;开始时间已过,请重新创建");return r.kind===TRIGGER_KINDS.interval&&void 0!==r.startAtMs?r.startAtMs:computeNextRunAtMs(r,e)}export function parseTriggerJson(r){let e;try{e=JSON.parse(r)}catch{throw new ScheduleTriggerError("trigger 不是合法 JSON")}const t=triggerSpecSchema.safeParse(e);if(!t.success)throw new ScheduleTriggerError(`trigger 不合法(${t.error.issues[0]?.path.join(".")??""}):${t.error.issues[0]?.message??"unknown"}`);return t.data}
|
|
@@ -193,6 +193,8 @@ export interface CreateScheduleInput {
|
|
|
193
193
|
readonly cwd: string;
|
|
194
194
|
readonly trigger: TriggerSpec;
|
|
195
195
|
readonly fireImmediately?: boolean;
|
|
196
|
+
/** Approval snapshot, checked atomically when inserting a new schedule. */
|
|
197
|
+
readonly expectedFirstRunAtMs?: number;
|
|
196
198
|
readonly authorityDigest?: string;
|
|
197
199
|
readonly maxRunMs?: number;
|
|
198
200
|
readonly maxRounds?: number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StopCondition, ToolSet } from "ai";
|
|
2
|
+
/** Local to one inference sequence; never blocks repaired inputs or future user turns. */
|
|
3
|
+
export declare function createInvalidScheduleRetryGuard(): {
|
|
4
|
+
stop: StopCondition<ToolSet>;
|
|
5
|
+
readonly message: string | undefined;
|
|
6
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isDeepStrictEqual as o}from"node:util";import{readToolOutcome as t,TOOL_OUTCOME_KINDS as e}from"./normalize-result.js";import{SCHEDULE_CREATE_TOOL_ID as r}from"./schedule-tool.js";export function createInvalidScheduleRetryGuard(){let n;return{stop:({steps:l})=>{const i=[];for(const u of l)for(const l of u.toolCalls){if(l.toolName!==r)continue;const s=u.toolResults.find(o=>o.toolCallId===l.toolCallId);if(!0===l.invalid||void 0!==s&&t(s.output).kind===e.invalidInput){if(i.some(t=>o(t,l.input)))return n="定时任务参数连续重复出错,已停止本轮自动重试。请按错误提示修正 recurrence;若只指定了开始时间和总轮数,请补充两轮间隔。此前已成功执行的其他操作不会回滚。",!0;i.push(l.input)}}return!1},get message(){return n}}}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ScheduleToolCreateRequest } from "@roll-agent/core/scheduler-host/schedule-tool-binding";
|
|
3
|
+
/** Provider strict mode requires every property; null represents only declared defaults. */
|
|
4
|
+
export declare const scheduleCreateInputSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
prompt: z.ZodString;
|
|
7
|
+
recurrence: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
8
|
+
kind: z.ZodLiteral<"interval">;
|
|
9
|
+
every: z.ZodString;
|
|
10
|
+
}, "strict", z.ZodTypeAny, {
|
|
11
|
+
every: string;
|
|
12
|
+
kind: "interval";
|
|
13
|
+
}, {
|
|
14
|
+
every: string;
|
|
15
|
+
kind: "interval";
|
|
16
|
+
}>, z.ZodObject<{
|
|
17
|
+
time: z.ZodString;
|
|
18
|
+
} & {
|
|
19
|
+
kind: z.ZodLiteral<"daily">;
|
|
20
|
+
timeZone: z.ZodNullable<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
21
|
+
}, "strict", z.ZodTypeAny, {
|
|
22
|
+
time: string;
|
|
23
|
+
kind: "daily";
|
|
24
|
+
timeZone: string | null;
|
|
25
|
+
}, {
|
|
26
|
+
time: string;
|
|
27
|
+
kind: "daily";
|
|
28
|
+
timeZone: string | null;
|
|
29
|
+
}>, z.ZodObject<{
|
|
30
|
+
time: z.ZodString;
|
|
31
|
+
weekdays: z.ZodEffects<z.ZodArray<z.ZodNumber, "many">, number[], number[]>;
|
|
32
|
+
} & {
|
|
33
|
+
kind: z.ZodLiteral<"weekly">;
|
|
34
|
+
timeZone: z.ZodNullable<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
35
|
+
}, "strict", z.ZodTypeAny, {
|
|
36
|
+
time: string;
|
|
37
|
+
kind: "weekly";
|
|
38
|
+
timeZone: string | null;
|
|
39
|
+
weekdays: number[];
|
|
40
|
+
}, {
|
|
41
|
+
time: string;
|
|
42
|
+
kind: "weekly";
|
|
43
|
+
timeZone: string | null;
|
|
44
|
+
weekdays: number[];
|
|
45
|
+
}>]>;
|
|
46
|
+
startAt: z.ZodNullable<z.ZodString>;
|
|
47
|
+
cwd: z.ZodNullable<z.ZodString>;
|
|
48
|
+
rounds: z.ZodNullable<z.ZodNumber>;
|
|
49
|
+
maxRun: z.ZodNullable<z.ZodString>;
|
|
50
|
+
}, "strict", z.ZodTypeAny, {
|
|
51
|
+
name: string;
|
|
52
|
+
prompt: string;
|
|
53
|
+
cwd: string | null;
|
|
54
|
+
startAt: string | null;
|
|
55
|
+
maxRun: string | null;
|
|
56
|
+
rounds: number | null;
|
|
57
|
+
recurrence: {
|
|
58
|
+
every: string;
|
|
59
|
+
kind: "interval";
|
|
60
|
+
} | {
|
|
61
|
+
time: string;
|
|
62
|
+
kind: "daily";
|
|
63
|
+
timeZone: string | null;
|
|
64
|
+
} | {
|
|
65
|
+
time: string;
|
|
66
|
+
kind: "weekly";
|
|
67
|
+
timeZone: string | null;
|
|
68
|
+
weekdays: number[];
|
|
69
|
+
};
|
|
70
|
+
}, {
|
|
71
|
+
name: string;
|
|
72
|
+
prompt: string;
|
|
73
|
+
cwd: string | null;
|
|
74
|
+
startAt: string | null;
|
|
75
|
+
maxRun: string | null;
|
|
76
|
+
rounds: number | null;
|
|
77
|
+
recurrence: {
|
|
78
|
+
every: string;
|
|
79
|
+
kind: "interval";
|
|
80
|
+
} | {
|
|
81
|
+
time: string;
|
|
82
|
+
kind: "daily";
|
|
83
|
+
timeZone: string | null;
|
|
84
|
+
} | {
|
|
85
|
+
time: string;
|
|
86
|
+
kind: "weekly";
|
|
87
|
+
timeZone: string | null;
|
|
88
|
+
weekdays: number[];
|
|
89
|
+
};
|
|
90
|
+
}>;
|
|
91
|
+
export declare function toScheduleCreateRequest(input: z.output<typeof scheduleCreateInputSchema>): ScheduleToolCreateRequest;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{z as e}from"zod";import{calendarScheduleSchema as n,timeZoneSchema as t}from"../scheduler/calendar.js";const r=e.object({kind:e.literal("interval"),every:e.string().trim().min(1).describe("两轮间隔,如 1m、30m(60s..365d);用户未指定时先询问,不能猜测")}).strict(),i=n.options[0].omit({frequency:!0}).extend({kind:e.literal("daily"),timeZone:t.nullable().describe("IANA 时区;null 使用并保存运行 Roll 的机器时区")}),l=n.options[1].omit({frequency:!0}).extend({kind:e.literal("weekly"),timeZone:i.shape.timeZone});export const scheduleCreateInputSchema=e.object({name:e.string().trim().min(1).max(120).describe("任务名称"),prompt:e.string().trim().min(1).max(4e3).describe("每轮执行的工作;不要把触发时间或停止条件只写在 prompt 中"),recurrence:e.discriminatedUnion("kind",[r,i,l]).describe("重复规则:interval=固定间隔,daily=每天,weekly=每周。今天/明天某时只指定 startAt,不代表 daily。缺少两轮间隔时先询问用户"),startAt:e.string().trim().min(1).nullable().describe("未来开始时间 YYYY-MM-DDTHH:mm 或带偏移 ISO;null 不指定。interval 首轮在此时执行,daily/weekly 在此时起找匹配日期;不是重复规则"),cwd:e.string().nullable().describe("执行工作目录;null 使用当前会话目录"),rounds:e.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable().describe("总自动轮数;null 不限。失败、取消、空结果占轮数;同轮重试和手动运行不额外计数"),maxRun:e.string().nullable().describe("单轮时长上限,如 1h(60s..24h);null 使用默认 1 小时")}).strict();export function toScheduleCreateRequest(e){const{recurrence:n,startAt:t,cwd:r,rounds:i,maxRun:l,name:m,prompt:a}=e,s={name:m,prompt:a,...null===t?{}:{startAt:t},...null===r?{}:{cwd:r},...null===i?{}:{rounds:i},...null===l?{}:{maxRun:l}};if("interval"===n.kind)return{...s,every:n.every};const d={time:n.time,...null===n.timeZone?{}:{timeZone:n.timeZone}};return{...s,calendar:"daily"===n.kind?{frequency:"daily",...d}:{frequency:"weekly",...d,weekdays:n.weekdays}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{tool as e}from"ai";import{z as t}from"zod";import{buildScheduleExtendTools as r}from"./schedule-extend-tool.js";import{SCHEDULE_STATUSES as n}from"../scheduler/types.js";import{
|
|
1
|
+
import{tool as e}from"ai";import{z as t}from"zod";import{buildScheduleExtendTools as r}from"./schedule-extend-tool.js";import{SCHEDULE_STATUSES as n}from"../scheduler/types.js";import{systemTimeZone as i}from"../scheduler/calendar.js";import{computeNextRunAtMs as o}from"../scheduler/trigger.js";import{scheduleCreateInputSchema as s,toScheduleCreateRequest as a}from"./schedule-create-input.js";import{gateToolCall as u}from"./build-tools.js";import{TOOL_OUTCOME_KINDS as c,failedToolResult as l,successfulToolResult as d,toolResultToModelOutput as m}from"./normalize-result.js";import{TOOL_RESOURCE_ACCESS_MODES as p,executeCoordinatedTool as g}from"./tool-execution-coordinator.js";export const SCHEDULE_TOOL_AGENT_NAME="roll";export const SCHEDULE_CREATE_TOOL_NAME="schedule_create";export const SCHEDULE_LIST_TOOL_NAME="schedule_list";export const SCHEDULE_CREATE_TOOL_ID="roll__schedule_create";export const SCHEDULE_LIST_TOOL_ID="roll__schedule_list";const $=t.object({status:t.enum(["all",...Object.values(n)]).optional().describe("按状态过滤,省略时返回全部"),offset:t.number().int().min(0).optional().describe("分页偏移,默认 0"),limit:t.number().int().min(1).max(100).optional().describe("最多返回条数,默认 50")}),f=Symbol("schedule-invalid-input");function h(e){return"object"==typeof e&&null!==e&&e.kind===f}function _(e){return"object"==typeof e&&null!==e&&!1===e.ok}function x(e){const t="invalid_input"===e.code||"schedule_invalid"===e.code||"schedule_trigger_invalid"===e.code?c.invalidInput:c.toolFailed;return l(t,e.message,{raw:e})}function y(e){return 0===e.warnings.length?"":`\n${e.warnings.map(e=>`注意:${e.message}。`).join("\n")}`}function v(e){const t="calendar"===e.trigger.kind?e.trigger.calendar.timeZone:e.trigger.timeZone??i();return{name:e.name,prompt:e.prompt,every:e.everyDisplay,recurrence:e.everyDisplay,cwd:e.cwd,maxRun:e.maxRunDisplay,firstRunAt:E(e.firstRunAt,t),firstRunAtIso:e.firstRunAt,timeZone:t,...1===e.maxRounds?{}:{nextRunAtEstimate:E(new Date(o(e.trigger,Date.parse(e.firstRunAt))).toISOString(),t)},timingSemantics:"interval"===e.trigger.kind?"第二轮时间仅为预计;按每轮实际领取时间加间隔计算,延迟领取时顺延,不固定每天执行":"按保存的时区和日历规则匹配;跳时日期跳过,重复钟点仅执行较早一次",rounds:void 0===e.maxRounds?"不限轮数":`最多自动执行 ${String(e.maxRounds)} 轮`,lifecycle:void 0===e.maxRounds?"会持续运行,直到暂停或删除;创建时记录当前权限边界":"达到轮数上限后,最后一轮执行、重试及清场结算完毕自动结束;创建时记录当前权限边界",...e.readiness.warnings.length>0?{serviceStatus:e.readiness.warnings.map(e=>e.message).join(";")}:{}}}function E(e,t=i()){const r=new Date(e);return Number.isNaN(r.getTime())?e:r.toLocaleString("zh-CN",{timeZone:t,hour12:!1})}export function buildScheduleToolset(t,n,o){const S=t.includeCreate??!0,R=S?n.register("roll","schedule_create"):void 0,j=n.register("roll","schedule_list"),A={captureExecutionState:e=>{const r=s.safeParse(e);return r.success?t.port.captureCreate(a(r.data),t.sessionCwd):{kind:f,message:`参数校验失败: ${r.error.issues.map(e=>`${e.path.join(".")||"input"}: ${e.message}`).join("; ")}。使用唯一 recurrence 分支;今天/明天某时填写 startAt,缺少两轮间隔先询问用户。缺省字段填写 null。不要原样重试相同参数。`}},prepare:async(e,t)=>{if(h(t))return l(c.invalidInput,t.message);if(_(t))return x(t);const r=t;return u(o,"roll","schedule_create",v(r),void 0,{explanation:`将登记定时任务「${r.name}」(${r.everyDisplay})`})},resources:()=>[{key:"schedule-ledger",mode:p.write}]};void 0!==R&&o.coordinator?.register(R,A);const w={prepare:async e=>u(o,"roll","schedule_list",e??{},{readOnlyHint:!0}),resources:()=>[{key:"schedule-ledger",mode:p.read}]};o.coordinator?.register(j,w);const D=void 0===R?{}:{[R]:e({description:"登记定时任务,每轮无人值守 chat 执行 prompt。startAt=开始时间,recurrence=重复规则,rounds=总轮数,三个维度独立。今天/明天某时不是 daily;缺少两轮间隔先澄清,不能猜成每天一次。信息齐全后只做工具自带的完整参数确认,不重复口头确认。可选值用 null 表示缺省。不支持 cron 或每天开启一组多轮循环。",inputSchema:s,strict:!0,toModelOutput:({output:e})=>m(e),execute:async(e,r)=>g(o.coordinator,A,R,r.toolCallId,e,r.abortSignal,async e=>{if(h(e))return l(c.invalidInput,e.message);if(_(e))return x(e);const r=e,n=await t.port.create(r);if(_(n))return x(n);const o=n.schedule,s="calendar"===o.trigger.spec?.kind?o.trigger.spec.calendar.timeZone:o.trigger.spec?.timeZone??i(),a=n.created?`已登记定时任务 "${o.name}"(${o.trigger.display},单次上限 ${o.maxRun.display})。`:n.reauthorized?`已存在相同定义的任务 "${o.name}"(id ${o.id}),未重复创建;已按当前权限边界重新授权。`:`已存在相同定义的任务 "${o.name}"(id ${o.id}),未重复创建。`,u=void 0===o.nextRunAt?"":`下次运行约 ${E(o.nextRunAt,s)}(${s})。`,m=y(n.readiness)||(n.readiness.automaticRunsReady?"":"\n注意:调度服务未就绪,任务不会自动执行。");return d(`${a}${null===o.rounds.max?"":`最多自动执行 ${String(o.rounds.max)} 轮。`}${o.roundsDisplay}。${u}${m}`,{raw:n})})})},b={[j]:e({description:"列出当前配置下已登记的定时任务(含状态、频率、下次运行时间与最近错误摘要)。创建前可用它确认是否已有同类任务。",inputSchema:$,toModelOutput:({output:e})=>m(e),execute:async(e,r)=>g(o.coordinator,w,j,r.toolCallId,e,r.abortSignal,async()=>{const r=$.safeParse(e??{});if(!r.success)return l(c.invalidInput,`参数校验失败: ${r.error.issues.map(e=>e.message).join("; ")}`);const n=await t.port.list(r.data,t.sessionCwd);if(_(n))return x(n);if(0===n.total)return d(`暂无定时任务。${y(n.readiness)}`.trim(),{raw:n});const o=n.schedules.map(e=>{const t="active"===e.status&&void 0!==e.nextRunAt?`,下次 ${E(e.nextRunAt,e.timeZone)}(${e.timeZone??i()})`:"",r=void 0===e.lastError?"":`,最近错误:${e.lastError}`;return`- ${e.name}(${e.status},${e.trigger},${e.roundsDisplay}${t})id=${e.id}\n 内容:${e.promptExcerpt}${r}`}),s=n.hasMore?`\n共 ${String(n.total)} 个,仅显示 ${String(n.schedules.length)} 个;用 offset/limit 翻页。`:"";return d(`${o.join("\n")}${s}${y(n.readiness)}`,{raw:n})})})};return{createTools:D,extendTools:S?r(t,n,o):{},listTools:b}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"zod": "^3.25.76"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@roll-agent/core": "0.
|
|
38
|
+
"@roll-agent/core": "0.40.0",
|
|
39
39
|
"@types/node": "^22.0.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|