@roll-agent/runtime 0.19.1 → 0.20.1
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.d.ts +35 -4
- package/dist/engine/agent-session.js +1 -1
- package/dist/engine/compaction-checkpoint.d.ts +16 -16
- package/dist/engine/context-window.d.ts +18 -0
- package/dist/engine/context-window.js +1 -1
- package/dist/engine/conversation-engine.d.ts +28 -4
- package/dist/engine/conversation-engine.js +1 -1
- package/dist/engine/model-catalog-default.d.ts +2 -0
- package/dist/engine/model-catalog-default.js +1 -0
- package/dist/engine/model-catalog-snapshot.d.ts +2 -0
- package/dist/engine/model-catalog-snapshot.js +1 -0
- package/dist/engine/model-catalog.d.ts +72 -0
- package/dist/engine/model-catalog.js +1 -0
- package/dist/engine/system-prompt.d.ts +2 -0
- package/dist/engine/system-prompt.js +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1 -1
- package/dist/scheduler/schedule-store.d.ts +24 -1
- package/dist/scheduler/schedule-store.js +1 -1
- package/dist/scheduler/types.d.ts +38 -0
- package/dist/server/protocol.d.ts +2 -2
- package/dist/service/runtime-service.js +1 -1
- package/dist/store/thread-origin.d.ts +135 -0
- package/dist/store/thread-origin.js +1 -0
- package/dist/store/thread-store.d.ts +44 -2
- package/dist/store/thread-store.js +1 -1
- package/dist/tool-bridge/build-tools.d.ts +2 -0
- package/dist/tool-bridge/build-tools.js +1 -1
- package/dist/tool-bridge/file-tools/read-file-tool.d.ts +2 -2
- package/dist/tool-bridge/transcript-tool.d.ts +2 -2
- package/dist/types/cancellation.d.ts +2 -2
- 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 a}from"node:sqlite";import{expandTilde as o}from"../store/thread-store.js";import{SCHEDULER_LIMITS as d}from"./limits.js";import{TRIGGER_KINDS as c,computeNextRunAtMs as u,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 m,INVOCATION_LIVE_STATUSES as T,INVOCATION_MODES as v,INVOCATION_STATUSES as R,INVOCATION_TERMINAL_STATUSES as L,INVOCATION_TREE_LIVENESS as S,TRACKED_LEADER_STATES as g,SCHEDULE_STATUSES as N,SCHEDULE_STORE_ERROR_CODES as f,ScheduleStoreError as A}from"./types.js";const I=5,M=15e3,w=L.map(()=>"?").join(", "),b={dead:"dead",notDead:"not-dead",deferred:"deferred"},O={valid:"valid",invalid:"invalid"};function x(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(5)}`)}function D(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>5)throw x(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")}const C=Object.values(v),k=Object.values(R);function y(e){return C.includes(e)}function U(e){return k.includes(e)}function F(e){try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}function H(e){if(null===e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(P):[]}catch{return[]}}function P(e){return"number"==typeof e&&Number.isInteger(e)&&e>0}function W(e){return e===g.alive||e===g.exited||e===g.unknown}function B(e){return P(e)?{pgid:e,leaderState:g.unknown}:"object"==typeof e&&null!==e&&"pgid"in e&&P(e.pgid)&&"leaderState"in e&&W(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=B(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 G(e){return new A(f.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function X(e,t){const n=$(e);if(n.kind===O.invalid)throw G(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 Y(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const q="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",J=[R.claimed,R.running,R.retry],z=J;function K(e){return null!==e.executor_pid&&null!==e.executor_start_token?{pid:e.executor_pid,startToken:e.executor_start_token}:void 0}function V(e){if(!y(e.mode)||!U(e.status))throw new A(f.invalid,`invocation ${e.id} 的 mode/status 非法: ${e.mode}/${e.status}`);const t=X(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:K(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:F(e.pending_actions_json),treeTrackedPgids:t.map(e=>e.pgid),treeTrackedGroups:t,treeUnsettled:1===e.tree_unsettled,treeSurvivorPids:H(e.tree_survivor_pids),createdAtMs:e.created_at,startedAtMs:e.started_at??void 0,finishedAtMs:e.finished_at??void 0}}function Q(e){return e===N.active||e===N.paused}function Z(e){if(!Q(e.status))throw new A(f.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);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:ee(e.max_run_ms),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 ee(e){if(null!=e){if("number"!=typeof e||!Number.isInteger(e)||e<d.minMaxRunMs||e>d.maxRunCeilingMs)throw new A(f.invalid,`maxRunMs 必须是 ${String(d.minMaxRunMs)}..${String(d.maxRunCeilingMs)} 的整数毫秒`);return e}}function te(e){const t=e.name.trim();if(0===t.length||t.length>d.maxNameChars)throw new A(f.invalid,`name 必须为 1..${String(d.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>d.maxPromptChars)throw new A(f.invalid,`prompt 必须为 1..${String(d.maxPromptChars)} 个字符`);if(!i(e.cwd))throw new A(f.invalid,"cwd 必须是绝对路径");const r=h.safeParse(e.trigger);if(!r.success)throw new A(f.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:r.data,maxRunMs:ee(e.maxRunMs)}}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??d.claimLeaseMs,this.retryBudget=s.retryBudget??d.retryBudget,this.retryBackoffMs=s.retryBackoffMs??d.retryBackoffMs,this.executorLiveness=s.executorLiveness??(()=>_.unknown),this.treeLiveness=s.treeLiveness,this.maxLivenessProbesPerClaim=s.maxLivenessProbesPerClaim??d.maxLivenessProbesPerClaim,this.livenessProbeDeferralMs=s.livenessProbeDeferralMs??d.livenessProbeDeferralMs,this.invocationRetentionPerSchedule=s.invocationRetentionPerSchedule??d.invocationRetentionPerSchedule,this.invocationRetentionMs=s.invocationRetentionMs??d.invocationRetentionMs;const c=o(i);if(!0===s.requireExistingDatabase&&!t(c))throw new Error("authoritative scheduler database does not exist");t(c)||n(c,{recursive:!0,mode:448}),"win32"!==process.platform&&e(c,448);const u=r(c,"schedules.db");if(!0===s.requireExistingDatabase&&!t(u))throw new Error("authoritative scheduler database does not exist");if(this.db=new a(u),!0===s.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(M)}`),D(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{e(u,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(M)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>5)throw x(e.user_version);this.transaction(()=>{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')),\n authority_digest TEXT,\n max_run_ms INTEGER,\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(d.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');`),e.user_version<5&&this.addMissingColumns(),this.db.exec(`PRAGMA user_version = ${String(5)};`)})}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(d.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!==c.interval||!("everyMs"in e)||"number"!=typeof e.everyMs||e.everyMs<=d.maxIntervalMs||this.db.prepare("UPDATE schedules SET trigger_json = ?, status = ?, last_error = ?, updated_at = ?\n WHERE id = ?").run(JSON.stringify({kind:c.interval,everyMs:d.maxIntervalMs}),N.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=te(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t))}createScheduleIdempotent(e,t=Date.now()){const n=te(e),i=String(d.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})`).get(N.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??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:Z(r)}}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 A(f.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);const i=s(),r=!0===t.fireImmediately?n:u(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)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),N.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n),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(()=>1===this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(N.active,t,n,e).changes)}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?Z(t):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(Z)}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===N.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,v.scheduled,R.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(R.running).map(V)}listActiveWorkerInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status IN (?, ?)\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC").all(R.claimed,R.running).map(V)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${q}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...J).map(V)}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,R.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,R.running).map(e=>{if(null===e.ownership_token)throw new A(f.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new A(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 ${q} LIMIT 1`).get(e,...J);if(void 0!==t)throw new A(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,R.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new A(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 ${q})`).run(R.claimed,t,i,n+this.claimLeaseMs,e,R.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 ${q}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...J);return t?V(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,R.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!==R.running||t)return;const n=K(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&&U(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||!U(e.status))return E.notFound;if(!T.includes(e.status))return E.terminal;const a=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==a)return a;const o=!0===i.abandon;return!o&&this.treeBlocksTerminal(e)?E.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:o})?E.cancelled:E.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===R.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(N.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return Z(e)}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 ${q} 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(R.pending,R.retry,e.nowMs,R.claimed,R.running,e.nowMs),r=[],a=i.filter(e=>e.status===R.running&&!t.has(e.id)&&void 0!==K(e));a.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const o=new Map;let d=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of a){const n=K(t);void 0!==n&&(d<=0?o.set(t.id,b.deferred):(d-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),o.set(t.id,this.executorLiveness(n)===_.dead?b.dead:b.notDead)))}for(const n of i){const i=n.status===R.claimed||n.status===R.running,s=o.get(n.id);!i||!t.has(n.id)&&s!==b.notDead?s!==b.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!==R.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===v.scheduled&&i.status===N.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===R.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 a=s();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(R.claimed,e.workerId,a,e.nowMs+this.claimLeaseMs,r,t.id);const o=this.loadClaim(t.id,a);void 0!==o&&n.push(o)}if(n.length>=e.limit)return n;const c=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 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(N.active,e.nowMs,R.pending,R.claimed,R.running,R.retry,this.maxSchedules);for(const t of c){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=s(),a=s(),o=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,R.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,a,e.nowMs+this.claimLeaseMs,e.nowMs);if(this.db.prepare("UPDATE schedules SET next_run_at = ?, updated_at = ? WHERE id = ?").run(u(i.trigger,e.nowMs),e.nowMs,t.id),1===o.changes){const e=this.loadClaim(r,a);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(R.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,R.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,R.claimed,R.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!==R.claimed&&t.status!==R.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,R.claimed,R.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),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!==R.completed&&t.status!==R.needsConfirmation)return m.lostClaim;if(this.treeBlocksTerminal(t))return m.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,R.completed,R.needsConfirmation).changes?m.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===v.scheduled?m.terminalPaused:m.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(R.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,R.completed,R.needsConfirmation).changes?m.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),m.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?j(t):null,i=Y(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,R.claimed,R.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,R.claimed,R.running,R.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!==R.claimed&&s.status!==R.running?m.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?m.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===v.scheduled?m.terminalPaused:m.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(R.retry,i+this.retryBackoffMs,n,e),m.retryScheduled)})}getInvocation(e){const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return t?V(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(V)}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(...L,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(...L,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 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(N.active,R.retry,R.claimed,R.running,R.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(V(e))!==S.settled)}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(R.failed,t,n,e),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===v.scheduled?this.db.prepare("UPDATE schedules SET status = ?, last_error = ?, updated_at = ? WHERE id = ?").run(N.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 A(f.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new A(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(o(e),"schedules.db");if(!t(n))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=new a(n,{readOnly:!0});try{i.exec(`PRAGMA busy_timeout = ${String(M)}`);const e=i.prepare("PRAGMA user_version").get();if(5!==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(Z)}}finally{i.close()}}
|
|
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 a}from"node:sqlite";import{expandTilde as d}from"../store/thread-store.js";import{SCHEDULER_LIMITS as o}from"./limits.js";import{TRIGGER_KINDS as c,computeNextRunAtMs as u,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 m,INVOCATION_LIVE_STATUSES as T,INVOCATION_MODES as v,INVOCATION_STATUSES as R,INVOCATION_TERMINAL_STATUSES as S,INVOCATION_TREE_LIVENESS as L,TRACKED_LEADER_STATES as f,SCHEDULE_STATUSES as N,SCHEDULE_STORE_ERROR_CODES as g,ScheduleStoreError as A}from"./types.js";const I=6,M=15e3,w=S.map(()=>"?").join(", "),O={dead:"dead",notDead:"not-dead",deferred:"deferred"},D={valid:"valid",invalid:"invalid"};function C(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(6)}`)}function b(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>6)throw C(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 x(e){if(!F(e.mode))throw new A(g.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 A(g.invalid,"会话关联需要有效的 attempt、会话 ID 和绝对存储路径")}const y=Object.values(v),k=Object.values(R);function F(e){return y.includes(e)}function H(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===f.alive||e===f.exited||e===f.unknown}function $(e){return B(e)?{pgid:e,leaderState:f.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 X(e){if(null===e)return{kind:D.valid,groups:[]};try{const t=JSON.parse(e);if(!Array.isArray(t))return{kind:D.invalid};const n=new Map;for(const e of t){const t=$(e);if(void 0===t)return{kind:D.invalid};n.set(t.pgid,t)}return{kind:D.valid,groups:[...n.values()]}}catch{return{kind:D.invalid}}}function j(e){return new A(g.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function Y(e,t){const n=X(e);if(n.kind===D.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 V(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const J="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",K=[R.claimed,R.running,R.retry],z=K;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(!F(e.mode)||!H(e.status))throw new A(g.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 e===N.active||e===N.paused}function te(e){if(!ee(e.status))throw new A(g.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);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),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 A(g.invalid,`maxRunMs 必须是 ${String(o.minMaxRunMs)}..${String(o.maxRunCeilingMs)} 的整数毫秒`);return e}}function ie(e){const t=e.name.trim();if(0===t.length||t.length>o.maxNameChars)throw new A(g.invalid,`name 必须为 1..${String(o.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>o.maxPromptChars)throw new A(g.invalid,`prompt 必须为 1..${String(o.maxPromptChars)} 个字符`);if(!i(e.cwd))throw new A(g.invalid,"cwd 必须是绝对路径");const r=h.safeParse(e.trigger);if(!r.success)throw new A(g.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:r.data,maxRunMs:ne(e.maxRunMs)}}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 c=d(i);if(!0===s.requireExistingDatabase&&!t(c))throw new Error("authoritative scheduler database does not exist");t(c)||n(c,{recursive:!0,mode:448}),"win32"!==process.platform&&e(c,448);const u=r(c,"schedules.db");if(!0===s.requireExistingDatabase&&!t(u))throw new Error("authoritative scheduler database does not exist");if(this.db=new a(u),!0===s.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(M)}`),b(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{e(u,384)}catch(e){throw this.db.close(),e}this.init()}init(){this.db.exec(`PRAGMA busy_timeout = ${String(M)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>6)throw C(e.user_version);this.transaction(()=>{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')),\n authority_digest TEXT,\n max_run_ms INTEGER,\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.user_version<6&&this.addMissingColumns(),this.db.exec(`PRAGMA user_version = ${String(6)};`)})}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!==c.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:c.interval,everyMs:o.maxIntervalMs}),N.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=ie(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t))}createScheduleIdempotent(e,t=Date.now()){const n=ie(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})`).get(N.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??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)}}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 A(g.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);const i=s(),r=!0===t.fireImmediately?n:u(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)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),N.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n),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(()=>1===this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(N.active,t,n,e).changes)}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?te(t):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(te)}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===N.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,v.scheduled,R.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(R.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(R.claimed,R.running).map(Z)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${J}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...K).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,R.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,R.running).map(e=>{if(null===e.ownership_token)throw new A(g.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new A(g.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 ${J} LIMIT 1`).get(e,...K);if(void 0!==t)throw new A(g.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,R.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new A(g.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 ${J})`).run(R.claimed,t,i,n+this.claimLeaseMs,e,R.pending,e,e,...K).changes?this.loadClaim(e,i):void 0})}findLiveRun(e){const t=this.db.prepare(`SELECT * FROM invocations WHERE schedule_id = ? AND ${J}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...K);return t?Z(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,R.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&&X(t.tree_tracked_pgids).kind===D.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!==R.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&&H(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||!H(e.status))return E.notFound;if(!T.includes(e.status))return E.terminal;const a=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==a)return a;const d=!0===i.abandon;return!d&&this.treeBlocksTerminal(e)?E.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:d})?E.cancelled:E.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===R.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(N.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return te(e)}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 ${J} LIMIT 1`).get(e,t,...K)}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(R.pending,R.retry,e.nowMs,R.claimed,R.running,e.nowMs),r=[],a=i.filter(e=>e.status===R.running&&!t.has(e.id)&&void 0!==Q(e));a.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const d=new Map;let o=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of a){const n=Q(t);void 0!==n&&(o<=0?d.set(t.id,O.deferred):(o-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),d.set(t.id,this.executorLiveness(n)===_.dead?O.dead:O.notDead)))}for(const n of i){const i=n.status===R.claimed||n.status===R.running,s=d.get(n.id);!i||!t.has(n.id)&&s!==O.notDead?s!==O.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(X(t.tree_tracked_pgids).kind===D.invalid){this.deferLiveRowInTransaction(t,e.nowMs);continue}const i=this.loadScheduleForClaimInTransaction(t.schedule_id,e.nowMs);if(void 0===i){t.status!==R.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===v.scheduled&&i.status===N.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===R.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 a=s();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(R.claimed,e.workerId,a,e.nowMs+this.claimLeaseMs,r,t.id);const d=this.loadClaim(t.id,a);void 0!==d&&n.push(d)}if(n.length>=e.limit)return n;const c=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 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(N.active,e.nowMs,R.pending,R.claimed,R.running,R.retry,this.maxSchedules);for(const t of c){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=s(),a=s(),d=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,R.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,a,e.nowMs+this.claimLeaseMs,e.nowMs);if(this.db.prepare("UPDATE schedules SET next_run_at = ?, updated_at = ? WHERE id = ?").run(u(i.trigger,e.nowMs),e.nowMs,t.id),1===d.changes){const e=this.loadClaim(r,a);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(R.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,R.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,R.claimed,R.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!==R.claimed&&t.status!==R.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,R.claimed,R.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),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!==R.completed&&t.status!==R.needsConfirmation)return m.lostClaim;if(this.treeBlocksTerminal(t))return m.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,R.completed,R.needsConfirmation).changes?m.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===v.scheduled?m.terminalPaused:m.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(R.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,R.completed,R.needsConfirmation).changes?m.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),m.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?q(t):null,i=V(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,R.claimed,R.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,R.claimed,R.running,R.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!==R.claimed&&s.status!==R.running?m.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?m.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===v.scheduled?m.terminalPaused:m.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(R.retry,i+this.retryBackoffMs,n,e),m.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,R.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 A(g.invalid,`invocation ${t.invocationId} attempt ${String(t.expectedAttempt)} 已关联其他会话`);return x(i)}const r=this.getSchedule(e.schedule_id);if(void 0!==r){if(!F(e.mode))throw new A(g.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(...S,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(...S,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 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(N.active,R.retry,R.claimed,R.running,R.pending).wake??void 0}treeBlocksTerminal(e){const t=X(e.tree_tracked_pgids);return t.kind===D.invalid||(1===e.tree_unsettled||0!==t.groups.length)&&(void 0===this.treeLiveness||this.treeLiveness(Z(e))!==L.settled)}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(R.failed,t,n,e),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===v.scheduled?this.db.prepare("UPDATE schedules SET status = ?, last_error = ?, updated_at = ? WHERE id = ?").run(N.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 A(g.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new A(g.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(d(e),"schedules.db");if(!t(n))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=new a(n,{readOnly:!0});try{i.exec(`PRAGMA busy_timeout = ${String(M)}`);const e=i.prepare("PRAGMA user_version").get();if(6!==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(te)}}finally{i.close()}}function re(e,n){const i=r(d(e),"schedules.db");if(!t(i))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty};const s=new a(i,{readOnly:!0});try{s.exec(`PRAGMA busy_timeout = ${String(M)}; BEGIN;`);const e=s.prepare("PRAGMA user_version").get();return 5!==e.user_version&&6!==e.user_version?{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version}:{status:SCHEDULE_LEDGER_READ_STATUSES.ok,value:n(s,6===e.user_version)}}finally{s.close()}}function se(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(x):[],s=void 0===i?void 0:Z(i),a=s??r[0];return void 0===a?void 0:{invocationId:t,scheduleId:a.scheduleId,scheduledForMs:a.scheduledForMs,mode:a.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 de(e,t){const n=e.prepare("SELECT * FROM schedules").all().map(te),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),a=void 0===r?void 0:se(e,r.id,t),d=i.get(n),o=a?.references.at(-1),c=d??o;void 0!==c&&s.push({scheduleId:n,name:c.name,cwd:c.cwd,schedule:d,latestRun:a})}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=re(e,(e,r)=>{const s=void 0===t.scheduleId?"":"WHERE schedule_id = ?",a=e.prepare(`SELECT id FROM (${ae(r)}) ${s}\n ORDER BY scheduled_for DESC, id DESC LIMIT ? OFFSET ?`),d=void 0===t.scheduleId?[n+1,i]:[t.scheduleId,n+1,i],o=a.all(...d),c=[];for(const{id:t}of o.slice(0,n)){const n=se(e,t,r);void 0!==n&&c.push(n)}return{tasks:de(e,r),runs:c,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=re(e,(e,n)=>se(e,t,n));return{status:n.status,...void 0===n.schemaVersion?{}:{schemaVersion:n.schemaVersion},run:n.value}}
|
|
@@ -138,6 +138,44 @@ export interface InvocationRecord {
|
|
|
138
138
|
readonly startedAtMs: number | undefined;
|
|
139
139
|
readonly finishedAtMs: number | undefined;
|
|
140
140
|
}
|
|
141
|
+
/** Durable provenance; intentionally survives invocation retention and schedule removal. */
|
|
142
|
+
export interface ScheduleThreadReference {
|
|
143
|
+
readonly invocationId: string;
|
|
144
|
+
readonly attempt: number;
|
|
145
|
+
readonly scheduleId: string;
|
|
146
|
+
readonly threadId: string;
|
|
147
|
+
readonly threadsDir: string;
|
|
148
|
+
readonly name: string;
|
|
149
|
+
readonly cwd: string;
|
|
150
|
+
readonly scheduledForMs: number;
|
|
151
|
+
readonly mode: InvocationMode;
|
|
152
|
+
readonly createdAtMs: number;
|
|
153
|
+
}
|
|
154
|
+
export interface BackfillThreadReferenceInput {
|
|
155
|
+
readonly invocationId: string;
|
|
156
|
+
readonly expectedAttempt: number;
|
|
157
|
+
readonly threadId: string;
|
|
158
|
+
readonly threadsDir: string;
|
|
159
|
+
}
|
|
160
|
+
export interface RegisterThreadReferenceInput extends BackfillThreadReferenceInput {
|
|
161
|
+
readonly ownershipToken: string;
|
|
162
|
+
}
|
|
163
|
+
export interface ScheduleRunHistoryEntry {
|
|
164
|
+
readonly invocationId: string;
|
|
165
|
+
readonly scheduleId: string;
|
|
166
|
+
readonly scheduledForMs: number;
|
|
167
|
+
readonly mode: InvocationMode;
|
|
168
|
+
/** Absent after ledger retention/removal; never infer a terminal status from a thread. */
|
|
169
|
+
readonly invocation: InvocationRecord | undefined;
|
|
170
|
+
readonly references: readonly ScheduleThreadReference[];
|
|
171
|
+
}
|
|
172
|
+
export interface ScheduleHistoryTask {
|
|
173
|
+
readonly scheduleId: string;
|
|
174
|
+
readonly name: string;
|
|
175
|
+
readonly cwd: string;
|
|
176
|
+
readonly schedule: ScheduleRecord | undefined;
|
|
177
|
+
readonly latestRun: ScheduleRunHistoryEntry | undefined;
|
|
178
|
+
}
|
|
141
179
|
export type InvocationTreeLivenessProbe = (record: InvocationRecord) => InvocationTreeLiveness;
|
|
142
180
|
export interface ClaimedInvocation {
|
|
143
181
|
readonly invocation: InvocationRecord;
|
|
@@ -125,14 +125,14 @@ export declare const toolExecutionsParamsSchema: z.ZodObject<{
|
|
|
125
125
|
}, "strip", z.ZodTypeAny, {
|
|
126
126
|
sessionId: string;
|
|
127
127
|
includeRaw: boolean;
|
|
128
|
-
toolCallId?: string | undefined;
|
|
129
128
|
limit?: number | undefined;
|
|
129
|
+
toolCallId?: string | undefined;
|
|
130
130
|
executionId?: string | undefined;
|
|
131
131
|
afterSequence?: number | undefined;
|
|
132
132
|
}, {
|
|
133
133
|
sessionId: string;
|
|
134
|
-
toolCallId?: string | undefined;
|
|
135
134
|
limit?: number | undefined;
|
|
135
|
+
toolCallId?: string | undefined;
|
|
136
136
|
executionId?: string | undefined;
|
|
137
137
|
afterSequence?: number | undefined;
|
|
138
138
|
includeRaw?: boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash as e,randomUUID as t}from"node:crypto";import{APPROVAL_DIFF_PREVIEW_KEY as r,APPROVAL_EXPLANATION_PREVIEW_KEY as s,RUNTIME_ERROR_CODES as n,RUNTIME_FEATURES_V13 as i,RUNTIME_METHODS as a,RUNTIME_PROTOCOL_VERSION as o,RUNTIME_V14_MAX_ATTACHMENT_BYTES as d,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as u,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as h,RUNTIME_V14_MAX_TURN_ATTACHMENTS as c,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as l,approvalExplanationSchema as p,approvalIdSchema as m,normalizeUserInputResultForForm as I,operationIdSchema as v,parseLatestRuntimeMethodResult as g,runtimeDurableEventV13Schema as f,runtimeEphemeralEventV13Schema as y,runtimeInstanceIdSchema as T,runtimeProtocolVersionSchema as A,streamIdSchema as w,threadIdSchema as q,userInputFormSchema as S}from"@roll-agent/protocol";import{AttachmentStore as R}from"./attachment-store.js";import{createSafeCapabilitySnapshot as x}from"../engine/capability-manifest.js";import{isSensitiveFieldName as E,redactSecretText as b,toRedactedToolExecutionRecordSummary as O}from"../tool-bridge/tool-execution-record.js";import{RuntimeEventCursorExpiredError as C,RuntimeEventCursorGapError as P,ThreadStore as U}from"../store/thread-store.js";const N=4194304,F=500,k=1e4,B=16e3,j=64,M=128,V=16,$=new Set(["_meta","authorization","cookie","password","provideroptions","raw","secret","token"]);export class RuntimeServiceError extends Error{rollCode;retryable;details;constructor(e,t,r={}){super(t),this.name="RuntimeServiceError",this.rollCode=e,this.retryable=r.retryable??!1,this.details=r.details}}function L(t){const r=JSON.stringify(t);if(void 0===r)throw new RuntimeServiceError(n.invalidParams,"无法为 mutation 请求生成稳定指纹");return e("sha256").update(r).digest("hex")}export class MutationRequestCache{maxEntries;inFlight=new Map;settled=new Map;constructor(e){if(!Number.isInteger(e)||e<1)throw new Error("idempotencyCacheEntries must be a positive integer");this.maxEntries=e}run(e,t,r,s){const n=L(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>g(t,e));const a=this.settled.get(e);if(void 0!==a)return this.assertMatchingRequest(e,t,n,a),this.settled.delete(e),this.settled.set(e,a),a.response.then(e=>g(t,e));const o=Promise.resolve().then(s),d={method:t,fingerprint:n,response:o};return this.inFlight.set(e,d),o.then(()=>{this.settle(e,d)},()=>{this.settle(e,d)}),o}clear(){this.inFlight.clear(),this.settled.clear()}assertMatchingRequest(e,t,r,s){if(s.method!==t||s.fingerprint!==r)throw new RuntimeServiceError(n.invalidParams,`requestId "${e}" 已用于不同请求`)}settle(e,t){if(this.inFlight.get(e)===t)for(this.inFlight.delete(e),this.settled.set(e,t);this.settled.size>this.maxEntries;){const e=this.settled.keys().next().value;if(void 0===e)return;this.settled.delete(e)}}}function z(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function D(e,t,r=0){if(void 0!==t&&($.has(t.toLowerCase())||E(t)))return"[redacted]";if(r>=V)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=b(e);return t.length<=B?t:`${t.slice(0,B)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,j).map(e=>D(e,void 0,r+1));return e.length>j&&t.push(`[${String(e.length-j)} items omitted]`),t}if(z(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,M))void 0!==n&&(t[s]=D(n,s,r+1));return Object.keys(e).length>M&&(t._omitted=`${String(Object.keys(e).length-M)} keys omitted`),t}return void 0===e?null:String(e)}function W(e){return e instanceof Error?e.message:String(e)}function _(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>z(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function G(e){const t=e.endsWith("==")?2:e.endsWith("=")?1:0;return Math.max(0,3*Math.floor(e.length/4)-t)}function J(e){const t=e.content;return"user"===e.role&&Array.isArray(t)?t.flatMap(e=>z(e)?"file"===e.type&&"string"==typeof e.mediaType&&"string"==typeof e.data?[{type:"attachment",mediaType:e.mediaType,bytes:G(e.data)}]:"image"===e.type&&"string"==typeof e.image?[{type:"attachment",mediaType:"image/*",bytes:G(e.image)}]:[]:[]):[]}function H(e){return q.parse(e)}function K(e,t){return{id:H(t.id),...void 0!==t.title?{title:t.title}:{},...void 0!==t.model?{model:t.model}:{},createdAt:t.createdAt,updatedAt:t.updatedAt,messageCount:e.countTranscriptMessages(t.id)}}function Q(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=b(_(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:[...t.length>0?[{type:"text",text:t}]:[],...J(e.message)]}}function X(e){const t=O(e);return{id:v.parse(t.id),sequence:e.sequence,toolCallId:t.toolCallId,agentName:t.agentName,toolName:t.toolName,createdAt:t.createdAt,outcome:{kind:t.outcome.kind,..."reason"in t.outcome&&void 0!==t.outcome.reason?{reason:t.outcome.reason}:{}},display:D(t.display,void 0)}}function Y(e,t){const n=D(t.input,void 0),i=void 0===t.explanation?void 0:p.safeParse(b(t.explanation)),a=void 0===t.diff?void 0:D(t.diff,void 0),o=z(n)?{...n,...!0===i?.success?{[s]:i.data}:{},...void 0!==a?{[r]:a}:{}}:n;return{id:m.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:o,...void 0!==t.reason?{reason:b(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;attachmentStore;sessions=new Map;activeTurns=new Map;activeTurnOwners=new Map;settledTurnOwners=new Map;pendingApprovals=new Map;pendingUserInputs=new Map;listeners=new Set;fatalErrorListeners=new Set;userInputListeners=new Set;mutationRequests;sequence=0;closing=!1;userInputAvailable=!1;constructor(e,r,s={}){this.engine=e,this.store=r,this.serverName=s.serverName??"roll-runtime",this.serverVersion=s.serverVersion??"1.0",this.runtimeVersion=s.runtimeVersion??"development",this.maxFrameBytes=s.maxFrameBytes??N,this.idempotencyCacheEntries=s.idempotencyCacheEntries??k,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.attachmentStore=s.attachmentStore,this.runtimeInstanceId=T.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=A.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(n.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:l}});const r=i.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),s={protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:r,limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:F,idempotencyCacheEntries:this.idempotencyCacheEntries}};if("1.4"===t){const e=this.attachmentStore?.limits;return{...s,protocolVersion:t,features:void 0!==this.attachmentStore?[...r,"attachments"]:[...r],limits:{...s.limits,eventReplay:!0,maxAttachmentBytes:e?.maxAttachmentBytes??d,maxAttachmentChunkBytes:e?.maxAttachmentChunkBytes??u,maxTurnAttachments:c,maxStagedAttachments:e?.maxStagedAttachments??h}}}return"1.3"===t?{...s,protocolVersion:t,limits:{...s.limits,eventReplay:!0}}:{...s,protocolVersion:t,limits:{...s.limits,eventReplay:!1}}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onFatalError(e){return this.fatalErrorListeners.add(e),()=>{this.fatalErrorListeners.delete(e)}}onUserInputInteraction(e){return this.userInputListeners.add(e),()=>{this.userInputListeners.delete(e)}}setUserInputAvailable(e){if(this.userInputAvailable!==e){this.userInputAvailable=e,e||this.cancelAllPendingUserInputs("当前客户端已撤销用户输入处理能力");for(const t of this.sessions.values())t.setUserInputAvailable?.(e)}}listThreads(e){this.assertOpen();const t=void 0===e.cursor?0:Number(e.cursor);if(!Number.isSafeInteger(t)||t<0)throw new RuntimeServiceError(n.invalidParams,"thread.list cursor 超出安全整数范围");const r=this.store.listThreads(),s=r.slice(t,t+e.limit).map(e=>K(this.store,e)),i=t+s.length;return{items:s,nextCursor:i<r.length?String(i):null}}async createThread(e){return this.mutationRequests.run(e.requestId,a.threadCreate,e,async()=>{this.assertOpen();const t=await this.engine.createSession(void 0!==e.title?{title:e.title}:{});return this.rememberSession(t),{thread:K(this.store,this.requireThread(t.id))}})}async openThread(e){return await this.requireSession(e.threadId),this.snapshotThread({threadId:e.threadId,limit:100})}snapshotThread(e){this.assertOpen();const t=this.requireThread(e.threadId),r=this.store.listRecentTranscriptMessages(e.threadId,{...void 0!==e.messageBeforeSequence?{beforeSequence:e.messageBeforeSequence}:{},limit:e.limit}),s=this.store.listRecentToolExecutions(e.threadId,{...void 0!==e.operationBeforeSequence?{beforeSequence:e.operationBeforeSequence}:{},limit:e.limit}),n=this.activeTurns.get(e.threadId),i=void 0===n?void 0:{id:n.turnId,status:n.status,startedAt:n.startedAt};return{thread:K(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=Q(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map(X),nextBeforeSequence:s.nextBeforeSequence??null},...void 0!==i?{activeTurn:i}:{},pendingApprovals:[...this.pendingApprovals.values()].filter(t=>t.threadId===e.threadId).map(e=>e.approval),transcriptCompleteness:this.store.getTranscriptCompleteness(e.threadId),eventCursor:this.store.getRuntimeEventCursor(e.threadId)}}resumeEvents(e){this.assertOpen(),this.requireThread(e.threadId);try{return this.store.resumeRuntimeEvents(e.threadId,e.afterCursor)}catch(t){if(t instanceof C)throw new RuntimeServiceError(n.eventCursorExpired,"Runtime Event cursor 已超出当前 Thread 的保留窗口,请回退到 thread.snapshot",{details:{threadId:e.threadId}});if(t instanceof P)throw new RuntimeServiceError(n.eventCursorGap,"Runtime Event cursor 与当前 Thread 事件日志不连续,请回退到 thread.snapshot",{details:{threadId:e.threadId}});throw t}}async renameThread(e){return this.mutationRequests.run(e.requestId,a.threadRename,e,()=>(this.assertOpen(),this.requireThread(e.threadId),this.store.updateTitle(e.threadId,e.title),{thread:K(this.store,this.requireThread(e.threadId))}))}async deleteThread(e){return this.mutationRequests.run(e.requestId,a.threadDelete,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.threadBusy,`Thread "${e.threadId}" 仍有活动 Turn`,{retryable:!0});const t=this.sessions.get(e.threadId);return void 0!==t&&(await t.close(),this.sessions.delete(e.threadId)),this.attachmentStore?.releaseThread(e.threadId),this.store.deleteThread(e.threadId),{deleted:!0}})}async detachThread(e){return this.mutationRequests.run(e.requestId,a.threadDetach,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))return{detached:!1};const t=this.sessions.get(e.threadId);return void 0===t?{detached:!1}:(await t.close(),this.sessions.delete(e.threadId),{detached:!0})})}async threadCapabilities(e){const t=await this.requireSession(e.threadId);return{manifest:D(x(t.getCapabilityManifest(),t.getCapabilityTurnContext()),void 0)}}async startTurn(e){return this.mutationRequests.run(e.requestId,a.turnStart,e,async()=>{this.assertOpen();const t=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==t){if(t===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const r=await this.requireSession(e.threadId),s=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==s){if(s===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const i=this.resolveTurnAttachments(e),a={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};this.activeTurns.set(e.threadId,a),this.activeTurnOwners.set(e.turnId,e.threadId);try{this.emit(e.threadId,e.turnId,{type:"turn.started"})}catch(t){throw this.activeTurns.delete(e.threadId),this.activeTurnOwners.delete(e.turnId),t}return this.driveTurn(a,e.input.text,i).catch(()=>{}),{accepted:!0,turnId:e.turnId}})}async cancelTurn(e){return this.mutationRequests.run(e.requestId,a.turnCancel,e,()=>{this.assertOpen();const t=this.activeTurns.get(e.threadId);if(void 0===t||t.turnId!==e.turnId)throw new RuntimeServiceError(n.turnNotFound,`Turn "${e.turnId}" 不存在或已结束`);t.status="cancelling";let r=!1;try{this.cancelPendingUserInputsForTurn(e.turnId,"Turn 已由客户端取消"),this.cancelPendingApprovalsForTurn(e.turnId,{status:"cancelled",reason:"Turn 已由客户端取消"})}finally{r=t.session.cancel()}return{cancelling:r}})}async respondApproval(e){return this.mutationRequests.run(e.requestId,a.approvalRespond,e,()=>this.resolvePendingApproval(e,"approve"===e.decision?{decision:"approve"}:{decision:"reject",...void 0!==e.reason?{reason:e.reason}:{}}))}resolvePendingApproval(e,t){this.assertOpen();const r=this.requirePendingApproval(e),s="approve"===t.decision?r.session.approve(e.approvalId,t.scope):r.session.reject(e.approvalId,t.reason);if(this.pendingApprovals.delete(e.approvalId),!s)throw this.emitApprovalResolved(r,{status:"cancelled",reason:"审批已失效"}),new RuntimeServiceError(n.approvalNotFound,`Approval "${e.approvalId}" 已失效`);return this.emitApprovalResolved(r,"approve"===t.decision?{status:"resolved",decision:"approve"}:{status:"resolved",decision:"reject",...void 0!==t.reason?{reason:t.reason}:{}}),{resolved:!0}}getPendingApprovalExpiresAt(e){return this.findPendingApproval(e)?.expiresAt}async stageAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentStage,e,()=>{this.assertOpen(),this.requireThread(e.threadId);const t=this.requireAttachmentStore().stage({threadId:e.threadId,fileName:e.fileName,mediaType:e.mediaType,bytes:e.bytes,sha256:e.sha256,source:e.source,sourcePath:e.sourcePath});return t.ok||this.throwAttachmentFailure(t),{attachmentId:t.attachmentId,state:t.state,...void 0!==t.descriptor?{descriptor:t.descriptor}:{}}})}async appendAttachmentChunk(e){return this.mutationRequests.run(e.requestId,a.attachmentChunk,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().appendChunk({threadId:e.threadId,attachmentId:e.attachmentId,sequence:e.sequence,dataBase64:e.dataBase64});return t.ok||this.throwAttachmentFailure(t),{receivedBytes:t.receivedBytes,nextSequence:t.nextSequence}})}async commitAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentCommit,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().commit({threadId:e.threadId,attachmentId:e.attachmentId});return t.ok||this.throwAttachmentFailure(t),{descriptor:t.descriptor}})}async releaseAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentRelease,e,()=>{this.assertOpen();return{released:this.requireAttachmentStore().release({threadId:e.threadId,attachmentId:e.attachmentId}).released}})}requireAttachmentStore(){if(void 0===this.attachmentStore)throw new RuntimeServiceError(n.capabilityUnavailable,"Runtime 未配置附件存储,attachments 能力不可用");return this.attachmentStore}throwAttachmentFailure(e){throw new RuntimeServiceError(e.code,e.message,{...void 0!==e.retryable?{retryable:e.retryable}:{}})}resolveTurnAttachments(e){const t=e.input.attachments;if(void 0===t||0===t.length)return;const r=this.requireAttachmentStore(),s=new Set,i=[];for(const a of t){if(s.has(a))throw new RuntimeServiceError(n.invalidParams,`附件 "${a}" 在同一 Turn 中重复引用`);s.add(a);const t=r.readCommitted({threadId:e.threadId,attachmentId:a});t.ok||this.throwAttachmentFailure(t),i.push({data:t.dataBase64,mediaType:t.descriptor.mediaType})}return i}cancelPendingApproval(e,t){const r=this.findPendingApproval(e);return void 0!==r&&(this.pendingApprovals.delete(e.approvalId),this.emitApprovalResolved(r,t),!0)}async failPendingApprovalInteraction(e,t,r={status:"cancelled",reason:t}){const s=this.findPendingApproval(e);if(void 0===s)return!1;const n=this.activeTurns.get(e.threadId);void 0!==n&&n.turnId===e.turnId&&(n.interactionFailure=t),this.pendingApprovals.delete(e.approvalId);try{this.emitApprovalResolved(s,r)}finally{s.session.cancel()}return!0}resolvePendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;let s;try{s=I(r.form,t)}catch{return this.cancelPendingUserInput(e,"用户输入不符合原始表单约束"),!1}this.pendingUserInputs.delete(e);const n=r.session.resolveUserInput?.(e,s)??!1;if(!n){const t="用户输入请求已失效";return r.session.cancelUserInput?.(e,t),this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),!1}return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:"submitted"===s.status?"用户已提交输入":"用户已取消输入"}),n}cancelPendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;this.pendingUserInputs.delete(e);const s=r.session.cancelUserInput?.(e,t)??!1;return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),s}getOperation(e){this.assertOpen(),this.requireThread(e.threadId);const t=this.store.getToolExecution(e.threadId,e.operationId);return{operation:void 0===t?null:X(t)}}async close(){if(this.closing)return;this.closing=!0,this.setUserInputAvailable(!1);for(const e of this.activeTurns.values())e.session.cancel();const e=[...this.sessions.values()];this.sessions.clear(),this.activeTurns.clear(),this.activeTurnOwners.clear(),this.settledTurnOwners.clear(),this.pendingApprovals.clear(),this.pendingUserInputs.clear(),this.userInputListeners.clear(),this.attachmentStore?.close(),await Promise.allSettled(e.map(e=>e.close())),this.mutationRequests.clear(),this.fatalErrorListeners.clear()}assertOpen(){if(this.closing)throw new RuntimeServiceError(n.runtimeClosing,"Runtime 正在关闭",{retryable:!0})}requireThread(e){const t=this.store.getThread(e);if(void 0===t)throw new RuntimeServiceError(n.threadNotFound,`Thread "${e}" 不存在`);return t}async requireSession(e){this.assertOpen(),this.requireThread(e);const t=this.sessions.get(e);if(void 0!==t)return t;const r=await this.engine.resumeSession(e);return this.rememberSession(r),r}rememberSession(e){e.setUserInputAvailable?.(this.userInputAvailable),this.sessions.set(e.id,e)}emit(e,t,r){const s=(new Date).toISOString(),n={protocolVersion:o,runtimeInstanceId:this.runtimeInstanceId,sequence:this.sequence,timestamp:s,threadId:e,...void 0!==t?{turnId:t}:{}},i=f.safeParse(r),a=i.success?(()=>{let r;try{r=this.store.appendRuntimeEvent({threadId:e,...void 0===t?{}:{turnId:t},timestamp:s,event:i.data})}catch(e){throw this.notifyFatalError(e),e}return{...n,durability:"durable",eventId:r.eventId,cursor:r.cursor,event:i.data}})():{...n,durability:"ephemeral",event:y.parse(r)};this.sequence+=1;for(const e of this.listeners)try{e(a)}catch{}}notifyFatalError(e){for(const e of this.activeTurns.values())try{e.session.cancel()}catch{}for(const t of this.fatalErrorListeners)try{t(e)}catch{}}async driveTurn(e,t,r){const s={streamId:void 0,text:"",terminalEvent:void 0},n=void 0!==r&&r.length>0?{text:t,attachments:r}:t;let i;try{for await(const t of e.session.send(n))this.projectSessionEvent(e,s,t);i=s.terminalEvent??{type:"turn.completed"}}catch(e){i=s.terminalEvent??{type:"turn.failed",stage:"execute",message:b(W(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,i)}findTurnOwner(e,t){const r=this.activeTurnOwners.get(e);if(void 0!==r)return r;const s=this.settledTurnOwners.get(e);return s===t&&(this.settledTurnOwners.delete(e),this.settledTurnOwners.set(e,s)),s}settleTurnOwner(e){if(this.activeTurnOwners.get(e.turnId)===e.threadId&&(this.activeTurnOwners.delete(e.turnId),!this.closing))for(this.settledTurnOwners.set(e.turnId,e.threadId);this.settledTurnOwners.size>this.idempotencyCacheEntries;){const e=this.settledTurnOwners.keys().next().value;if(void 0===e)return;this.settledTurnOwners.delete(e)}}projectSessionEvent(e,r,s){switch(s.type){case"message-start":{const s=w.parse(t());return r.streamId=s,r.text="",void this.emit(e.threadId,e.turnId,{type:"message.started",streamId:s})}case"text-delta":{const n=r.streamId??w.parse(t());return void 0===r.streamId&&(r.streamId=n,this.emit(e.threadId,e.turnId,{type:"message.started",streamId:n})),r.text+=s.delta,void this.emit(e.threadId,e.turnId,{type:"message.delta",streamId:n,delta:s.delta})}case"message-finish":{const n=r.streamId??w.parse(t());return void this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:b(s.text||r.text)})}case"reasoning-delta":{const t=this.reasoningSummaryProjector?.(s.delta);return void(void 0!==t&&t.length>0&&this.emit(e.threadId,e.turnId,{type:"reasoning.summary.delta",reasoningId:s.reasoningId,delta:b(t)}))}case"tool-call":return void this.emit(e.threadId,e.turnId,{type:"tool.started",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,input:D(s.input,void 0)});case"tool-output-delta":return void this.emit(e.threadId,e.turnId,{type:"tool.output",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,stream:s.stream,delta:b(s.delta)});case"tool-result":return void this.emit(e.threadId,e.turnId,{type:"tool.completed",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,...void 0!==s.executionId?{operationId:v.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:D(s.display,void 0)});case"confirmation-required":{const t=Y(e.turnId,s);return this.pendingApprovals.set(t.id,{threadId:e.threadId,session:e.session,approval:t,expiresAt:s.expiresAt}),void this.emit(e.threadId,e.turnId,{type:"approval.required",approval:t})}case"user-input-required":{if(!this.userInputAvailable||void 0===e.session.resolveUserInput||void 0===e.session.cancelUserInput)return void e.session.cancelUserInput?.(s.requestId,"当前客户端未协商用户输入处理能力");const t=S.parse(s.form),r=S.parse(t),n={requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,session:e.session,form:t,expiresAt:s.expiresAt};this.pendingUserInputs.set(s.requestId,n),e.status="waiting-for-user";return void(this.emitUserInputInteraction({type:"required",requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,form:r,expiresAt:s.expiresAt})||this.cancelPendingUserInput(s.requestId,"当前没有可处理用户输入请求的客户端"))}case"user-input-settled":{const e=this.pendingUserInputs.get(s.requestId);if(void 0===e)return;return this.pendingUserInputs.delete(s.requestId),this.restoreRunningStatus(e),void this.emitUserInputInteraction({type:"settled",requestId:e.requestId,threadId:e.threadId,turnId:e.turnId,reason:"submitted"===s.status?"用户输入请求已结算":"用户输入请求已取消"})}case"turn-cancelled":r.terminalEvent??=void 0===e.interactionFailure?{type:"turn.cancelled",reason:s.reason,message:b(s.message)}:{type:"turn.failed",stage:"execute",message:b(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:b(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:b(e.reason)}:{}}}findPendingApproval(e){const t=this.pendingApprovals.get(e.approvalId);if(void 0!==t&&t.threadId===e.threadId&&t.approval.turnId===e.turnId)return t}requirePendingApproval(e){const t=this.findPendingApproval(e);if(void 0===t)throw new RuntimeServiceError(n.approvalNotFound,`Approval "${e.approvalId}" 不存在或已失效`);return t}emitApprovalResolved(e,t){const r="reason"in t&&void 0!==t.reason?{...t,reason:b(t.reason)}:t;this.emit(e.threadId,e.approval.turnId,{type:"approval.resolved",approvalId:e.approval.id,resolution:r})}cancelPendingApprovalsForTurn(e,t){const r=[...this.pendingApprovals.values()].filter(t=>t.approval.turnId===e);let s;for(const e of r)try{this.cancelPendingApproval({threadId:e.threadId,turnId:e.approval.turnId,approvalId:e.approval.id},t)}catch(e){s??=e}if(void 0!==s)throw s}cancelPendingUserInputsForTurn(e,t){const r=[...this.pendingUserInputs.values()].filter(t=>t.turnId===e).map(e=>e.requestId);for(const e of r)this.cancelPendingUserInput(e,t)}cancelAllPendingUserInputs(e){for(const t of[...this.pendingUserInputs.keys()])this.cancelPendingUserInput(t,e)}restoreRunningStatus(e){const t=this.activeTurns.get(e.threadId);void 0!==t&&t.turnId===e.turnId&&"waiting-for-user"===t.status&&(t.status="running")}emitUserInputInteraction(e){let t=!1;for(const r of this.userInputListeners)try{r(e),t=!0}catch{}return t}emitTurnTerminal(e,t){this.cancelPendingUserInputsForTurn(e.turnId,`Turn 已终止:${t.type}`),this.cancelPendingApprovalsForTurn(e.turnId,"turn.cancelled"===t.type&&"timeout"===t.reason?{status:"expired",reason:"Turn 已超时"}:{status:"cancelled",reason:`Turn 已终止:${t.type}`}),this.emit(e.threadId,e.turnId,t)}}
|
|
1
|
+
import{createHash as e,randomUUID as t}from"node:crypto";import{APPROVAL_DIFF_PREVIEW_KEY as r,APPROVAL_EXPLANATION_PREVIEW_KEY as s,RUNTIME_ERROR_CODES as n,RUNTIME_FEATURES_V13 as i,RUNTIME_METHODS as a,RUNTIME_PROTOCOL_VERSION as o,RUNTIME_V14_MAX_ATTACHMENT_BYTES as d,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as u,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as h,RUNTIME_V14_MAX_TURN_ATTACHMENTS as c,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as l,approvalExplanationSchema as p,approvalIdSchema as m,normalizeUserInputResultForForm as I,operationIdSchema as v,parseLatestRuntimeMethodResult as g,runtimeDurableEventV13Schema as f,runtimeEphemeralEventV13Schema as y,runtimeInstanceIdSchema as T,runtimeProtocolVersionSchema as w,streamIdSchema as A,threadIdSchema as q,userInputFormSchema as S}from"@roll-agent/protocol";import{AttachmentStore as R}from"./attachment-store.js";import{createSafeCapabilitySnapshot as x}from"../engine/capability-manifest.js";import{isSensitiveFieldName as E,redactSecretText as b,toRedactedToolExecutionRecordSummary as O}from"../tool-bridge/tool-execution-record.js";import{RuntimeEventCursorExpiredError as P,RuntimeEventCursorGapError as C,ThreadStore as U}from"../store/thread-store.js";const N=4194304,F=500,k=1e4,B=16e3,j=64,M=128,V=16,$=new Set(["_meta","authorization","cookie","password","provideroptions","raw","secret","token"]);export class RuntimeServiceError extends Error{rollCode;retryable;details;constructor(e,t,r={}){super(t),this.name="RuntimeServiceError",this.rollCode=e,this.retryable=r.retryable??!1,this.details=r.details}}function L(t){const r=JSON.stringify(t);if(void 0===r)throw new RuntimeServiceError(n.invalidParams,"无法为 mutation 请求生成稳定指纹");return e("sha256").update(r).digest("hex")}export class MutationRequestCache{maxEntries;inFlight=new Map;settled=new Map;constructor(e){if(!Number.isInteger(e)||e<1)throw new Error("idempotencyCacheEntries must be a positive integer");this.maxEntries=e}run(e,t,r,s){const n=L(r),i=this.inFlight.get(e);if(void 0!==i)return this.assertMatchingRequest(e,t,n,i),i.response.then(e=>g(t,e));const a=this.settled.get(e);if(void 0!==a)return this.assertMatchingRequest(e,t,n,a),this.settled.delete(e),this.settled.set(e,a),a.response.then(e=>g(t,e));const o=Promise.resolve().then(s),d={method:t,fingerprint:n,response:o};return this.inFlight.set(e,d),o.then(()=>{this.settle(e,d)},()=>{this.settle(e,d)}),o}clear(){this.inFlight.clear(),this.settled.clear()}assertMatchingRequest(e,t,r,s){if(s.method!==t||s.fingerprint!==r)throw new RuntimeServiceError(n.invalidParams,`requestId "${e}" 已用于不同请求`)}settle(e,t){if(this.inFlight.get(e)===t)for(this.inFlight.delete(e),this.settled.set(e,t);this.settled.size>this.maxEntries;){const e=this.settled.keys().next().value;if(void 0===e)return;this.settled.delete(e)}}}function z(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function D(e,t,r=0){if(void 0!==t&&($.has(t.toLowerCase())||E(t)))return"[redacted]";if(r>=V)return"[nested value omitted]";if(null===e||"boolean"==typeof e)return e;if("string"==typeof e){const t=b(e);return t.length<=B?t:`${t.slice(0,B)}\n[value clipped]`}if("number"==typeof e)return Number.isFinite(e)?e:String(e);if(Array.isArray(e)){const t=e.slice(0,j).map(e=>D(e,void 0,r+1));return e.length>j&&t.push(`[${String(e.length-j)} items omitted]`),t}if(z(e)){const t={};for(const[s,n]of Object.entries(e).slice(0,M))void 0!==n&&(t[s]=D(n,s,r+1));return Object.keys(e).length>M&&(t._omitted=`${String(Object.keys(e).length-M)} keys omitted`),t}return void 0===e?null:String(e)}function W(e){return e instanceof Error?e.message:String(e)}function _(e){const t=e.content;return"string"==typeof t?t:Array.isArray(t)?t.flatMap(e=>z(e)&&"text"===e.type&&"string"==typeof e.text?[e.text]:[]).join(""):""}function G(e){const t=e.endsWith("==")?2:e.endsWith("=")?1:0;return Math.max(0,3*Math.floor(e.length/4)-t)}function J(e){const t=e.content;return"user"===e.role&&Array.isArray(t)?t.flatMap(e=>z(e)?"file"===e.type&&"string"==typeof e.mediaType&&"string"==typeof e.data?[{type:"attachment",mediaType:e.mediaType,bytes:G(e.data)}]:"image"===e.type&&"string"==typeof e.image?[{type:"attachment",mediaType:"image/*",bytes:G(e.image)}]:[]:[]):[]}function H(e){return q.parse(e)}function K(e,t){return{id:H(t.id),...void 0!==t.title?{title:t.title}:{},...void 0!==t.model?{model:t.model}:{},createdAt:t.createdAt,updatedAt:t.updatedAt,messageCount:e.countTranscriptMessages(t.id)}}function Q(e){if("user"!==e.message.role&&"assistant"!==e.message.role)return;const t=b(_(e.message));return{sequence:e.sequence,role:e.message.role,createdAt:e.createdAt,parts:[...t.length>0?[{type:"text",text:t}]:[],...J(e.message)]}}function X(e){const t=O(e);return{id:v.parse(t.id),sequence:e.sequence,toolCallId:t.toolCallId,agentName:t.agentName,toolName:t.toolName,createdAt:t.createdAt,outcome:{kind:t.outcome.kind,..."reason"in t.outcome&&void 0!==t.outcome.reason?{reason:t.outcome.reason}:{}},display:D(t.display,void 0)}}function Y(e,t){const n=D(t.input,void 0),i=void 0===t.explanation?void 0:p.safeParse(b(t.explanation)),a=void 0===t.diff?void 0:D(t.diff,void 0),o=z(n)?{...n,...!0===i?.success?{[s]:i.data}:{},...void 0!==a?{[r]:a}:{}}:n;return{id:m.parse(t.approvalId),turnId:e,agentName:t.agentName,toolName:t.toolName,preview:o,...void 0!==t.reason?{reason:b(t.reason)}:{}}}export class RuntimeService{runtimeInstanceId;engine;store;serverName;serverVersion;runtimeVersion;maxFrameBytes;idempotencyCacheEntries;reasoningSummaryProjector;attachmentStore;sessions=new Map;activeTurns=new Map;activeTurnOwners=new Map;settledTurnOwners=new Map;pendingApprovals=new Map;pendingUserInputs=new Map;listeners=new Set;fatalErrorListeners=new Set;userInputListeners=new Set;mutationRequests;sequence=0;closing=!1;userInputAvailable=!1;constructor(e,r,s={}){this.engine=e,this.store=r,this.serverName=s.serverName??"roll-runtime",this.serverVersion=s.serverVersion??"1.0",this.runtimeVersion=s.runtimeVersion??"development",this.maxFrameBytes=s.maxFrameBytes??N,this.idempotencyCacheEntries=s.idempotencyCacheEntries??k,this.mutationRequests=new MutationRequestCache(this.idempotencyCacheEntries),this.reasoningSummaryProjector=s.reasoningSummaryProjector,this.attachmentStore=s.attachmentStore,this.runtimeInstanceId=T.parse(t())}initialize(e){let t;this.assertOpen();for(const r of e.protocolVersions){const e=w.safeParse(r);if(e.success){t=e.data;break}}if(void 0===t)throw new RuntimeServiceError(n.protocolVersionUnsupported,`不支持客户端协议版本:${e.protocolVersions.join(", ")}`,{details:{supportedVersions:l}});const r=i.filter(e=>"reasoning-summary"!==e||void 0!==this.reasoningSummaryProjector),s={protocolVersion:t,runtimeInstanceId:this.runtimeInstanceId,server:{name:this.serverName,version:this.serverVersion,runtimeVersion:this.runtimeVersion},features:r,limits:{maxFrameBytes:this.maxFrameBytes,maxPageSize:F,idempotencyCacheEntries:this.idempotencyCacheEntries}};if("1.4"===t){const e=this.attachmentStore?.limits;return{...s,protocolVersion:t,features:void 0!==this.attachmentStore?[...r,"attachments"]:[...r],limits:{...s.limits,eventReplay:!0,maxAttachmentBytes:e?.maxAttachmentBytes??d,maxAttachmentChunkBytes:e?.maxAttachmentChunkBytes??u,maxTurnAttachments:c,maxStagedAttachments:e?.maxStagedAttachments??h}}}return"1.3"===t?{...s,protocolVersion:t,limits:{...s.limits,eventReplay:!0}}:{...s,protocolVersion:t,limits:{...s.limits,eventReplay:!1}}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onFatalError(e){return this.fatalErrorListeners.add(e),()=>{this.fatalErrorListeners.delete(e)}}onUserInputInteraction(e){return this.userInputListeners.add(e),()=>{this.userInputListeners.delete(e)}}setUserInputAvailable(e){if(this.userInputAvailable!==e){this.userInputAvailable=e,e||this.cancelAllPendingUserInputs("当前客户端已撤销用户输入处理能力");for(const t of this.sessions.values())t.setUserInputAvailable?.(e)}}listThreads(e){this.assertOpen();const t=void 0===e.cursor?0:Number(e.cursor);if(!Number.isSafeInteger(t)||t<0)throw new RuntimeServiceError(n.invalidParams,"thread.list cursor 超出安全整数范围");const r=this.store.listThreads({origin:"interactive"}),s=r.slice(t,t+e.limit).map(e=>K(this.store,e)),i=t+s.length;return{items:s,nextCursor:i<r.length?String(i):null}}async createThread(e){return this.mutationRequests.run(e.requestId,a.threadCreate,e,async()=>{this.assertOpen();const t=await this.engine.createSession(void 0!==e.title?{title:e.title}:{});return this.rememberSession(t),{thread:K(this.store,this.requireThread(t.id))}})}async openThread(e){return this.assertOpen(),"scheduled"!==this.requireThread(e.threadId).origin.kind&&await this.requireSession(e.threadId),this.snapshotThread({threadId:e.threadId,limit:100})}snapshotThread(e){this.assertOpen();const t=this.requireThread(e.threadId),r=this.store.listRecentTranscriptMessages(e.threadId,{...void 0!==e.messageBeforeSequence?{beforeSequence:e.messageBeforeSequence}:{},limit:e.limit}),s=this.store.listRecentToolExecutions(e.threadId,{...void 0!==e.operationBeforeSequence?{beforeSequence:e.operationBeforeSequence}:{},limit:e.limit}),n=this.activeTurns.get(e.threadId),i=void 0===n?void 0:{id:n.turnId,status:n.status,startedAt:n.startedAt};return{thread:K(this.store,t),messages:{items:r.entries.flatMap(e=>{const t=Q(e);return void 0===t?[]:[t]}),nextBeforeSequence:r.nextBeforeSequence??null},operations:{items:s.entries.map(X),nextBeforeSequence:s.nextBeforeSequence??null},...void 0!==i?{activeTurn:i}:{},pendingApprovals:[...this.pendingApprovals.values()].filter(t=>t.threadId===e.threadId).map(e=>e.approval),transcriptCompleteness:this.store.getTranscriptCompleteness(e.threadId),eventCursor:this.store.getRuntimeEventCursor(e.threadId)}}resumeEvents(e){this.assertOpen(),this.requireThread(e.threadId);try{return this.store.resumeRuntimeEvents(e.threadId,e.afterCursor)}catch(t){if(t instanceof P)throw new RuntimeServiceError(n.eventCursorExpired,"Runtime Event cursor 已超出当前 Thread 的保留窗口,请回退到 thread.snapshot",{details:{threadId:e.threadId}});if(t instanceof C)throw new RuntimeServiceError(n.eventCursorGap,"Runtime Event cursor 与当前 Thread 事件日志不连续,请回退到 thread.snapshot",{details:{threadId:e.threadId}});throw t}}async renameThread(e){return this.mutationRequests.run(e.requestId,a.threadRename,e,()=>(this.assertOpen(),this.requireThread(e.threadId),this.store.updateTitle(e.threadId,e.title),{thread:K(this.store,this.requireThread(e.threadId))}))}async deleteThread(e){return this.mutationRequests.run(e.requestId,a.threadDelete,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.threadBusy,`Thread "${e.threadId}" 仍有活动 Turn`,{retryable:!0});const t=this.sessions.get(e.threadId);return void 0!==t&&(await t.close(),this.sessions.delete(e.threadId)),this.attachmentStore?.releaseThread(e.threadId),this.store.deleteThread(e.threadId),{deleted:!0}})}async detachThread(e){return this.mutationRequests.run(e.requestId,a.threadDetach,e,async()=>{if(this.assertOpen(),this.requireThread(e.threadId),this.activeTurns.has(e.threadId))return{detached:!1};const t=this.sessions.get(e.threadId);return void 0===t?{detached:!1}:(await t.close(),this.sessions.delete(e.threadId),{detached:!0})})}async threadCapabilities(e){const t=await this.requireSession(e.threadId);return{manifest:D(x(t.getCapabilityManifest(),t.getCapabilityTurnContext()),void 0)}}async startTurn(e){return this.mutationRequests.run(e.requestId,a.turnStart,e,async()=>{if(this.assertOpen(),"scheduled"===this.requireThread(e.threadId).origin.kind)throw new RuntimeServiceError(n.invalidParams,"定时执行会话是只读记录;请基于该次运行创建新对话");const t=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==t){if(t===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const r=await this.requireSession(e.threadId),s=this.findTurnOwner(e.turnId,e.threadId);if(void 0!==s){if(s===e.threadId)return{accepted:!0,turnId:e.turnId};throw new RuntimeServiceError(n.turnAlreadyActive,`Turn "${e.turnId}" 已属于另一个 Thread`)}if(this.activeTurns.has(e.threadId))throw new RuntimeServiceError(n.turnAlreadyActive,`Thread "${e.threadId}" 已有活动 Turn`,{retryable:!0});const i=this.resolveTurnAttachments(e),a={threadId:e.threadId,turnId:e.turnId,session:r,startedAt:(new Date).toISOString(),status:"running",interactionFailure:void 0};this.activeTurns.set(e.threadId,a),this.activeTurnOwners.set(e.turnId,e.threadId);try{this.emit(e.threadId,e.turnId,{type:"turn.started"})}catch(t){throw this.activeTurns.delete(e.threadId),this.activeTurnOwners.delete(e.turnId),t}return this.driveTurn(a,e.input.text,i).catch(()=>{}),{accepted:!0,turnId:e.turnId}})}async cancelTurn(e){return this.mutationRequests.run(e.requestId,a.turnCancel,e,()=>{this.assertOpen();const t=this.activeTurns.get(e.threadId);if(void 0===t||t.turnId!==e.turnId)throw new RuntimeServiceError(n.turnNotFound,`Turn "${e.turnId}" 不存在或已结束`);t.status="cancelling";let r=!1;try{this.cancelPendingUserInputsForTurn(e.turnId,"Turn 已由客户端取消"),this.cancelPendingApprovalsForTurn(e.turnId,{status:"cancelled",reason:"Turn 已由客户端取消"})}finally{r=t.session.cancel()}return{cancelling:r}})}async respondApproval(e){return this.mutationRequests.run(e.requestId,a.approvalRespond,e,()=>this.resolvePendingApproval(e,"approve"===e.decision?{decision:"approve"}:{decision:"reject",...void 0!==e.reason?{reason:e.reason}:{}}))}resolvePendingApproval(e,t){this.assertOpen();const r=this.requirePendingApproval(e),s="approve"===t.decision?r.session.approve(e.approvalId,t.scope):r.session.reject(e.approvalId,t.reason);if(this.pendingApprovals.delete(e.approvalId),!s)throw this.emitApprovalResolved(r,{status:"cancelled",reason:"审批已失效"}),new RuntimeServiceError(n.approvalNotFound,`Approval "${e.approvalId}" 已失效`);return this.emitApprovalResolved(r,"approve"===t.decision?{status:"resolved",decision:"approve"}:{status:"resolved",decision:"reject",...void 0!==t.reason?{reason:t.reason}:{}}),{resolved:!0}}getPendingApprovalExpiresAt(e){return this.findPendingApproval(e)?.expiresAt}async stageAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentStage,e,()=>{this.assertOpen(),this.requireThread(e.threadId);const t=this.requireAttachmentStore().stage({threadId:e.threadId,fileName:e.fileName,mediaType:e.mediaType,bytes:e.bytes,sha256:e.sha256,source:e.source,sourcePath:e.sourcePath});return t.ok||this.throwAttachmentFailure(t),{attachmentId:t.attachmentId,state:t.state,...void 0!==t.descriptor?{descriptor:t.descriptor}:{}}})}async appendAttachmentChunk(e){return this.mutationRequests.run(e.requestId,a.attachmentChunk,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().appendChunk({threadId:e.threadId,attachmentId:e.attachmentId,sequence:e.sequence,dataBase64:e.dataBase64});return t.ok||this.throwAttachmentFailure(t),{receivedBytes:t.receivedBytes,nextSequence:t.nextSequence}})}async commitAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentCommit,e,()=>{this.assertOpen();const t=this.requireAttachmentStore().commit({threadId:e.threadId,attachmentId:e.attachmentId});return t.ok||this.throwAttachmentFailure(t),{descriptor:t.descriptor}})}async releaseAttachment(e){return this.mutationRequests.run(e.requestId,a.attachmentRelease,e,()=>{this.assertOpen();return{released:this.requireAttachmentStore().release({threadId:e.threadId,attachmentId:e.attachmentId}).released}})}requireAttachmentStore(){if(void 0===this.attachmentStore)throw new RuntimeServiceError(n.capabilityUnavailable,"Runtime 未配置附件存储,attachments 能力不可用");return this.attachmentStore}throwAttachmentFailure(e){throw new RuntimeServiceError(e.code,e.message,{...void 0!==e.retryable?{retryable:e.retryable}:{}})}resolveTurnAttachments(e){const t=e.input.attachments;if(void 0===t||0===t.length)return;const r=this.requireAttachmentStore(),s=new Set,i=[];for(const a of t){if(s.has(a))throw new RuntimeServiceError(n.invalidParams,`附件 "${a}" 在同一 Turn 中重复引用`);s.add(a);const t=r.readCommitted({threadId:e.threadId,attachmentId:a});t.ok||this.throwAttachmentFailure(t),i.push({data:t.dataBase64,mediaType:t.descriptor.mediaType})}return i}cancelPendingApproval(e,t){const r=this.findPendingApproval(e);return void 0!==r&&(this.pendingApprovals.delete(e.approvalId),this.emitApprovalResolved(r,t),!0)}async failPendingApprovalInteraction(e,t,r={status:"cancelled",reason:t}){const s=this.findPendingApproval(e);if(void 0===s)return!1;const n=this.activeTurns.get(e.threadId);void 0!==n&&n.turnId===e.turnId&&(n.interactionFailure=t),this.pendingApprovals.delete(e.approvalId);try{this.emitApprovalResolved(s,r)}finally{s.session.cancel()}return!0}resolvePendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;let s;try{s=I(r.form,t)}catch{return this.cancelPendingUserInput(e,"用户输入不符合原始表单约束"),!1}this.pendingUserInputs.delete(e);const n=r.session.resolveUserInput?.(e,s)??!1;if(!n){const t="用户输入请求已失效";return r.session.cancelUserInput?.(e,t),this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),!1}return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:"submitted"===s.status?"用户已提交输入":"用户已取消输入"}),n}cancelPendingUserInput(e,t){const r=this.pendingUserInputs.get(e);if(void 0===r)return!1;this.pendingUserInputs.delete(e);const s=r.session.cancelUserInput?.(e,t)??!1;return this.restoreRunningStatus(r),this.emitUserInputInteraction({type:"settled",requestId:e,threadId:r.threadId,turnId:r.turnId,reason:t}),s}getOperation(e){this.assertOpen(),this.requireThread(e.threadId);const t=this.store.getToolExecution(e.threadId,e.operationId);return{operation:void 0===t?null:X(t)}}async close(){if(this.closing)return;this.closing=!0,this.setUserInputAvailable(!1);for(const e of this.activeTurns.values())e.session.cancel();const e=[...this.sessions.values()];this.sessions.clear(),this.activeTurns.clear(),this.activeTurnOwners.clear(),this.settledTurnOwners.clear(),this.pendingApprovals.clear(),this.pendingUserInputs.clear(),this.userInputListeners.clear(),this.attachmentStore?.close(),await Promise.allSettled(e.map(e=>e.close())),this.mutationRequests.clear(),this.fatalErrorListeners.clear()}assertOpen(){if(this.closing)throw new RuntimeServiceError(n.runtimeClosing,"Runtime 正在关闭",{retryable:!0})}requireThread(e){const t=this.store.getThread(e);if(void 0===t)throw new RuntimeServiceError(n.threadNotFound,`Thread "${e}" 不存在`);return t}async requireSession(e){this.assertOpen(),this.requireThread(e);const t=this.sessions.get(e);if(void 0!==t)return t;const r=await this.engine.resumeSession(e);return this.rememberSession(r),r}rememberSession(e){e.setUserInputAvailable?.(this.userInputAvailable),this.sessions.set(e.id,e)}emit(e,t,r){const s=(new Date).toISOString(),n={protocolVersion:o,runtimeInstanceId:this.runtimeInstanceId,sequence:this.sequence,timestamp:s,threadId:e,...void 0!==t?{turnId:t}:{}},i=f.safeParse(r),a=i.success?(()=>{let r;try{r=this.store.appendRuntimeEvent({threadId:e,...void 0===t?{}:{turnId:t},timestamp:s,event:i.data})}catch(e){throw this.notifyFatalError(e),e}return{...n,durability:"durable",eventId:r.eventId,cursor:r.cursor,event:i.data}})():{...n,durability:"ephemeral",event:y.parse(r)};this.sequence+=1;for(const e of this.listeners)try{e(a)}catch{}}notifyFatalError(e){for(const e of this.activeTurns.values())try{e.session.cancel()}catch{}for(const t of this.fatalErrorListeners)try{t(e)}catch{}}async driveTurn(e,t,r){const s={streamId:void 0,text:"",terminalEvent:void 0},n=void 0!==r&&r.length>0?{text:t,attachments:r}:t;let i;try{for await(const t of e.session.send(n))this.projectSessionEvent(e,s,t);i=s.terminalEvent??{type:"turn.completed"}}catch(e){i=s.terminalEvent??{type:"turn.failed",stage:"execute",message:b(W(e))}}finally{this.activeTurns.get(e.threadId)===e&&this.activeTurns.delete(e.threadId),this.settleTurnOwner(e)}this.emitTurnTerminal(e,i)}findTurnOwner(e,t){const r=this.activeTurnOwners.get(e);if(void 0!==r)return r;const s=this.settledTurnOwners.get(e);return s===t&&(this.settledTurnOwners.delete(e),this.settledTurnOwners.set(e,s)),s}settleTurnOwner(e){if(this.activeTurnOwners.get(e.turnId)===e.threadId&&(this.activeTurnOwners.delete(e.turnId),!this.closing))for(this.settledTurnOwners.set(e.turnId,e.threadId);this.settledTurnOwners.size>this.idempotencyCacheEntries;){const e=this.settledTurnOwners.keys().next().value;if(void 0===e)return;this.settledTurnOwners.delete(e)}}projectSessionEvent(e,r,s){switch(s.type){case"message-start":{const s=A.parse(t());return r.streamId=s,r.text="",void this.emit(e.threadId,e.turnId,{type:"message.started",streamId:s})}case"text-delta":{const n=r.streamId??A.parse(t());return void 0===r.streamId&&(r.streamId=n,this.emit(e.threadId,e.turnId,{type:"message.started",streamId:n})),r.text+=s.delta,void this.emit(e.threadId,e.turnId,{type:"message.delta",streamId:n,delta:s.delta})}case"message-finish":{const n=r.streamId??A.parse(t());return void this.emit(e.threadId,e.turnId,{type:"message.completed",streamId:n,text:b(s.text||r.text)})}case"reasoning-delta":{const t=this.reasoningSummaryProjector?.(s.delta);return void(void 0!==t&&t.length>0&&this.emit(e.threadId,e.turnId,{type:"reasoning.summary.delta",reasoningId:s.reasoningId,delta:b(t)}))}case"tool-call":return void this.emit(e.threadId,e.turnId,{type:"tool.started",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,input:D(s.input,void 0)});case"tool-output-delta":return void this.emit(e.threadId,e.turnId,{type:"tool.output",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,stream:s.stream,delta:b(s.delta)});case"tool-result":return void this.emit(e.threadId,e.turnId,{type:"tool.completed",toolCallId:s.toolCallId,agentName:s.agentName,toolName:s.toolName,...void 0!==s.executionId?{operationId:v.parse(s.executionId)}:{},...void 0!==s.outcome?{outcome:this.projectToolOutcome(s.outcome)}:{},display:D(s.display,void 0)});case"confirmation-required":{const t=Y(e.turnId,s);return this.pendingApprovals.set(t.id,{threadId:e.threadId,session:e.session,approval:t,expiresAt:s.expiresAt}),void this.emit(e.threadId,e.turnId,{type:"approval.required",approval:t})}case"user-input-required":{if(!this.userInputAvailable||void 0===e.session.resolveUserInput||void 0===e.session.cancelUserInput)return void e.session.cancelUserInput?.(s.requestId,"当前客户端未协商用户输入处理能力");const t=S.parse(s.form),r=S.parse(t),n={requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,session:e.session,form:t,expiresAt:s.expiresAt};this.pendingUserInputs.set(s.requestId,n),e.status="waiting-for-user";return void(this.emitUserInputInteraction({type:"required",requestId:s.requestId,threadId:e.threadId,turnId:e.turnId,form:r,expiresAt:s.expiresAt})||this.cancelPendingUserInput(s.requestId,"当前没有可处理用户输入请求的客户端"))}case"user-input-settled":{const e=this.pendingUserInputs.get(s.requestId);if(void 0===e)return;return this.pendingUserInputs.delete(s.requestId),this.restoreRunningStatus(e),void this.emitUserInputInteraction({type:"settled",requestId:e.requestId,threadId:e.threadId,turnId:e.turnId,reason:"submitted"===s.status?"用户输入请求已结算":"用户输入请求已取消"})}case"turn-cancelled":r.terminalEvent??=void 0===e.interactionFailure?{type:"turn.cancelled",reason:s.reason,message:b(s.message)}:{type:"turn.failed",stage:"execute",message:b(e.interactionFailure)};break;case"error":r.terminalEvent??={type:"turn.failed",stage:s.stage,message:b(s.message)}}}projectToolOutcome(e){return{kind:e.kind,..."reason"in e&&void 0!==e.reason?{reason:b(e.reason)}:{}}}findPendingApproval(e){const t=this.pendingApprovals.get(e.approvalId);if(void 0!==t&&t.threadId===e.threadId&&t.approval.turnId===e.turnId)return t}requirePendingApproval(e){const t=this.findPendingApproval(e);if(void 0===t)throw new RuntimeServiceError(n.approvalNotFound,`Approval "${e.approvalId}" 不存在或已失效`);return t}emitApprovalResolved(e,t){const r="reason"in t&&void 0!==t.reason?{...t,reason:b(t.reason)}:t;this.emit(e.threadId,e.approval.turnId,{type:"approval.resolved",approvalId:e.approval.id,resolution:r})}cancelPendingApprovalsForTurn(e,t){const r=[...this.pendingApprovals.values()].filter(t=>t.approval.turnId===e);let s;for(const e of r)try{this.cancelPendingApproval({threadId:e.threadId,turnId:e.approval.turnId,approvalId:e.approval.id},t)}catch(e){s??=e}if(void 0!==s)throw s}cancelPendingUserInputsForTurn(e,t){const r=[...this.pendingUserInputs.values()].filter(t=>t.turnId===e).map(e=>e.requestId);for(const e of r)this.cancelPendingUserInput(e,t)}cancelAllPendingUserInputs(e){for(const t of[...this.pendingUserInputs.keys()])this.cancelPendingUserInput(t,e)}restoreRunningStatus(e){const t=this.activeTurns.get(e.threadId);void 0!==t&&t.turnId===e.turnId&&"waiting-for-user"===t.status&&(t.status="running")}emitUserInputInteraction(e){let t=!1;for(const r of this.userInputListeners)try{r(e),t=!0}catch{}return t}emitTurnTerminal(e,t){this.cancelPendingUserInputsForTurn(e.turnId,`Turn 已终止:${t.type}`),this.cancelPendingApprovalsForTurn(e.turnId,"turn.cancelled"===t.type&&"timeout"===t.reason?{status:"expired",reason:"Turn 已超时"}:{status:"cancelled",reason:`Turn 已终止:${t.type}`}),this.emit(e.threadId,e.turnId,t)}}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const THREAD_ORIGIN_KINDS: readonly ["interactive", "scheduled"];
|
|
3
|
+
export declare const scheduledThreadOriginSchema: z.ZodReadonly<z.ZodObject<{
|
|
4
|
+
kind: z.ZodLiteral<"scheduled">;
|
|
5
|
+
scheduleId: z.ZodString;
|
|
6
|
+
invocationId: z.ZodString;
|
|
7
|
+
attempt: z.ZodNumber;
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
cwd: z.ZodString;
|
|
10
|
+
scheduledFor: z.ZodString;
|
|
11
|
+
ledgerDir: z.ZodString;
|
|
12
|
+
}, "strict", z.ZodTypeAny, {
|
|
13
|
+
name: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
attempt: number;
|
|
16
|
+
kind: "scheduled";
|
|
17
|
+
scheduleId: string;
|
|
18
|
+
invocationId: string;
|
|
19
|
+
scheduledFor: string;
|
|
20
|
+
ledgerDir: string;
|
|
21
|
+
}, {
|
|
22
|
+
name: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
attempt: number;
|
|
25
|
+
kind: "scheduled";
|
|
26
|
+
scheduleId: string;
|
|
27
|
+
invocationId: string;
|
|
28
|
+
scheduledFor: string;
|
|
29
|
+
ledgerDir: string;
|
|
30
|
+
}>>;
|
|
31
|
+
export declare const threadOriginSchema: z.ZodReadonly<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
32
|
+
kind: z.ZodLiteral<"interactive">;
|
|
33
|
+
}, "strict", z.ZodTypeAny, {
|
|
34
|
+
kind: "interactive";
|
|
35
|
+
}, {
|
|
36
|
+
kind: "interactive";
|
|
37
|
+
}>, z.ZodObject<{
|
|
38
|
+
kind: z.ZodLiteral<"scheduled">;
|
|
39
|
+
scheduleId: z.ZodString;
|
|
40
|
+
invocationId: z.ZodString;
|
|
41
|
+
attempt: z.ZodNumber;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
cwd: z.ZodString;
|
|
44
|
+
scheduledFor: z.ZodString;
|
|
45
|
+
ledgerDir: z.ZodString;
|
|
46
|
+
}, "strict", z.ZodTypeAny, {
|
|
47
|
+
name: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
attempt: number;
|
|
50
|
+
kind: "scheduled";
|
|
51
|
+
scheduleId: string;
|
|
52
|
+
invocationId: string;
|
|
53
|
+
scheduledFor: string;
|
|
54
|
+
ledgerDir: string;
|
|
55
|
+
}, {
|
|
56
|
+
name: string;
|
|
57
|
+
cwd: string;
|
|
58
|
+
attempt: number;
|
|
59
|
+
kind: "scheduled";
|
|
60
|
+
scheduleId: string;
|
|
61
|
+
invocationId: string;
|
|
62
|
+
scheduledFor: string;
|
|
63
|
+
ledgerDir: string;
|
|
64
|
+
}>]>>;
|
|
65
|
+
export declare const threadDerivedFromSchema: z.ZodReadonly<z.ZodObject<{
|
|
66
|
+
threadId: z.ZodString;
|
|
67
|
+
origin: z.ZodReadonly<z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
68
|
+
kind: z.ZodLiteral<"interactive">;
|
|
69
|
+
}, "strict", z.ZodTypeAny, {
|
|
70
|
+
kind: "interactive";
|
|
71
|
+
}, {
|
|
72
|
+
kind: "interactive";
|
|
73
|
+
}>, z.ZodObject<{
|
|
74
|
+
kind: z.ZodLiteral<"scheduled">;
|
|
75
|
+
scheduleId: z.ZodString;
|
|
76
|
+
invocationId: z.ZodString;
|
|
77
|
+
attempt: z.ZodNumber;
|
|
78
|
+
name: z.ZodString;
|
|
79
|
+
cwd: z.ZodString;
|
|
80
|
+
scheduledFor: z.ZodString;
|
|
81
|
+
ledgerDir: z.ZodString;
|
|
82
|
+
}, "strict", z.ZodTypeAny, {
|
|
83
|
+
name: string;
|
|
84
|
+
cwd: string;
|
|
85
|
+
attempt: number;
|
|
86
|
+
kind: "scheduled";
|
|
87
|
+
scheduleId: string;
|
|
88
|
+
invocationId: string;
|
|
89
|
+
scheduledFor: string;
|
|
90
|
+
ledgerDir: string;
|
|
91
|
+
}, {
|
|
92
|
+
name: string;
|
|
93
|
+
cwd: string;
|
|
94
|
+
attempt: number;
|
|
95
|
+
kind: "scheduled";
|
|
96
|
+
scheduleId: string;
|
|
97
|
+
invocationId: string;
|
|
98
|
+
scheduledFor: string;
|
|
99
|
+
ledgerDir: string;
|
|
100
|
+
}>]>>;
|
|
101
|
+
capturedAt: z.ZodString;
|
|
102
|
+
}, "strict", z.ZodTypeAny, {
|
|
103
|
+
threadId: string;
|
|
104
|
+
origin: Readonly<{
|
|
105
|
+
name: string;
|
|
106
|
+
cwd: string;
|
|
107
|
+
attempt: number;
|
|
108
|
+
kind: "scheduled";
|
|
109
|
+
scheduleId: string;
|
|
110
|
+
invocationId: string;
|
|
111
|
+
scheduledFor: string;
|
|
112
|
+
ledgerDir: string;
|
|
113
|
+
}> | Readonly<{
|
|
114
|
+
kind: "interactive";
|
|
115
|
+
}>;
|
|
116
|
+
capturedAt: string;
|
|
117
|
+
}, {
|
|
118
|
+
threadId: string;
|
|
119
|
+
origin: Readonly<{
|
|
120
|
+
name: string;
|
|
121
|
+
cwd: string;
|
|
122
|
+
attempt: number;
|
|
123
|
+
kind: "scheduled";
|
|
124
|
+
scheduleId: string;
|
|
125
|
+
invocationId: string;
|
|
126
|
+
scheduledFor: string;
|
|
127
|
+
ledgerDir: string;
|
|
128
|
+
}> | Readonly<{
|
|
129
|
+
kind: "interactive";
|
|
130
|
+
}>;
|
|
131
|
+
capturedAt: string;
|
|
132
|
+
}>>;
|
|
133
|
+
export type ThreadOrigin = z.infer<typeof threadOriginSchema>;
|
|
134
|
+
export type ScheduledThreadOrigin = z.infer<typeof scheduledThreadOriginSchema>;
|
|
135
|
+
export type ThreadDerivedFrom = z.infer<typeof threadDerivedFromSchema>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{z as e}from"zod";export const THREAD_ORIGIN_KINDS=["interactive","scheduled"];export const scheduledThreadOriginSchema=e.object({kind:e.literal(THREAD_ORIGIN_KINDS[1]),scheduleId:e.string().min(1),invocationId:e.string().min(1),attempt:e.number().int().positive(),name:e.string().min(1),cwd:e.string().min(1),scheduledFor:e.string().datetime({offset:!0}),ledgerDir:e.string().min(1)}).strict().readonly();export const threadOriginSchema=e.discriminatedUnion("kind",[e.object({kind:e.literal(THREAD_ORIGIN_KINDS[0])}).strict(),scheduledThreadOriginSchema.unwrap()]).readonly();export const threadDerivedFromSchema=e.object({threadId:e.string().min(1),origin:threadOriginSchema,capturedAt:e.string().datetime({offset:!0})}).strict().readonly();
|
|
@@ -3,6 +3,7 @@ import { type RuntimeDurableEventV13, type RuntimeEventCursor, type RuntimeEvent
|
|
|
3
3
|
import { type ToolExecutionPersistenceMetadata, type ToolExecutionRecord, type ToolExecutionRecordId } from "../tool-bridge/tool-execution-record.ts";
|
|
4
4
|
import { COMPACTION_TRANSCRIPT_COMPLETENESS, type ArchivedTranscriptMessage, type CompactionCheckpoint, type CompactionCheckpointDraftInput, type CompactionSemanticEvidenceWatermarks } from "../engine/compaction-checkpoint.ts";
|
|
5
5
|
import { type CompactionSemanticState } from "../engine/compaction-semantic-state.ts";
|
|
6
|
+
import { type ThreadOrigin, type ScheduledThreadOrigin, type ThreadDerivedFrom } from "./thread-origin.ts";
|
|
6
7
|
export declare const TOOL_EXECUTION_RETENTION_POLICY: {
|
|
7
8
|
readonly maxBytesPerThread: number;
|
|
8
9
|
readonly maxRecordsPerThread: 2000;
|
|
@@ -62,11 +63,39 @@ export interface ThreadRecord {
|
|
|
62
63
|
readonly model: string | undefined;
|
|
63
64
|
readonly createdAt: string;
|
|
64
65
|
readonly updatedAt: string;
|
|
66
|
+
readonly origin: ThreadOrigin;
|
|
67
|
+
readonly derivedFrom?: ThreadDerivedFrom;
|
|
65
68
|
}
|
|
66
69
|
export interface CreateThreadInput {
|
|
67
70
|
readonly id?: string;
|
|
68
71
|
readonly title?: string;
|
|
69
72
|
readonly model?: string;
|
|
73
|
+
readonly origin?: ThreadOrigin;
|
|
74
|
+
readonly derivedFrom?: ThreadDerivedFrom;
|
|
75
|
+
}
|
|
76
|
+
export interface ListThreadsOptions {
|
|
77
|
+
readonly origin?: ThreadOrigin["kind"];
|
|
78
|
+
}
|
|
79
|
+
export interface ForkThreadSnapshotInput {
|
|
80
|
+
readonly id?: string;
|
|
81
|
+
readonly title?: string;
|
|
82
|
+
readonly model?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ThreadSnapshot {
|
|
85
|
+
readonly thread: ThreadRecord;
|
|
86
|
+
readonly capturedAt: string;
|
|
87
|
+
readonly messages: readonly ModelMessage[];
|
|
88
|
+
readonly transcript: readonly ArchivedTranscriptMessage[];
|
|
89
|
+
readonly toolExecutions: readonly SequencedToolExecutionRecord[];
|
|
90
|
+
readonly checkpoints: readonly CompactionCheckpoint[];
|
|
91
|
+
readonly transcriptCompleteness: TranscriptCompleteness;
|
|
92
|
+
readonly nextToolExecutionSequence: number;
|
|
93
|
+
readonly toolExecutionCoverage: readonly {
|
|
94
|
+
readonly executionId: string;
|
|
95
|
+
readonly representation: ToolExecutionContextRepresentation | "legacy_assumed";
|
|
96
|
+
readonly transcriptSequence: number | null;
|
|
97
|
+
readonly createdAt: string;
|
|
98
|
+
}[];
|
|
70
99
|
}
|
|
71
100
|
export type SequencedToolExecutionRecord = ToolExecutionRecord & {
|
|
72
101
|
readonly sequence: number;
|
|
@@ -147,9 +176,15 @@ export interface ReadCheckpointTranscriptOptions {
|
|
|
147
176
|
}
|
|
148
177
|
export declare function expandTilde(path: string): string;
|
|
149
178
|
export declare function defaultThreadsDir(): string;
|
|
179
|
+
export interface ThreadStoreOptions {
|
|
180
|
+
readonly now?: () => Date;
|
|
181
|
+
readonly readOnly?: boolean;
|
|
182
|
+
}
|
|
150
183
|
export declare class ThreadStore {
|
|
151
184
|
private readonly db;
|
|
152
|
-
|
|
185
|
+
private readonly now;
|
|
186
|
+
private readonly readOnly;
|
|
187
|
+
constructor(dir?: string, options?: ThreadStoreOptions);
|
|
153
188
|
private init;
|
|
154
189
|
private migrateToolExecutionPersistenceInTransaction;
|
|
155
190
|
private enforceAllToolExecutionRetentionInTransaction;
|
|
@@ -159,10 +194,17 @@ export declare class ThreadStore {
|
|
|
159
194
|
createThread(input?: CreateThreadInput): string;
|
|
160
195
|
hasThread(id: string): boolean;
|
|
161
196
|
getThread(id: string): ThreadRecord | undefined;
|
|
162
|
-
listThreads(): ThreadRecord[];
|
|
197
|
+
listThreads(options?: ListThreadsOptions): ThreadRecord[];
|
|
198
|
+
/** Backfill only legacy rows; an explicit source is immutable, including interactive. */
|
|
199
|
+
backfillScheduledOrigin(threadId: string, origin: ScheduledThreadOrigin): boolean;
|
|
200
|
+
/** A single SQLite read transaction pins every part of the committed snapshot. */
|
|
201
|
+
readSnapshot(threadId: string): ThreadSnapshot;
|
|
202
|
+
/** Copies durable evidence atomically; Runtime events and live authority are never copied. */
|
|
203
|
+
forkSnapshot(snapshot: ThreadSnapshot, input?: ForkThreadSnapshotInput): string;
|
|
163
204
|
countMessages(threadId: string): number;
|
|
164
205
|
countTranscriptMessages(threadId: string): number;
|
|
165
206
|
updateTitle(threadId: string, title: string): void;
|
|
207
|
+
updateModel(threadId: string, model: string): void;
|
|
166
208
|
deleteThread(threadId: string): void;
|
|
167
209
|
appendRuntimeEvent(input: AppendRuntimeEventInput): StoredRuntimeEvent;
|
|
168
210
|
getRuntimeEventCursor(threadId: string): RuntimeEventCursor | null;
|