@roll-agent/runtime 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/host-contracts/index.d.ts +1 -0
- package/dist/host-contracts/index.js +1 -1
- package/dist/host-contracts/schedule-context.d.ts +4 -0
- package/dist/host-contracts/schedule-context.js +1 -0
- package/dist/host-contracts/schedule-tools.d.ts +4 -0
- package/dist/ownership/session-claims.d.ts +2 -0
- package/dist/ownership/session-claims.js +1 -1
- package/dist/scheduler/database-version.d.ts +1 -1
- package/dist/scheduler/database-version.js +1 -1
- package/dist/scheduler/schedule-store.js +1 -1
- package/dist/scheduler/types.d.ts +3 -0
- package/dist/store/thread-store.js +1 -1
- package/package.json +1 -1
|
@@ -6,3 +6,4 @@ export type { AgentToolSource, SourceTool } from "../tool-bridge/build-tools.ts"
|
|
|
6
6
|
export type { ToolAnnotations } from "../types/policy.ts";
|
|
7
7
|
export type { ToolResourceHint } from "../tool-contracts/resource-hints.ts";
|
|
8
8
|
export { SESSION_OWNER_LIVENESS, type SessionOwnershipHost } from "./session-ownership.ts";
|
|
9
|
+
export { scheduleHostContextSchema, type ScheduleHostContext } from "./schedule-context.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./schedule-tools.js";export*from"./agent-host.js";export*from"./runtime-settings.js";export{AGENT_BOOTSTRAP_MAX_CONCURRENCY,mapWithBoundedConcurrency}from"../engine/agent-bootstrap.js";export{SESSION_OWNER_LIVENESS}from"./session-ownership.js";
|
|
1
|
+
export*from"./schedule-tools.js";export*from"./agent-host.js";export*from"./runtime-settings.js";export{AGENT_BOOTSTRAP_MAX_CONCURRENCY,mapWithBoundedConcurrency}from"../engine/agent-bootstrap.js";export{SESSION_OWNER_LIVENESS}from"./session-ownership.js";export{scheduleHostContextSchema}from"./schedule-context.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Opaque, bounded host-owned reference. Runtime must never interpret its contents. */
|
|
3
|
+
export declare const scheduleHostContextSchema: z.core.$ZodBranded<z.ZodString, "ScheduleHostContext", "out">;
|
|
4
|
+
export type ScheduleHostContext = z.infer<typeof scheduleHostContextSchema>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{z as o}from"zod";export const scheduleHostContextSchema=o.string().min(1).max(8192).brand();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScheduleHostContext } from "./schedule-context.ts";
|
|
1
2
|
import type { ScheduleRecord } from "../scheduler/types.ts";
|
|
2
3
|
import type { ScheduleTimingInput } from "../scheduler/trigger.ts";
|
|
3
4
|
export declare const SCHEDULE_TOOL_ERROR_CODES: {
|
|
@@ -30,6 +31,8 @@ export interface ScheduleToolCreateRequest extends ScheduleTimingInput {
|
|
|
30
31
|
readonly rounds?: number | undefined;
|
|
31
32
|
}
|
|
32
33
|
export interface ScheduleCreateAdmission {
|
|
34
|
+
readonly hostContextRevision?: string;
|
|
35
|
+
readonly hostContext?: ScheduleHostContext;
|
|
33
36
|
readonly ok: true;
|
|
34
37
|
readonly name: string;
|
|
35
38
|
readonly prompt: string;
|
|
@@ -115,6 +118,7 @@ export interface ScheduleToolExtendRequest {
|
|
|
115
118
|
readonly requestId: string;
|
|
116
119
|
}
|
|
117
120
|
export interface ScheduleExtendAdmission {
|
|
121
|
+
readonly hostContextRevision?: string;
|
|
118
122
|
readonly ok: true;
|
|
119
123
|
readonly request: ScheduleToolExtendRequest;
|
|
120
124
|
readonly sessionCwd: string;
|
|
@@ -53,6 +53,8 @@ export declare class SessionClaims {
|
|
|
53
53
|
private acquireKey;
|
|
54
54
|
/** No writer can enter while maintenance owns this gate; any held writer blocks it. */
|
|
55
55
|
maintenance(): SessionClaim;
|
|
56
|
+
/** Keep first creation and writable preflight under the same cross-process SQLite gate. */
|
|
57
|
+
beginLibraryInitialization(isNew: () => boolean): () => void;
|
|
56
58
|
private assert;
|
|
57
59
|
private handle;
|
|
58
60
|
/** Refuse to convert resource disposal into proof that unsettled sessions are safe. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID as e}from"node:crypto";import{chmodSync as i,existsSync as r,lstatSync as t,mkdirSync as s,realpathSync as a,statSync as o}from"node:fs";import{dirname as n,resolve as d}from"node:path";import{DatabaseSync as c}from"node:sqlite";import{z as h}from"zod";export const SESSION_OCCUPANCY_CAPABILITY={version:1,scope:"canonical-library-and-thread",claims:"sqlite-sidecar-cas",idleOwnership:!0,forceTakeover:!1,legacyWriters:"unverified-sequential-only"};const l=h.object({thread_id:h.string(),claim_id:h.string(),identity:h.string(),owner_json:h.string()}),p=h.object({kind:h.string(),label:h.string(),workspace:h.string().optional()});function m(e){if(void 0!==e)try{return p.parse(JSON.parse(e.owner_json))}catch{return}}const u="";export class SessionOccupiedError extends Error{details;constructor(e,i,r="session_occupied"){super("此会话正在另一个客户端中使用。请先退出该客户端的当前会话,再重新检测;两端请选择其一。"),this.name="SessionOccupiedError";const t=m(i);this.details={reason:r,threadId:e,action:"close_other_client_and_retry",...void 0!==t?{ownerKind:t.kind,label:t.label,...void 0===t.workspace?{}:{workspace:t.workspace}}:{}}}}export function canonicalSessionDatabasePath(e){if(!r(e)){if(t(e,{throwIfNoEntry:!1})?.isSymbolicLink())throw new SessionOccupiedError("",void 0,"ambiguous_library_alias");return d(a(n(e)),"threads.db")}const i=a(e);if(o(i).nlink>1)throw new SessionOccupiedError("",void 0,"ambiguous_library_alias");return i}export class SessionClaims{db;held=new Map;closed=!1;databasePath;constructor(e,r){if(this.host=r,!r.identity)throw new Error("Session ownership requires a verifiable host identity");s(n(e),{recursive:!0,mode:448}),this.databasePath=canonicalSessionDatabasePath(e);const t=`${this.databasePath}.occupancy.sqlite`;this.db=new c(t);try{"win32"!==process.platform&&i(t,384),this.db.exec("PRAGMA busy_timeout = 15000;\n CREATE TABLE IF NOT EXISTS claims (\n thread_id TEXT PRIMARY KEY, claim_id TEXT NOT NULL,\n identity TEXT NOT NULL, owner_json TEXT NOT NULL\n );")}catch(e){throw this.db.close(),e}}host;read(e){const i=this.db.prepare("SELECT * FROM claims WHERE thread_id = ?").get(e);return void 0===i?void 0:l.parse(i)}checkOpen(){if(this.closed)throw new Error("Session claims are closed");if(r(this.databasePath)&&canonicalSessionDatabasePath(this.databasePath)!==this.databasePath)throw new SessionOccupiedError("",void 0,"ambiguous_library_alias")}reap(e){let i="unknown";try{i=this.host.probe(e.identity)}catch{}return"dead"===i&&(this.db.prepare("DELETE FROM claims WHERE thread_id = ? AND claim_id = ?").run(e.thread_id,e.claim_id),!0)}acquire(e){if(!e)throw new Error("Thread ID must be nonempty");return this.acquireKey(e)}acquireKey(i){this.checkOpen();const r=this.held.get(i);if(void 0!==r)return this.assert(r.claim),r.references++,this.handle(r.claim);const t={thread_id:i,claim_id:e(),identity:this.host.identity,owner_json:JSON.stringify(this.host.owner)};for(let e=0;e<32;e++){let e;this.db.exec("BEGIN IMMEDIATE");try{e=this.read(u)??(i===u?this.db.prepare("SELECT * FROM claims LIMIT 1").all().map(e=>l.parse(e))[0]:this.read(i)),void 0===e&&this.db.prepare("INSERT INTO claims VALUES (?, ?, ?, ?)").run(i,t.claim_id,t.identity,t.owner_json),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}if(void 0===e)return this.held.set(i,{claim:t,references:1}),this.handle(t);if(!this.reap(e))throw new SessionOccupiedError(i,e)}throw new SessionOccupiedError(i)}maintenance(){return this.acquireKey(u)}assert(e){if(this.checkOpen(),this.read(e.thread_id)?.claim_id!==e.claim_id)throw new SessionOccupiedError(e.thread_id,void 0,"stale_session_claim")}handle(e){let i=!1;return{threadId:e.thread_id,token:e.claim_id,assert:()=>{if(i)throw new SessionOccupiedError(e.thread_id,void 0,"stale_session_claim");this.assert(e)},release:()=>{if(i)return;i=!0;const r=this.held.get(e.thread_id);r?.claim.claim_id===e.claim_id&&(r.references--,0===r.references&&(this.db.prepare("DELETE FROM claims WHERE thread_id = ? AND claim_id = ?").run(e.thread_id,e.claim_id),this.held.delete(e.thread_id)))}}}close(){if(!this.closed){if(0!==this.held.size)throw new Error("Cannot close ownership store with unsettled session claims");this.db.close(),this.closed=!0}}}
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{chmodSync as i,existsSync as r,lstatSync as t,mkdirSync as s,realpathSync as a,statSync as o}from"node:fs";import{dirname as n,resolve as d}from"node:path";import{DatabaseSync as c}from"node:sqlite";import{z as h}from"zod";export const SESSION_OCCUPANCY_CAPABILITY={version:1,scope:"canonical-library-and-thread",claims:"sqlite-sidecar-cas",idleOwnership:!0,forceTakeover:!1,legacyWriters:"unverified-sequential-only"};const l=h.object({thread_id:h.string(),claim_id:h.string(),identity:h.string(),owner_json:h.string()}),p=h.object({kind:h.string(),label:h.string(),workspace:h.string().optional()});function m(e){if(void 0!==e)try{return p.parse(JSON.parse(e.owner_json))}catch{return}}const u="";export class SessionOccupiedError extends Error{details;constructor(e,i,r="session_occupied"){super("此会话正在另一个客户端中使用。请先退出该客户端的当前会话,再重新检测;两端请选择其一。"),this.name="SessionOccupiedError";const t=m(i);this.details={reason:r,threadId:e,action:"close_other_client_and_retry",...void 0!==t?{ownerKind:t.kind,label:t.label,...void 0===t.workspace?{}:{workspace:t.workspace}}:{}}}}export function canonicalSessionDatabasePath(e){if(!r(e)){if(t(e,{throwIfNoEntry:!1})?.isSymbolicLink())throw new SessionOccupiedError("",void 0,"ambiguous_library_alias");return d(a(n(e)),"threads.db")}const i=a(e);if(o(i).nlink>1)throw new SessionOccupiedError("",void 0,"ambiguous_library_alias");return i}export class SessionClaims{db;held=new Map;closed=!1;databasePath;constructor(e,r){if(this.host=r,!r.identity)throw new Error("Session ownership requires a verifiable host identity");s(n(e),{recursive:!0,mode:448}),this.databasePath=canonicalSessionDatabasePath(e);const t=`${this.databasePath}.occupancy.sqlite`;this.db=new c(t);try{"win32"!==process.platform&&i(t,384),this.db.exec("PRAGMA busy_timeout = 15000;\n CREATE TABLE IF NOT EXISTS claims (\n thread_id TEXT PRIMARY KEY, claim_id TEXT NOT NULL,\n identity TEXT NOT NULL, owner_json TEXT NOT NULL\n );")}catch(e){throw this.db.close(),e}}host;read(e){const i=this.db.prepare("SELECT * FROM claims WHERE thread_id = ?").get(e);return void 0===i?void 0:l.parse(i)}checkOpen(){if(this.closed)throw new Error("Session claims are closed");if(r(this.databasePath)&&canonicalSessionDatabasePath(this.databasePath)!==this.databasePath)throw new SessionOccupiedError("",void 0,"ambiguous_library_alias")}reap(e){let i="unknown";try{i=this.host.probe(e.identity)}catch{}return"dead"===i&&(this.db.prepare("DELETE FROM claims WHERE thread_id = ? AND claim_id = ?").run(e.thread_id,e.claim_id),!0)}acquire(e){if(!e)throw new Error("Thread ID must be nonempty");return this.acquireKey(e)}acquireKey(i){this.checkOpen();const r=this.held.get(i);if(void 0!==r)return this.assert(r.claim),r.references++,this.handle(r.claim);const t={thread_id:i,claim_id:e(),identity:this.host.identity,owner_json:JSON.stringify(this.host.owner)};for(let e=0;e<32;e++){let e;this.db.exec("BEGIN IMMEDIATE");try{e=this.read(u)??(i===u?this.db.prepare("SELECT * FROM claims LIMIT 1").all().map(e=>l.parse(e))[0]:this.read(i)),void 0===e&&this.db.prepare("INSERT INTO claims VALUES (?, ?, ?, ?)").run(i,t.claim_id,t.identity,t.owner_json),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}if(void 0===e)return this.held.set(i,{claim:t,references:1}),this.handle(t);if(!this.reap(e))throw new SessionOccupiedError(i,e)}throw new SessionOccupiedError(i)}maintenance(){return this.acquireKey(u)}beginLibraryInitialization(e){this.checkOpen();for(let i=0;i<32;i++){let i;this.db.exec("BEGIN IMMEDIATE");try{const r=e();if(i=this.read(u)??(r?this.db.prepare("SELECT * FROM claims LIMIT 1").all().map(e=>l.parse(e))[0]:void 0),void 0===i){let e=!1;return()=>{e||(this.db.exec("COMMIT"),e=!0)}}this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}if(!this.reap(i))throw new SessionOccupiedError(u,i)}throw new SessionOccupiedError(u)}assert(e){if(this.checkOpen(),this.read(e.thread_id)?.claim_id!==e.claim_id)throw new SessionOccupiedError(e.thread_id,void 0,"stale_session_claim")}handle(e){let i=!1;return{threadId:e.thread_id,token:e.claim_id,assert:()=>{if(i)throw new SessionOccupiedError(e.thread_id,void 0,"stale_session_claim");this.assert(e)},release:()=>{if(i)return;i=!0;const r=this.held.get(e.thread_id);r?.claim.claim_id===e.claim_id&&(r.references--,0===r.references&&(this.db.prepare("DELETE FROM claims WHERE thread_id = ? AND claim_id = ?").run(e.thread_id,e.claim_id),this.held.delete(e.thread_id)))}}}close(){if(!this.closed){if(0!==this.held.size)throw new Error("Cannot close ownership store with unsettled session claims");this.db.close(),this.closed=!0}}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
export declare const SCHEDULER_SCHEMA_VERSION =
|
|
2
|
+
export declare const SCHEDULER_SCHEMA_VERSION = 10;
|
|
3
3
|
/** Connection-local compatibility marker, not an authorization or security boundary. */
|
|
4
4
|
export declare function registerSchedulerWriter(db: DatabaseSync): void;
|
|
5
5
|
/** user_version cannot fence a pre-upgrade connection; persisted triggers can. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SCHEDULER_SCHEMA_VERSION=
|
|
1
|
+
export const SCHEDULER_SCHEMA_VERSION=10;const e=`roll_scheduler_writer_v${String(10)}`;export function registerSchedulerWriter(r){r.exec(`ATTACH DATABASE ':memory:' AS ${e}`)}export function installSchedulerWriterGuards(r){for(const t of["schedules","invocations","schedule_thread_refs","schedule_extensions"])for(const E of["INSERT","UPDATE","DELETE"]){const s=`scheduler_writer_${t}_${E.toLowerCase()}`,o=`CREATE TRIGGER ${s} BEFORE ${E} ON ${t} WHEN NOT EXISTS (SELECT 1 FROM pragma_database_list WHERE name = '${e}') BEGIN SELECT RAISE(ABORT, 'scheduler writer version mismatch: restart Roll scheduler'); END`,c=r.prepare("SELECT sql FROM sqlite_schema WHERE type = 'trigger' AND name = ?").get(s);c?.sql!==o&&(void 0!==c&&r.exec(`DROP TRIGGER ${s}`),r.exec(o))}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{chmodSync as e,existsSync as t,mkdirSync as n}from"node:fs";import{isAbsolute as i,resolve as r}from"node:path";import{randomUUID as s}from"node:crypto";import{DatabaseSync as d}from"node:sqlite";import{expandTilde as a}from"../store/thread-store.js";import{SCHEDULER_LIMITS as o}from"./limits.js";import{openReadOnlySchedulerDatabase as u,enableSchedulerQueryOnly as c}from"./read-only-sqlite.js";import{SCHEDULER_SCHEMA_VERSION as l,registerSchedulerWriter as h,installSchedulerWriterGuards as E}from"./database-version.js";import{TRIGGER_KINDS as p,computeNextRunAtMs as _,computeFirstRunAtMs as m,parseTriggerJson as T,triggerSpecSchema as v}from"./trigger.js";import{CANCEL_INVOCATION_OUTCOMES as N,COMPLETE_INVOCATION_OUTCOMES as R,EXECUTOR_LIVENESS as L,INVOCATION_FAILURE_OUTCOMES as S,INVOCATION_LIVE_STATUSES as f,INVOCATION_MODES as g,INVOCATION_STATUSES as A,INVOCATION_TERMINAL_STATUSES as I,INVOCATION_TREE_LIVENESS as w,TRACKED_LEADER_STATES as x,SCHEDULE_STATUSES as D,SCHEDULE_STORE_ERROR_CODES as O,ScheduleStoreError as M}from"./types.js";const C=15e3,b=I.map(()=>"?").join(", "),y={dead:"dead",notDead:"not-dead",deferred:"deferred"},U={valid:"valid",invalid:"invalid"};function k(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(l)}`)}function H(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>l)throw k(t.user_version);const n=e.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name IN ('schedules', 'invocations')").all(),i=new Set(n.map(e=>e.name));if(t.user_version<1||!i.has("schedules")||!i.has("invocations"))throw new Error("not a valid authoritative scheduler database")}function F(e){if(!G(e.mode))throw new M(O.invalid,"Invalid reference mode");return{invocationId:e.invocation_id,attempt:e.attempt,scheduleId:e.schedule_id,threadId:e.thread_id,threadsDir:e.threads_dir,name:e.name,cwd:e.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:e.created_at}}function W(e){if(!Number.isSafeInteger(e.expectedAttempt)||e.expectedAttempt<1||0===e.threadId.trim().length||!i(e.threadsDir))throw new M(O.invalid,"会话关联需要有效的 attempt、会话 ID 和绝对存储路径")}const P=Object.values(g),B=Object.values(A);function G(e){return P.includes(e)}function X(e){return B.includes(e)}function $(e){try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}function q(e){if(null===e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(j):[]}catch{return[]}}function j(e){return"number"==typeof e&&Number.isInteger(e)&&e>0}function Y(e){return e===x.alive||e===x.exited||e===x.unknown}function K(e){return j(e)?{pgid:e,leaderState:x.unknown}:"object"==typeof e&&null!==e&&"pgid"in e&&j(e.pgid)&&"leaderState"in e&&Y(e.leaderState)?"startToken"in e?"string"==typeof e.startToken&&e.startToken.length>0?{pgid:e.pgid,leaderState:e.leaderState,startToken:e.startToken}:void 0:{pgid:e.pgid,leaderState:e.leaderState}:void 0}function V(e){if(null===e)return{kind:U.valid,groups:[]};try{const t=JSON.parse(e);if(!Array.isArray(t))return{kind:U.invalid};const n=new Map;for(const e of t){const t=K(e);if(void 0===t)return{kind:U.invalid};n.set(t.pgid,t)}return{kind:U.valid,groups:[...n.values()]}}catch{return{kind:U.invalid}}}function J(e){return new M(O.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function z(e,t){const n=V(e);if(n.kind===U.invalid)throw J(t);return n.groups}function Q(e){const t=new Map;for(const n of e)!Number.isInteger(n.pgid)||n.pgid<=0||t.set(n.pgid,void 0!==n.startToken&&n.startToken.length>0?{pgid:n.pgid,leaderState:n.leaderState,startToken:n.startToken}:{pgid:n.pgid,leaderState:n.leaderState});return JSON.stringify([...t.values()])}function Z(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const ee="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",te=[A.claimed,A.running,A.retry],ne=te;function ie(e){return null!==e.executor_pid&&null!==e.executor_start_token?{pid:e.executor_pid,startToken:e.executor_start_token}:void 0}function re(e){if(!G(e.mode)||!X(e.status))throw new M(O.invalid,`invocation ${e.id} 的 mode/status 非法: ${e.mode}/${e.status}`);const t=z(e.tree_tracked_pgids,e.id);return{id:e.id,scheduleId:e.schedule_id,mode:e.mode,status:e.status,scheduledForMs:e.scheduled_for,attempt:e.attempt,maxAttempts:e.max_attempts,executor:ie(e),claimedBy:e.claimed_by??void 0,leaseUntilMs:e.lease_until??void 0,retryAtMs:e.retry_at??void 0,threadId:e.thread_id??void 0,outputExcerpt:e.output_excerpt??void 0,error:e.error??void 0,pendingActions:$(e.pending_actions_json),treeTrackedPgids:t.map(e=>e.pgid),treeTrackedGroups:t,treeUnsettled:1===e.tree_unsettled,treeSurvivorPids:q(e.tree_survivor_pids),createdAtMs:e.created_at,startedAtMs:e.started_at??void 0,finishedAtMs:e.finished_at??void 0}}function se(e){return Object.values(D).some(t=>t===e)}function de(e,t){if(!se(e.status))throw new M(O.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);const n=oe(e.max_rounds??void 0),i=e.rounds_started??0;if("number"!=typeof i||!Number.isSafeInteger(i)||i<0)throw new M(O.invalid,`schedule ${e.id} 的 roundsStarted 非法`);const r=void 0===n?void 0:t.prepare("SELECT * FROM invocations WHERE schedule_id = ? AND mode = ? ORDER BY scheduled_for DESC, rowid DESC LIMIT 1").get(e.id,g.scheduled),s=void 0===r?void 0:{status:re(r).status,treeUnsettled:1===r.tree_unsettled||null!==r.tree_tracked_pgids&&"[]"!==r.tree_tracked_pgids.trim()};return{id:e.id,name:e.name,prompt:e.prompt,cwd:e.cwd,trigger:T(e.trigger_json),status:e.status,authorityDigest:e.authority_digest??void 0,maxRunMs:ae(e.max_run_ms),maxRounds:n,roundsStarted:i,...void 0===s?{}:{lastScheduledRun:s},nextRunAtMs:e.next_run_at??void 0,lastRunAtMs:e.last_run_at??void 0,lastError:e.last_error??void 0,createdAtMs:e.created_at,updatedAtMs:e.updated_at}}function ae(e){if(null!=e){if("number"!=typeof e||!Number.isInteger(e)||e<o.minMaxRunMs||e>o.maxRunCeilingMs)throw new M(O.invalid,`maxRunMs 必须是 ${String(o.minMaxRunMs)}..${String(o.maxRunCeilingMs)} 的整数毫秒`);return e}}function oe(e){if(void 0!==e){if("number"!=typeof e||!Number.isSafeInteger(e)||e<1)throw new M(O.invalid,"maxRounds 必须是正安全整数");return e}}function ue(e){const t=e.name.trim();if(0===t.length||t.length>o.maxNameChars)throw new M(O.invalid,`name 必须为 1..${String(o.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>o.maxPromptChars)throw new M(O.invalid,`prompt 必须为 1..${String(o.maxPromptChars)} 个字符`);if(!i(e.cwd))throw new M(O.invalid,"cwd 必须是绝对路径");const r=v.safeParse(e.trigger);if(!r.success)throw new M(O.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:r.data,maxRunMs:ae(e.maxRunMs),maxRounds:oe(e.maxRounds)}}export class ScheduleStore{db;maxSchedules;claimLeaseMs;retryBudget;retryBackoffMs;executorLiveness;treeLiveness;maxLivenessProbesPerClaim;livenessProbeDeferralMs;invocationRetentionPerSchedule;invocationRetentionMs;constructor(i,s={}){this.maxSchedules=s.maxSchedules??50,this.claimLeaseMs=s.claimLeaseMs??o.claimLeaseMs,this.retryBudget=s.retryBudget??o.retryBudget,this.retryBackoffMs=s.retryBackoffMs??o.retryBackoffMs,this.executorLiveness=s.executorLiveness??(()=>L.unknown),this.treeLiveness=s.treeLiveness,this.maxLivenessProbesPerClaim=s.maxLivenessProbesPerClaim??o.maxLivenessProbesPerClaim,this.livenessProbeDeferralMs=s.livenessProbeDeferralMs??o.livenessProbeDeferralMs,this.invocationRetentionPerSchedule=s.invocationRetentionPerSchedule??o.invocationRetentionPerSchedule,this.invocationRetentionMs=s.invocationRetentionMs??o.invocationRetentionMs;const h=a(i);if(!0===s.readOnly){const e=r(h,"schedules.db"),n=t(e);if(!n&&!0===s.requireExistingDatabase)throw new Error("authoritative scheduler database does not exist");this.db=n?u(e):new d(":memory:");try{if(n){const e=this.db.prepare("PRAGMA user_version").get()?.user_version;if(![5,6,7,8,l].includes(Number(e)))throw new Error(`scheduler schema v${String(e)} 不支持只读查询;请停止旧 daemon 后由新版 daemon 初始化迁移`)}else this.init(),c(this.db)}catch(e){throw this.db.close(),e}return}if(!0===s.requireExistingDatabase&&!t(h))throw new Error("authoritative scheduler database does not exist");t(h)||n(h,{recursive:!0,mode:448}),"win32"!==process.platform&&e(h,448);const E=r(h,"schedules.db");if(!0===s.requireExistingDatabase&&!t(E))throw new Error("authoritative scheduler database does not exist");if(this.db=new d(E),!0===s.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(C)}`),H(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{e(E,384)}catch(e){throw this.db.close(),e}try{this.init()}catch(e){throw this.db.close(),e}}init(){this.db.exec(`PRAGMA busy_timeout = ${String(C)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`),h(this.db);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>l)throw k(e.user_version);const t=e.user_version<7;t&&this.db.exec("PRAGMA foreign_keys = OFF;");try{this.transaction(()=>{const e=this.db.prepare("PRAGMA user_version").get().user_version;if(e>l)throw k(e);const t=e<7&&void 0!==this.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'schedules'").get();if(this.db.exec(`CREATE TABLE IF NOT EXISTS schedules (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'running', 'retry', 'completed', 'needs_confirmation', 'failed')),\n scheduled_for INTEGER NOT NULL,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT ${String(o.retryBudget)},\n executor_pid INTEGER,\n executor_start_token TEXT,\n executor_probed_at INTEGER,\n claimed_by TEXT,\n ownership_token TEXT,\n lease_until INTEGER,\n retry_at INTEGER,\n thread_id TEXT,\n output_excerpt TEXT,\n error TEXT,\n pending_actions_json TEXT NOT NULL DEFAULT '[]',\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER,\n tree_tracked_pgids TEXT,\n tree_unsettled INTEGER NOT NULL DEFAULT 0,\n tree_survivor_pids TEXT,\n UNIQUE (schedule_id, mode, scheduled_for)\n );\n CREATE INDEX IF NOT EXISTS idx_schedules_due\n ON schedules (next_run_at) WHERE status = 'active' AND next_run_at IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_invocations_live\n ON invocations (schedule_id) WHERE status IN ('pending', 'claimed', 'running', 'retry');\n CREATE TABLE IF NOT EXISTS schedule_thread_refs (\n invocation_id TEXT NOT NULL,\n attempt INTEGER NOT NULL CHECK (attempt > 0),\n schedule_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n threads_dir TEXT NOT NULL,\n name TEXT NOT NULL,\n cwd TEXT NOT NULL,\n scheduled_for INTEGER NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n created_at INTEGER NOT NULL,\n PRIMARY KEY (invocation_id, attempt)\n );\n CREATE INDEX IF NOT EXISTS idx_schedule_thread_refs_history\n ON schedule_thread_refs (schedule_id, scheduled_for DESC, invocation_id, attempt DESC);`),e<7&&this.addMissingColumns(),t&&this.migrateRoundsInTransaction(),this.db.exec("CREATE TABLE IF NOT EXISTS schedule_extensions (\n request_id TEXT PRIMARY KEY CHECK (length(request_id) BETWEEN 1 AND 128),\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n expected_max_rounds INTEGER NOT NULL CHECK (typeof(expected_max_rounds) = 'integer' AND expected_max_rounds BETWEEN 1 AND 9007199254740991),\n additional_rounds INTEGER NOT NULL CHECK (typeof(additional_rounds) = 'integer' AND additional_rounds BETWEEN 1 AND 9007199254740991),\n authority_digest TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE (schedule_id, expected_max_rounds)\n );"),t&&0!==this.db.prepare("PRAGMA foreign_key_check").all().length)throw new M(O.invalid,"scheduler 迁移失败:外键关联不完整");E(this.db),e!==l&&this.db.exec(`PRAGMA user_version = ${String(l)};`)})}finally{t&&this.db.exec("PRAGMA foreign_keys = ON;")}}migrateRoundsInTransaction(){this.db.exec("CREATE TABLE schedules_v7 (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n INSERT INTO schedules_v7 (rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at)\n SELECT rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at FROM schedules;\n DROP TABLE schedules;\n ALTER TABLE schedules_v7 RENAME TO schedules;\n CREATE INDEX idx_schedules_due ON schedules (next_run_at)\n WHERE status = 'active' AND next_run_at IS NOT NULL;")}addMissingColumns(){const e=[{table:"schedules",column:"authority_digest",definition:"TEXT"},{table:"schedules",column:"max_run_ms",definition:"INTEGER"},{table:"invocations",column:"max_attempts",definition:`INTEGER NOT NULL DEFAULT ${String(o.retryBudget)}`},{table:"invocations",column:"executor_pid",definition:"INTEGER"},{table:"invocations",column:"executor_start_token",definition:"TEXT"},{table:"invocations",column:"executor_probed_at",definition:"INTEGER"},{table:"invocations",column:"tree_tracked_pgids",definition:"TEXT"},{table:"invocations",column:"tree_unsettled",definition:"INTEGER NOT NULL DEFAULT 0"},{table:"invocations",column:"tree_survivor_pids",definition:"TEXT"}];for(const t of e){this.db.prepare(`PRAGMA table_info(${t.table})`).all().map(e=>e.name).includes(t.column)||this.db.exec(`ALTER TABLE ${t.table} ADD COLUMN ${t.column} ${t.definition}`)}this.clampOversizedIntervals()}clampOversizedIntervals(){const e=this.db.prepare("SELECT id, trigger_json FROM schedules").all();for(const t of e){let e;try{e=JSON.parse(t.trigger_json)}catch{continue}"object"!=typeof e||null===e||!("kind"in e)||e.kind!==p.interval||!("everyMs"in e)||"number"!=typeof e.everyMs||e.everyMs<=o.maxIntervalMs||this.db.prepare("UPDATE schedules SET trigger_json = ?, status = ?, last_error = ?, updated_at = ?\n WHERE id = ?").run(JSON.stringify({kind:p.interval,everyMs:o.maxIntervalMs}),D.paused,`原间隔 ${String(e.everyMs)} ms 超过上限 365 天,已按 365 天登记并暂停;确认后 roll schedule resume`,Date.now(),t.id)}}transaction(e){this.db.exec("BEGIN IMMEDIATE");try{const t=e();return this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}createSchedule(e,t){const n=ue(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t??Date.now()))}createScheduleIdempotent(e,t){const n=ue(e),i=String(o.maxRunMs);return this.transaction(()=>{const r=t??Date.now(),s=this.db.prepare(`SELECT * FROM schedules\n WHERE status = ? AND prompt = ? AND cwd = ? AND trigger_json = ?\n AND COALESCE(max_run_ms, ${i}) = COALESCE(?, ${i})\n AND max_rounds IS ?`).get(D.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??null,n.maxRounds??null);if(void 0!==s){const t=e.authorityDigest??null;return(s.authority_digest??null)!==t?(this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,r,s.id),{created:!1,reauthorized:!0,schedule:this.requireSchedule(s.id)}):{created:!1,reauthorized:!1,schedule:de(s,this.db)}}return{created:!0,reauthorized:!1,schedule:this.insertScheduleInTransaction(n,e,r)}})}insertScheduleInTransaction(e,t,n){if(this.db.prepare("SELECT COUNT(*) AS count FROM schedules").get().count>=this.maxSchedules)throw new M(O.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);if(!0===t.fireImmediately&&("interval"!==e.trigger.kind||void 0!==e.trigger.startAtMs))throw new M(O.invalid,"立即触发不能与日历规则或 startAt 同时使用");const i=s(),r=!0===t.fireImmediately?n:m(e.trigger,n);if(void 0!==t.expectedFirstRunAtMs&&r!==t.expectedFirstRunAtMs)throw new M(O.invalid,"首次运行时间在确认后已变化,请重新发起确认");return this.db.prepare("INSERT INTO schedules\n (id, name, prompt, cwd, trigger_json, status, authority_digest, max_run_ms,\n next_run_at, created_at, updated_at, max_rounds)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),D.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n,e.maxRounds??null),this.requireSchedule(i)}setAuthorityDigest(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes}resumeSchedule(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(i?.status===D.completed)throw new M(O.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");const r=this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(D.active,t,n,e);return this.reconcileRoundsInTransaction(e,n),1===r.changes})}getScheduleExtension(e){const t=this.db.prepare("SELECT * FROM schedule_extensions WHERE request_id = ?").get(e);return void 0===t?void 0:{requestId:t.request_id,scheduleId:t.schedule_id,expectedMaxRounds:t.expected_max_rounds,additionalRounds:t.additional_rounds,authorityDigest:t.authority_digest,createdAtMs:t.created_at}}extendSchedule(e,t=Date.now()){const n=oe(e.additionalRounds),i=oe(e.expectedMaxRounds);if(void 0===n||void 0===i||!Number.isSafeInteger(i+n))throw new M(O.invalid,"追加轮数及原上限必须为正安全整数,追加后的总轮数不得溢出");if("string"!=typeof e.requestId||0===e.requestId.trim().length||e.requestId.length>128)throw new M(O.invalid,"requestId 必须为 1..128 字符的非空请求标识;重试时复用原标识");if("string"!=typeof e.authorityDigest||0===e.authorityDigest.trim().length)throw new M(O.invalid,"追加轮数必须重新核验工作区权限");return this.transaction(()=>{const r=this.getScheduleExtension(e.requestId);if(void 0!==r){if(r.scheduleId!==e.scheduleId||r.expectedMaxRounds!==i||r.additionalRounds!==n)throw new M(O.invalid,"requestId 已用于不同的追加请求;请检查原请求参数");return{extended:!1,schedule:this.requireSchedule(e.scheduleId)}}const s=this.requireSchedule(e.scheduleId);if(void 0===s.maxRounds)throw new M(O.invalid,"不限轮数的任务不需要追加额度");if(s.maxRounds!==i)throw new M(O.invalid,"任务轮数上限已变化,追加请求已过期;请刷新任务并核对原请求结果");if(s.status!==D.completed||s.roundsStarted!==s.maxRounds)throw new M(O.invalid,"仅能为已结束且轮数用尽的有限任务追加轮数");if(void 0!==this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND\n (status IN ('pending', 'claimed', 'running', 'retry') OR tree_unsettled = 1\n OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]')) LIMIT 1").get(s.id))throw new M(O.invalid,"任务仍有待执行、运行中或未清场的记录,暂不能追加轮数;请先完成运行收尾");const d=_(s.trigger,t);if(!Number.isSafeInteger(t)||!Number.isSafeInteger(d)||!Number.isFinite(new Date(d).getTime()))throw new M(O.invalid,"追加时间或下次执行时间超出有效范围");return this.db.prepare("INSERT INTO schedule_extensions (request_id, schedule_id, expected_max_rounds,\n additional_rounds, authority_digest, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(e.requestId,s.id,i,n,e.authorityDigest,t),this.db.prepare("UPDATE schedules SET max_rounds = ?, authority_digest = ?, status = ?,\n next_run_at = ?, updated_at = ? WHERE id = ?").run(i+n,e.authorityDigest,D.active,d,t,s.id),{extended:!0,schedule:this.requireSchedule(s.id)}})}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?de(t,this.db):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(e=>de(e,this.db))}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(void 0===i)return!1;if(i.status===D.completed){if(t===D.active)throw new M(O.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");return!0}if(t===D.completed)throw new M(O.invalid,"结束状态由调度器在轮数用尽且运行结算后设置");if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===D.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,g.scheduled,A.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return this.reconcileRoundsInTransaction(e,n),!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(A.running).map(re)}listActiveWorkerInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status IN (?, ?)\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC").all(A.claimed,A.running).map(re)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${ee}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...te).map(re)}prepareWorkerShutdown(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT id FROM invocations WHERE claimed_by = ? AND status = ?").all(e,A.claimed);for(const e of i)this.finishInvocationAsFailedInTransaction(e.id,t,n);return this.db.prepare("SELECT * FROM invocations WHERE claimed_by = ? AND status = ?\n ORDER BY started_at ASC, created_at ASC").all(e,A.running).map(e=>{if(null===e.ownership_token)throw new M(O.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new M(O.invalid,`running invocation ${e.id} 无法加载 worker claim`);return t})})}removeSchedule(e,t={}){return this.transaction(()=>{if(!0!==t.abandon){const t=this.db.prepare(`SELECT id, status FROM invocations WHERE schedule_id = ? AND ${ee} LIMIT 1`).get(e,...te);if(void 0!==t)throw new M(O.invalid,`定时任务 ${e} 仍有未结束的运行(invocation ${t.id},${t.status});先 cancel --kill 清场,或显式 --abandon 放弃追踪`)}return 1===this.db.prepare("DELETE FROM schedules WHERE id = ?").run(e).changes})}enqueueManualInvocation(e,t=Date.now(),n={}){const i=Math.max(1,Math.floor(n.maxAttempts??this.retryBudget));return this.transaction(()=>{this.requireSchedule(e);const n=s();for(let r=0;r<1e3;r+=1){if(1===this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, created_at)\n VALUES (?, ?, ?, ?, ?, 0, ?, ?)").run(n,e,g.manual,A.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new M(O.invalid,`schedule ${e} 的手动触发入队失败`)})}claimPendingInvocation(e,t,n=Date.now()){return this.transaction(()=>{const i=s();return 1===this.db.prepare(`UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?, attempt = 1,\n retry_at = NULL\n WHERE id = ? AND status = ?\n AND NOT EXISTS (\n SELECT 1 FROM invocations o\n WHERE o.schedule_id = (SELECT schedule_id FROM invocations WHERE id = ?)\n AND o.id != ? AND ${ee})`).run(A.claimed,t,i,n+this.claimLeaseMs,e,A.pending,e,e,...te).changes?this.loadClaim(e,i):void 0})}findLiveRun(e){const t=this.db.prepare(`SELECT * FROM invocations WHERE schedule_id = ? AND ${ee}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...te);return t?re(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,A.pending).changes}cancelInvocation(e,t,n=Date.now(),i={}){return this.transaction(()=>{const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return this.cancelInvocationInTransaction(r,t,n,i)})}finalizeCancellation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id),n=this.revisionMismatch(t,{...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}});if(void 0!==n)return n;if(!0!==e.abandon&&void 0!==t&&V(t.tree_tracked_pgids).kind===U.invalid)return N.treeUnsettled;const i=void 0===t?void 0:this.executorBlocksCancel(t,!0===e.abandon);if(void 0!==i)return i;if(void 0!==e.tree&&void 0!==t&&ne.includes(t.status)){if(!this.writeInvocationTreeInTransaction({id:e.id,unsettled:e.tree.unsettled,...void 0!==e.tree.trackedGroups?{trackedGroups:e.tree.trackedGroups}:{},...void 0!==e.tree.survivorPids?{survivorPids:e.tree.survivorPids}:{},expectedAttempt:e.expectedAttempt}))return N.ownershipChanged}const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);return this.cancelInvocationInTransaction(r,e.reason,e.nowMs,{...!0===e.abandon?{abandon:!0}:{},expectedAttempt:e.expectedAttempt,...void 0!==e.expectedOwnershipToken?{expectedOwnershipToken:e.expectedOwnershipToken}:{},...void 0!==e.expectedClaimedBy?{expectedClaimedBy:e.expectedClaimedBy}:{}},{executorChecked:!0})})}executorBlocksCancel(e,t){if(e.status!==A.running||t)return;const n=ie(e),i=void 0===n?L.unknown:this.executorLiveness(n);return i===L.alive||i===L.descendants?N.executorAlive:i===L.unknown?N.executorUnknown:void 0}revisionMismatch(e,t){return void 0!==e&&X(e.status)?void 0!==t.expectedOwnershipToken&&e.ownership_token!==t.expectedOwnershipToken||void 0!==t.expectedAttempt&&e.attempt!==t.expectedAttempt||void 0!==t.expectedClaimedBy&&(e.claimed_by??void 0)!==t.expectedClaimedBy?N.ownershipChanged:void 0:N.notFound}cancelInvocationInTransaction(e,t,n,i,r={}){const s=this.revisionMismatch(e,i);if(void 0!==s)return s;if(void 0===e||!X(e.status))return N.notFound;if(!f.includes(e.status))return N.terminal;const d=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==d)return d;const a=!0===i.abandon;return!a&&this.treeBlocksTerminal(e)?N.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:a})?N.cancelled:N.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===A.retry?"retry_at":"lease_until";this.db.prepare(`UPDATE invocations SET ${n} = ? WHERE id = ?`).run(t+this.claimLeaseMs,e.id)}pauseInvalidScheduleInTransaction(e,t,n){this.db.prepare("UPDATE schedules SET status = ?, last_error = ?, updated_at = ? WHERE id = ?").run(D.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return de(e,this.db)}catch(n){return void this.pauseInvalidScheduleInTransaction(e.id,n,t)}}loadScheduleForClaimInTransaction(e,t){const n=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return void 0===n?void 0:this.decodeScheduleForClaimInTransaction(n,t)}hasOtherLiveRunInTransaction(e,t){return void 0!==this.db.prepare(`SELECT 1 AS present FROM invocations\n WHERE schedule_id = ? AND id != ? AND ${ee} LIMIT 1`).get(e,t,...te)}claimDue(e){const t=e.heldInvocationIds??new Set;return this.transaction(()=>{const n=[],i=this.db.prepare("SELECT i.*, s.status AS schedule_status FROM invocations i\n JOIN schedules s ON s.id = i.schedule_id\n WHERE i.status = ?\n OR (i.status = ? AND i.retry_at IS NOT NULL AND i.retry_at <= ?)\n OR (i.status IN (?, ?) AND i.lease_until IS NOT NULL AND i.lease_until <= ?)\n ORDER BY i.scheduled_for ASC, i.created_at ASC").all(A.pending,A.retry,e.nowMs,A.claimed,A.running,e.nowMs),r=[],d=i.filter(e=>e.status===A.running&&!t.has(e.id)&&void 0!==ie(e));d.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const a=new Map;let o=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of d){const n=ie(t);void 0!==n&&(o<=0?a.set(t.id,y.deferred):(o-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),a.set(t.id,this.executorLiveness(n)===L.dead?y.dead:y.notDead)))}for(const n of i){const i=n.status===A.claimed||n.status===A.running,s=a.get(n.id);!i||!t.has(n.id)&&s!==y.notDead?s!==y.deferred?r.push(n):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.livenessProbeDeferralMs,n.id):this.db.prepare("UPDATE invocations SET lease_until = ? WHERE id = ?").run(e.nowMs+this.claimLeaseMs,n.id)}if(e.limit<=0)return n;for(const t of r){if(n.length>=e.limit)break;if(V(t.tree_tracked_pgids).kind===U.invalid){this.deferLiveRowInTransaction(t,e.nowMs);continue}const i=this.loadScheduleForClaimInTransaction(t.schedule_id,e.nowMs);if(void 0===i){t.status!==A.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===g.scheduled&&i.status===D.paused){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.finishInvocationAsFailedInTransaction(t.id,"任务已暂停,放弃本次运行",e.nowMs);continue}if(this.hasOtherLiveRunInTransaction(t.schedule_id,t.id))continue;const r=t.status===A.pending?1:t.attempt+1;if(r>t.max_attempts){if(this.treeBlocksTerminal(t)){this.deferLiveRowInTransaction(t,e.nowMs);continue}this.markTerminalFailureInTransaction(t,`重试预算耗尽(共 ${String(t.max_attempts)} 次尝试):${t.error??"exec 未成功完成"}`,e.nowMs);continue}const d=s();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(A.claimed,e.workerId,d,e.nowMs+this.claimLeaseMs,r,t.id);const a=this.loadClaim(t.id,d);void 0!==a&&n.push(a)}if(n.length>=e.limit)return n;const u=this.db.prepare("SELECT s.* FROM schedules s\n WHERE s.status = ? AND s.next_run_at IS NOT NULL AND s.next_run_at <= ?\n AND (s.max_rounds IS NULL OR s.rounds_started < s.max_rounds)\n AND NOT EXISTS (\n SELECT 1 FROM invocations i\n WHERE i.schedule_id = s.id AND i.status IN (?, ?, ?, ?))\n ORDER BY s.next_run_at ASC, s.created_at ASC\n LIMIT ?").all(D.active,e.nowMs,A.pending,A.claimed,A.running,A.retry,this.maxSchedules);for(const t of u){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=s(),d=s(),a=this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, claimed_by,\n ownership_token, lease_until, created_at)\n VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)").run(r,t.id,g.scheduled,A.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,d,e.nowMs+this.claimLeaseMs,e.nowMs),o=Number(a.changes);if(this.db.prepare("UPDATE schedules SET rounds_started = rounds_started + ?,\n next_run_at = CASE WHEN max_rounds IS NOT NULL AND rounds_started + ? >= max_rounds\n THEN NULL ELSE ? END, updated_at = ? WHERE id = ?").run(o,o,_(i.trigger,e.nowMs),e.nowMs,t.id),1===a.changes){const e=this.loadClaim(r,d);void 0!==e&&n.push(e)}}return n})}beginInvocation(e,t,n=Date.now(),i){return 1===this.db.prepare("UPDATE invocations\n SET status = ?, started_at = ?, lease_until = ?, executor_pid = ?,\n executor_start_token = ?, executor_probed_at = NULL\n WHERE id = ? AND ownership_token = ? AND status = ?").run(A.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,A.claimed).changes?this.loadClaim(e,t):void 0}renewLease(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE invocations SET lease_until = ?\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n+this.claimLeaseMs,e,t,A.claimed,A.running).changes}completeInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.ownership_token!==e.ownershipToken||t.status!==A.claimed&&t.status!==A.running)return R.lostClaim;if(this.treeBlocksTerminal(t))return R.treeUnsettled;return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, thread_id = ?, output_excerpt = ?, pending_actions_json = ?,\n error = NULL, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(e.status,e.threadId??null,e.outputExcerpt??null,JSON.stringify(e.pendingActions??[]),e.nowMs,e.id,e.ownershipToken,A.claimed,A.running).changes?R.lostClaim:(this.db.prepare("UPDATE schedules SET last_run_at = ?, last_error = NULL, updated_at = ?\n WHERE id = (SELECT schedule_id FROM invocations WHERE id = ?)").run(e.nowMs,e.nowMs,e.id),t.mode===g.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),R.written)})}reclassifyTimedOutInvocation(e){return this.transaction(()=>{const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e.id);if(void 0===t||t.attempt!==e.expectedAttempt||null!==t.finished_at&&t.finished_at<e.timedOutAtMs||t.status!==A.completed&&t.status!==A.needsConfirmation)return S.lostClaim;if(this.treeBlocksTerminal(t))return S.treeUnsettled;if(t.attempt>=t.max_attempts){return 1!==this.db.prepare("UPDATE invocations\n SET thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(e.id,e.expectedAttempt,A.completed,A.needsConfirmation).changes?S.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===g.scheduled&&this.getSchedule(t.schedule_id)?.status===D.paused?S.terminalPaused:S.terminal)}return 1!==this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, finished_at = NULL,\n claimed_by = NULL, ownership_token = NULL, lease_until = NULL,\n thread_id = NULL, output_excerpt = NULL, pending_actions_json = '[]'\n WHERE id = ? AND attempt = ? AND status IN (?, ?)").run(A.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,A.completed,A.needsConfirmation).changes?S.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),t.mode===g.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),S.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?Q(t):null,i=Z(e.unsettled?e.survivorPids??[]:[]),r=e.unsettled?1:0,s=e.error??null;if(void 0!==e.ownershipToken){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND ownership_token = ? AND status IN (?, ?)").run(n,r,i,s,e.id,e.ownershipToken,A.claimed,A.running).changes}if(void 0!==e.expectedAttempt){return 1===this.db.prepare("UPDATE invocations\n SET tree_tracked_pgids = ?, tree_unsettled = ?, tree_survivor_pids = ?,\n error = COALESCE(?, error)\n WHERE id = ? AND attempt = ? AND status IN (?, ?, ?)").run(n,r,i,s,e.id,e.expectedAttempt,A.claimed,A.running,A.retry).changes}return!1}failInvocation(e,t,n,i=Date.now(),r={}){return this.transaction(()=>{const s=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0===s||s.ownership_token!==t||s.status!==A.claimed&&s.status!==A.running?S.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?S.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===g.scheduled&&this.getSchedule(s.schedule_id)?.status===D.paused?S.terminalPaused:S.terminal):(this.db.prepare("UPDATE invocations\n SET status = ?, retry_at = ?, error = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL\n WHERE id = ?").run(A.retry,i+this.retryBackoffMs,n,e),S.retryScheduled)})}registerThreadReference(e,t=Date.now()){return W(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations\n WHERE id = ? AND attempt = ? AND ownership_token = ? AND status = ?").get(e.invocationId,e.expectedAttempt,e.ownershipToken,A.running);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}backfillThreadReference(e,t=Date.now()){return W(e),this.transaction(()=>{const n=this.db.prepare("SELECT * FROM invocations WHERE id = ? AND attempt = ? AND thread_id = ?").get(e.invocationId,e.expectedAttempt,e.threadId);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}insertThreadReference(e,t,n){const i=this.db.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? AND attempt = ?").get(t.invocationId,t.expectedAttempt);if(void 0!==i){if(i.thread_id!==t.threadId||i.threads_dir!==t.threadsDir)throw new M(O.invalid,`invocation ${t.invocationId} attempt ${String(t.expectedAttempt)} 已关联其他会话`);return F(i)}const r=this.getSchedule(e.schedule_id);if(void 0!==r){if(!G(e.mode))throw new M(O.invalid,"Invalid invocation mode");return this.db.prepare("INSERT INTO schedule_thread_refs\n (invocation_id, attempt, schedule_id, thread_id, threads_dir, name, cwd,\n scheduled_for, mode, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(t.invocationId,t.expectedAttempt,r.id,t.threadId,t.threadsDir,r.name,r.cwd,e.scheduled_for,e.mode,n),{invocationId:t.invocationId,attempt:t.expectedAttempt,scheduleId:r.id,threadId:t.threadId,threadsDir:t.threadsDir,name:r.name,cwd:r.cwd,scheduledForMs:e.scheduled_for,mode:e.mode,createdAtMs:n}}}getInvocation(e){const t=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return t?re(t):void 0}listInvocations(e,t=20){if(t<=0)return[];return this.db.prepare("SELECT * FROM invocations WHERE schedule_id = ?\n ORDER BY created_at DESC, rowid DESC LIMIT ?").all(e,t).map(re)}pruneInvocations(e=Date.now()){return this.transaction(()=>{const t=this.db.prepare(`DELETE FROM invocations\n WHERE status IN (${b})\n AND finished_at IS NOT NULL AND finished_at < ?`).run(...I,e-this.invocationRetentionMs),n=this.db.prepare(`DELETE FROM invocations WHERE id IN (\n SELECT id FROM (\n SELECT id, ROW_NUMBER() OVER (\n PARTITION BY schedule_id\n ORDER BY COALESCE(finished_at, created_at) DESC, rowid DESC) AS rn\n FROM invocations WHERE status IN (${b}))\n WHERE rn > ?)`).run(...I,this.invocationRetentionPerSchedule);return Number(t.changes)+Number(n.changes)})}nextWakeAtMs(){return this.db.prepare("SELECT MIN(t) AS wake FROM (\n SELECT MIN(next_run_at) AS t FROM schedules\n WHERE status = ? AND next_run_at IS NOT NULL\n AND (max_rounds IS NULL OR rounds_started < max_rounds)\n UNION ALL SELECT MIN(retry_at) FROM invocations WHERE status = ?\n UNION ALL SELECT MIN(lease_until) FROM invocations WHERE status IN (?, ?)\n UNION ALL SELECT MIN(scheduled_for) FROM invocations WHERE status = ?)").get(D.active,A.retry,A.claimed,A.running,A.pending).wake??void 0}treeBlocksTerminal(e){const t=V(e.tree_tracked_pgids);return t.kind===U.invalid||(1===e.tree_unsettled||0!==t.groups.length)&&(void 0===this.treeLiveness||this.treeLiveness(re(e))!==w.settled)}reconcileRoundsInTransaction(e,t){this.db.prepare("UPDATE schedules\n SET status = CASE\n WHEN NOT EXISTS (SELECT 1 FROM invocations i WHERE i.schedule_id = schedules.id AND i.mode = 'scheduled'\n AND (i.status IN ('pending', 'claimed', 'running', 'retry') OR i.tree_unsettled = 1\n OR (i.tree_tracked_pgids IS NOT NULL AND trim(i.tree_tracked_pgids) != '[]')))\n THEN 'completed'\n WHEN status = 'completed' THEN 'active'\n ELSE status END,\n next_run_at = NULL, updated_at = ?\n WHERE id = ? AND max_rounds IS NOT NULL AND rounds_started >= max_rounds").run(t,e)}finishInvocationAsFailedInTransaction(e,t,n,i={}){const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0!==r&&((!0===i.force||!this.treeBlocksTerminal(r))&&(this.db.prepare("UPDATE invocations\n SET status = ?, error = ?, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ?").run(A.failed,t,n,e),r.mode===g.scheduled&&this.reconcileRoundsInTransaction(r.schedule_id,n),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===g.scheduled?this.db.prepare("UPDATE schedules SET status = CASE WHEN status = 'completed' THEN status ELSE ? END, last_error = ?, updated_at = ? WHERE id = ?").run(D.paused,t,n,e.schedule_id):this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(t,n,e.schedule_id))}loadClaim(e,t){const n=this.getInvocation(e);if(void 0===n)return;const i=this.getSchedule(n.scheduleId);return void 0!==i?{invocation:n,schedule:i,ownershipToken:t}:void 0}requireInvocation(e){const t=this.getInvocation(e);if(void 0===t)throw new M(O.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new M(O.notFound,`schedule ${e} 不存在`);return t}}export const SCHEDULE_LEDGER_READ_STATUSES={ok:"ok",empty:"empty",migrationRequired:"migration-required"};export function readScheduleLedger(e){const n=r(a(e),"schedules.db");if(!t(n))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=u(n);let s=!1;try{i.exec(`PRAGMA busy_timeout = ${String(C)}; BEGIN;`),s=!0;const e=i.prepare("PRAGMA user_version").get();if(![5,6,7,8,l].includes(e.user_version))return{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version,schedules:[]};const t=i.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all();return{status:SCHEDULE_LEDGER_READ_STATUSES.ok,schedules:t.map(e=>de(e,i))}}finally{try{s&&i.exec("ROLLBACK")}finally{i.close()}}}function ce(e,n){const i=r(a(e),"schedules.db");if(!t(i))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty};const s=u(i);let d=!1;try{s.exec(`PRAGMA busy_timeout = ${String(C)}; BEGIN;`),d=!0;const e=s.prepare("PRAGMA user_version").get();return[5,6,7,8,l].includes(e.user_version)?{status:SCHEDULE_LEDGER_READ_STATUSES.ok,value:n(s,e.user_version>=6)}:{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version}}finally{try{d&&s.exec("ROLLBACK")}finally{s.close()}}}export function readScheduleOccupancy(e){const t=ce(e,e=>e.prepare(`SELECT * FROM invocations WHERE ${ee}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...te).map(re));if(t.status===SCHEDULE_LEDGER_READ_STATUSES.migrationRequired)throw new Error(`scheduler occupancy 无法读取 schema v${String(t.schemaVersion)},拒绝把未知占用当成空闲`);return t.value??[]}function le(e,t,n){const i=e.prepare("SELECT * FROM invocations WHERE id = ?").get(t),r=n?e.prepare("SELECT * FROM schedule_thread_refs WHERE invocation_id = ? ORDER BY attempt ASC").all(t).map(F):[],s=void 0===i?void 0:re(i),d=s??r[0];return void 0===d?void 0:{invocationId:t,scheduleId:d.scheduleId,scheduledForMs:d.scheduledForMs,mode:d.mode,invocation:s,references:r}}function he(e){return"SELECT id, schedule_id, scheduled_for FROM invocations"+(e?" UNION SELECT invocation_id AS id, schedule_id, scheduled_for FROM schedule_thread_refs":"")}function Ee(e,t){const n=e.prepare("SELECT * FROM schedules").all().map(t=>de(t,e)),i=new Map(n.map(e=>[e.id,e])),r=e.prepare("SELECT id FROM schedules"+(t?" UNION SELECT schedule_id AS id FROM schedule_thread_refs":"")).all(),s=[];for(const{id:n}of r){const r=e.prepare(`SELECT id FROM (${he(t)})\n WHERE schedule_id = ? ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(n),d=void 0===r?void 0:le(e,r.id,t),a=i.get(n),o=d?.references.at(-1),u=a??o;void 0!==u&&s.push({scheduleId:n,name:u.name,cwd:u.cwd,schedule:a,latestRun:d})}return s.sort((e,t)=>Number(void 0===e.schedule)-Number(void 0===t.schedule)||(t.latestRun?.scheduledForMs??t.schedule?.createdAtMs??0)-(e.latestRun?.scheduledForMs??e.schedule?.createdAtMs??0)||e.scheduleId.localeCompare(t.scheduleId))}export function readScheduleHistory(e,t={}){const n=t.limit??20,i=t.offset??0;if(!Number.isSafeInteger(n)||n<1||n>100||!Number.isSafeInteger(i)||i<0)throw new RangeError("History limit must be 1..100 and offset must be nonnegative integers");const r=ce(e,(e,r)=>{const s=void 0===t.scheduleId?"":"WHERE schedule_id = ?",d=e.prepare(`SELECT id FROM (${he(r)}) ${s}\n ORDER BY scheduled_for DESC, id DESC LIMIT ? OFFSET ?`),a=void 0===t.scheduleId?[n+1,i]:[t.scheduleId,n+1,i],o=d.all(...a),u=[];for(const{id:t}of o.slice(0,n)){const n=le(e,t,r);void 0!==n&&u.push(n)}return{tasks:Ee(e,r),runs:u,hasMore:o.length>n}});return{status:r.status,...void 0===r.schemaVersion?{}:{schemaVersion:r.schemaVersion},tasks:r.value?.tasks??[],runs:r.value?.runs??[],hasMore:r.value?.hasMore??!1}}export function readScheduleRun(e,t){const n=ce(e,(e,n)=>le(e,t,n));return{status:n.status,...void 0===n.schemaVersion?{}:{schemaVersion:n.schemaVersion},run:n.value}}
|
|
1
|
+
import{scheduleHostContextSchema as e}from"../host-contracts/schedule-context.js";import{chmodSync as t,existsSync as n,mkdirSync as i}from"node:fs";import{isAbsolute as r,resolve as s}from"node:path";import{randomUUID as d}from"node:crypto";import{DatabaseSync as a}from"node:sqlite";import{expandTilde as o}from"../store/thread-store.js";import{SCHEDULER_LIMITS as u}from"./limits.js";import{openReadOnlySchedulerDatabase as c,enableSchedulerQueryOnly as l}from"./read-only-sqlite.js";import{SCHEDULER_SCHEMA_VERSION as h,registerSchedulerWriter as E,installSchedulerWriterGuards as p}from"./database-version.js";import{TRIGGER_KINDS as _,computeNextRunAtMs as m,computeFirstRunAtMs as T,parseTriggerJson as v,triggerSpecSchema as N}from"./trigger.js";import{CANCEL_INVOCATION_OUTCOMES as R,COMPLETE_INVOCATION_OUTCOMES as L,EXECUTOR_LIVENESS as S,INVOCATION_FAILURE_OUTCOMES as f,INVOCATION_LIVE_STATUSES as g,INVOCATION_MODES as A,INVOCATION_STATUSES as I,INVOCATION_TERMINAL_STATUSES as x,INVOCATION_TREE_LIVENESS as w,TRACKED_LEADER_STATES as D,SCHEDULE_STATUSES as O,SCHEDULE_STORE_ERROR_CODES as M,ScheduleStoreError as C}from"./types.js";const b=15e3,y=x.map(()=>"?").join(", "),U={dead:"dead",notDead:"not-dead",deferred:"deferred"},k={valid:"valid",invalid:"invalid"};function H(e){return new Error(`ScheduleStore schema v${String(e)} 高于当前支持的 v${String(h)}`)}function F(e){const t=e.prepare("PRAGMA user_version").get();if(t.user_version>h)throw H(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 W(e){if(!X(e.mode))throw new C(M.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 P(e){if(!Number.isSafeInteger(e.expectedAttempt)||e.expectedAttempt<1||0===e.threadId.trim().length||!r(e.threadsDir))throw new C(M.invalid,"会话关联需要有效的 attempt、会话 ID 和绝对存储路径")}const B=Object.values(A),G=Object.values(I);function X(e){return B.includes(e)}function $(e){return G.includes(e)}function q(e){try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e):[]}catch{return[]}}function j(e){if(null===e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(Y):[]}catch{return[]}}function Y(e){return"number"==typeof e&&Number.isInteger(e)&&e>0}function K(e){return e===D.alive||e===D.exited||e===D.unknown}function V(e){return Y(e)?{pgid:e,leaderState:D.unknown}:"object"==typeof e&&null!==e&&"pgid"in e&&Y(e.pgid)&&"leaderState"in e&&K(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 J(e){if(null===e)return{kind:k.valid,groups:[]};try{const t=JSON.parse(e);if(!Array.isArray(t))return{kind:k.invalid};const n=new Map;for(const e of t){const t=V(e);if(void 0===t)return{kind:k.invalid};n.set(t.pgid,t)}return{kind:k.valid,groups:[...n.values()]}}catch{return{kind:k.invalid}}}function z(e){return new C(M.invalid,`invocation ${e} 的进程树所有权元数据无效;拒绝按空树继续,仅可显式 roll schedule cancel ${e} --abandon 放弃追踪`)}function Q(e,t){const n=J(e);if(n.kind===k.invalid)throw z(t);return n.groups}function Z(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 ee(e){return JSON.stringify([...new Set(e.filter(e=>Number.isInteger(e)&&e>0))])}const te="(status IN (?, ?) OR (status = ? AND (tree_unsettled = 1 OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]'))))",ne=[I.claimed,I.running,I.retry],ie=ne;function re(e){return null!==e.executor_pid&&null!==e.executor_start_token?{pid:e.executor_pid,startToken:e.executor_start_token}:void 0}function se(e){if(!X(e.mode)||!$(e.status))throw new C(M.invalid,`invocation ${e.id} 的 mode/status 非法: ${e.mode}/${e.status}`);const t=Q(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:re(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:q(e.pending_actions_json),treeTrackedPgids:t.map(e=>e.pgid),treeTrackedGroups:t,treeUnsettled:1===e.tree_unsettled,treeSurvivorPids:j(e.tree_survivor_pids),createdAtMs:e.created_at,startedAtMs:e.started_at??void 0,finishedAtMs:e.finished_at??void 0}}function de(e){return Object.values(O).some(t=>t===e)}function ae(e,t){if(!de(e.status))throw new C(M.invalid,`schedule ${e.id} 的 status 非法: ${e.status}`);const n=ue(e.max_rounds??void 0),i=e.rounds_started??0;if("number"!=typeof i||!Number.isSafeInteger(i)||i<0)throw new C(M.invalid,`schedule ${e.id} 的 roundsStarted 非法`);const r=void 0===n?void 0:t.prepare("SELECT * FROM invocations WHERE schedule_id = ? AND mode = ? ORDER BY scheduled_for DESC, rowid DESC LIMIT 1").get(e.id,A.scheduled),s=void 0===r?void 0:{status:se(r).status,treeUnsettled:1===r.tree_unsettled||null!==r.tree_tracked_pgids&&"[]"!==r.tree_tracked_pgids.trim()};return{id:e.id,name:e.name,prompt:e.prompt,cwd:e.cwd,trigger:v(e.trigger_json),status:e.status,authorityDigest:e.authority_digest??void 0,...null==e.host_context?{}:{hostContext:ce(e.host_context)},maxRunMs:oe(e.max_run_ms),maxRounds:n,roundsStarted:i,...void 0===s?{}:{lastScheduledRun:s},nextRunAtMs:e.next_run_at??void 0,lastRunAtMs:e.last_run_at??void 0,lastError:e.last_error??void 0,createdAtMs:e.created_at,updatedAtMs:e.updated_at}}function oe(e){if(null!=e){if("number"!=typeof e||!Number.isInteger(e)||e<u.minMaxRunMs||e>u.maxRunCeilingMs)throw new C(M.invalid,`maxRunMs 必须是 ${String(u.minMaxRunMs)}..${String(u.maxRunCeilingMs)} 的整数毫秒`);return e}}function ue(e){if(void 0!==e){if("number"!=typeof e||!Number.isSafeInteger(e)||e<1)throw new C(M.invalid,"maxRounds 必须是正安全整数");return e}}function ce(t){const n=e.safeParse(t);if(!n.success)throw new C(M.invalid,"Invalid schedule host context");return n.data}function le(e){void 0!==e.hostContext&&ce(e.hostContext);const t=e.name.trim();if(0===t.length||t.length>u.maxNameChars)throw new C(M.invalid,`name 必须为 1..${String(u.maxNameChars)} 个字符`);const n=e.prompt.trim();if(0===n.length||n.length>u.maxPromptChars)throw new C(M.invalid,`prompt 必须为 1..${String(u.maxPromptChars)} 个字符`);if(!r(e.cwd))throw new C(M.invalid,"cwd 必须是绝对路径");const i=N.safeParse(e.trigger);if(!i.success)throw new C(M.invalid,"trigger 不合法");return{name:t,prompt:n,cwd:e.cwd,trigger:i.data,maxRunMs:oe(e.maxRunMs),maxRounds:ue(e.maxRounds)}}export class ScheduleStore{db;maxSchedules;claimLeaseMs;retryBudget;retryBackoffMs;executorLiveness;treeLiveness;maxLivenessProbesPerClaim;livenessProbeDeferralMs;invocationRetentionPerSchedule;invocationRetentionMs;constructor(e,r={}){this.maxSchedules=r.maxSchedules??50,this.claimLeaseMs=r.claimLeaseMs??u.claimLeaseMs,this.retryBudget=r.retryBudget??u.retryBudget,this.retryBackoffMs=r.retryBackoffMs??u.retryBackoffMs,this.executorLiveness=r.executorLiveness??(()=>S.unknown),this.treeLiveness=r.treeLiveness,this.maxLivenessProbesPerClaim=r.maxLivenessProbesPerClaim??u.maxLivenessProbesPerClaim,this.livenessProbeDeferralMs=r.livenessProbeDeferralMs??u.livenessProbeDeferralMs,this.invocationRetentionPerSchedule=r.invocationRetentionPerSchedule??u.invocationRetentionPerSchedule,this.invocationRetentionMs=r.invocationRetentionMs??u.invocationRetentionMs;const d=o(e);if(!0===r.readOnly){const e=s(d,"schedules.db"),t=n(e);if(!t&&!0===r.requireExistingDatabase)throw new Error("authoritative scheduler database does not exist");this.db=t?c(e):new a(":memory:");try{if(t){const e=this.db.prepare("PRAGMA user_version").get()?.user_version;if(![5,6,7,8,9,h].includes(Number(e)))throw new Error(`scheduler schema v${String(e)} 不支持只读查询;请停止旧 daemon 后由新版 daemon 初始化迁移`)}else this.init(),l(this.db)}catch(e){throw this.db.close(),e}return}if(!0===r.requireExistingDatabase&&!n(d))throw new Error("authoritative scheduler database does not exist");n(d)||i(d,{recursive:!0,mode:448}),"win32"!==process.platform&&t(d,448);const E=s(d,"schedules.db");if(!0===r.requireExistingDatabase&&!n(E))throw new Error("authoritative scheduler database does not exist");if(this.db=new a(E),!0===r.requireExistingDatabase)try{this.db.exec(`PRAGMA busy_timeout = ${String(b)}`),F(this.db)}catch(e){throw this.db.close(),e}if("win32"!==process.platform)try{t(E,384)}catch(e){throw this.db.close(),e}try{this.init()}catch(e){throw this.db.close(),e}}init(){this.db.exec(`PRAGMA busy_timeout = ${String(b)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`),E(this.db);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>h)throw H(e.user_version);const t=e.user_version<7;t&&this.db.exec("PRAGMA foreign_keys = OFF;");try{this.transaction(()=>{const e=this.db.prepare("PRAGMA user_version").get().user_version;if(e>h)throw H(e);const t=e<7&&void 0!==this.db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'schedules'").get();if(this.db.exec(`CREATE TABLE IF NOT EXISTS schedules (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS invocations (\n id TEXT PRIMARY KEY,\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'running', 'retry', 'completed', 'needs_confirmation', 'failed')),\n scheduled_for INTEGER NOT NULL,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT ${String(u.retryBudget)},\n executor_pid INTEGER,\n executor_start_token TEXT,\n executor_probed_at INTEGER,\n claimed_by TEXT,\n ownership_token TEXT,\n lease_until INTEGER,\n retry_at INTEGER,\n thread_id TEXT,\n output_excerpt TEXT,\n error TEXT,\n pending_actions_json TEXT NOT NULL DEFAULT '[]',\n created_at INTEGER NOT NULL,\n started_at INTEGER,\n finished_at INTEGER,\n tree_tracked_pgids TEXT,\n tree_unsettled INTEGER NOT NULL DEFAULT 0,\n tree_survivor_pids TEXT,\n UNIQUE (schedule_id, mode, scheduled_for)\n );\n CREATE INDEX IF NOT EXISTS idx_schedules_due\n ON schedules (next_run_at) WHERE status = 'active' AND next_run_at IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_invocations_live\n ON invocations (schedule_id) WHERE status IN ('pending', 'claimed', 'running', 'retry');\n CREATE TABLE IF NOT EXISTS schedule_thread_refs (\n invocation_id TEXT NOT NULL,\n attempt INTEGER NOT NULL CHECK (attempt > 0),\n schedule_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n threads_dir TEXT NOT NULL,\n name TEXT NOT NULL,\n cwd TEXT NOT NULL,\n scheduled_for INTEGER NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('scheduled', 'manual')),\n created_at INTEGER NOT NULL,\n PRIMARY KEY (invocation_id, attempt)\n );\n CREATE INDEX IF NOT EXISTS idx_schedule_thread_refs_history\n ON schedule_thread_refs (schedule_id, scheduled_for DESC, invocation_id, attempt DESC);`),e<7&&this.addMissingColumns(),t&&this.migrateRoundsInTransaction(),this.db.exec("CREATE TABLE IF NOT EXISTS schedule_extensions (\n request_id TEXT PRIMARY KEY CHECK (length(request_id) BETWEEN 1 AND 128),\n schedule_id TEXT NOT NULL REFERENCES schedules(id) ON DELETE CASCADE,\n expected_max_rounds INTEGER NOT NULL CHECK (typeof(expected_max_rounds) = 'integer' AND expected_max_rounds BETWEEN 1 AND 9007199254740991),\n additional_rounds INTEGER NOT NULL CHECK (typeof(additional_rounds) = 'integer' AND additional_rounds BETWEEN 1 AND 9007199254740991),\n authority_digest TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE (schedule_id, expected_max_rounds)\n );"),t&&0!==this.db.prepare("PRAGMA foreign_key_check").all().length)throw new C(M.invalid,"scheduler 迁移失败:外键关联不完整");this.db.prepare("PRAGMA table_info(schedules)").all().some(e=>"host_context"===e.name)||this.db.exec("ALTER TABLE schedules ADD COLUMN host_context TEXT"),p(this.db),e!==h&&this.db.exec(`PRAGMA user_version = ${String(h)};`)})}finally{t&&this.db.exec("PRAGMA foreign_keys = ON;")}}migrateRoundsInTransaction(){this.db.exec("CREATE TABLE schedules_v7 (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n prompt TEXT NOT NULL,\n cwd TEXT NOT NULL,\n trigger_json TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'paused', 'completed')),\n authority_digest TEXT,\n max_run_ms INTEGER,\n max_rounds INTEGER CHECK (max_rounds IS NULL OR (typeof(max_rounds) = 'integer' AND max_rounds BETWEEN 1 AND 9007199254740991)),\n rounds_started INTEGER NOT NULL DEFAULT 0 CHECK (typeof(rounds_started) = 'integer' AND rounds_started BETWEEN 0 AND 9007199254740991),\n next_run_at INTEGER,\n last_run_at INTEGER,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n INSERT INTO schedules_v7 (rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at)\n SELECT rowid, id, name, prompt, cwd, trigger_json, status,\n authority_digest, max_run_ms, next_run_at, last_run_at, last_error, created_at, updated_at FROM schedules;\n DROP TABLE schedules;\n ALTER TABLE schedules_v7 RENAME TO schedules;\n CREATE INDEX idx_schedules_due ON schedules (next_run_at)\n WHERE status = 'active' AND next_run_at IS NOT NULL;")}addMissingColumns(){const e=[{table:"schedules",column:"authority_digest",definition:"TEXT"},{table:"schedules",column:"max_run_ms",definition:"INTEGER"},{table:"invocations",column:"max_attempts",definition:`INTEGER NOT NULL DEFAULT ${String(u.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!==_.interval||!("everyMs"in e)||"number"!=typeof e.everyMs||e.everyMs<=u.maxIntervalMs||this.db.prepare("UPDATE schedules SET trigger_json = ?, status = ?, last_error = ?, updated_at = ?\n WHERE id = ?").run(JSON.stringify({kind:_.interval,everyMs:u.maxIntervalMs}),O.paused,`原间隔 ${String(e.everyMs)} ms 超过上限 365 天,已按 365 天登记并暂停;确认后 roll schedule resume`,Date.now(),t.id)}}transaction(e){this.db.exec("BEGIN IMMEDIATE");try{const t=e();return this.db.exec("COMMIT"),t}catch(e){throw this.db.exec("ROLLBACK"),e}}createSchedule(e,t){const n=le(e);return this.transaction(()=>this.insertScheduleInTransaction(n,e,t??Date.now()))}createScheduleIdempotent(e,t){const n=le(e),i=String(u.maxRunMs);return this.transaction(()=>{const r=t??Date.now(),s=this.db.prepare(`SELECT * FROM schedules\n WHERE status = ? AND prompt = ? AND cwd = ? AND trigger_json = ?\n AND COALESCE(max_run_ms, ${i}) = COALESCE(?, ${i})\n AND max_rounds IS ? AND host_context IS ?`).get(O.active,n.prompt,n.cwd,JSON.stringify(n.trigger),n.maxRunMs??null,n.maxRounds??null,e.hostContext??null);if(void 0!==s){const t=e.authorityDigest??null;return(s.authority_digest??null)!==t?(this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,r,s.id),{created:!1,reauthorized:!0,schedule:this.requireSchedule(s.id)}):{created:!1,reauthorized:!1,schedule:ae(s,this.db)}}return{created:!0,reauthorized:!1,schedule:this.insertScheduleInTransaction(n,e,r)}})}insertScheduleInTransaction(e,t,n){if(this.db.prepare("SELECT COUNT(*) AS count FROM schedules").get().count>=this.maxSchedules)throw new C(M.limitReached,`已达到定时任务上限 ${String(this.maxSchedules)},请先删除不再需要的任务`);if(!0===t.fireImmediately&&("interval"!==e.trigger.kind||void 0!==e.trigger.startAtMs))throw new C(M.invalid,"立即触发不能与日历规则或 startAt 同时使用");const i=d(),r=!0===t.fireImmediately?n:T(e.trigger,n);if(void 0!==t.expectedFirstRunAtMs&&r!==t.expectedFirstRunAtMs)throw new C(M.invalid,"首次运行时间在确认后已变化,请重新发起确认");return this.db.prepare("INSERT INTO schedules\n (id, name, prompt, cwd, trigger_json, status, authority_digest, max_run_ms,\n next_run_at, created_at, updated_at, max_rounds, host_context)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(i,e.name,e.prompt,e.cwd,JSON.stringify(e.trigger),O.active,t.authorityDigest??null,e.maxRunMs??null,r,n,n,e.maxRounds??null,t.hostContext??null),this.requireSchedule(i)}setAuthorityDigest(e,t,n=Date.now()){return 1===this.db.prepare("UPDATE schedules SET authority_digest = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes}resumeSchedule(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(i?.status===O.completed)throw new C(M.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");const r=this.db.prepare("UPDATE schedules SET status = ?, authority_digest = ?, updated_at = ? WHERE id = ?").run(O.active,t,n,e);return this.reconcileRoundsInTransaction(e,n),1===r.changes})}getScheduleExtension(e){const t=this.db.prepare("SELECT * FROM schedule_extensions WHERE request_id = ?").get(e);return void 0===t?void 0:{requestId:t.request_id,scheduleId:t.schedule_id,expectedMaxRounds:t.expected_max_rounds,additionalRounds:t.additional_rounds,authorityDigest:t.authority_digest,createdAtMs:t.created_at}}extendSchedule(e,t=Date.now()){const n=ue(e.additionalRounds),i=ue(e.expectedMaxRounds);if(void 0===n||void 0===i||!Number.isSafeInteger(i+n))throw new C(M.invalid,"追加轮数及原上限必须为正安全整数,追加后的总轮数不得溢出");if("string"!=typeof e.requestId||0===e.requestId.trim().length||e.requestId.length>128)throw new C(M.invalid,"requestId 必须为 1..128 字符的非空请求标识;重试时复用原标识");if("string"!=typeof e.authorityDigest||0===e.authorityDigest.trim().length)throw new C(M.invalid,"追加轮数必须重新核验工作区权限");return this.transaction(()=>{const r=this.getScheduleExtension(e.requestId);if(void 0!==r){if(r.scheduleId!==e.scheduleId||r.expectedMaxRounds!==i||r.additionalRounds!==n)throw new C(M.invalid,"requestId 已用于不同的追加请求;请检查原请求参数");return{extended:!1,schedule:this.requireSchedule(e.scheduleId)}}const s=this.requireSchedule(e.scheduleId);if(void 0===s.maxRounds)throw new C(M.invalid,"不限轮数的任务不需要追加额度");if(s.maxRounds!==i)throw new C(M.invalid,"任务轮数上限已变化,追加请求已过期;请刷新任务并核对原请求结果");if(s.status!==O.completed||s.roundsStarted!==s.maxRounds)throw new C(M.invalid,"仅能为已结束且轮数用尽的有限任务追加轮数");if(void 0!==this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND\n (status IN ('pending', 'claimed', 'running', 'retry') OR tree_unsettled = 1\n OR (tree_tracked_pgids IS NOT NULL AND trim(tree_tracked_pgids) != '[]')) LIMIT 1").get(s.id))throw new C(M.invalid,"任务仍有待执行、运行中或未清场的记录,暂不能追加轮数;请先完成运行收尾");const d=m(s.trigger,t);if(!Number.isSafeInteger(t)||!Number.isSafeInteger(d)||!Number.isFinite(new Date(d).getTime()))throw new C(M.invalid,"追加时间或下次执行时间超出有效范围");return this.db.prepare("INSERT INTO schedule_extensions (request_id, schedule_id, expected_max_rounds,\n additional_rounds, authority_digest, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(e.requestId,s.id,i,n,e.authorityDigest,t),this.db.prepare("UPDATE schedules SET max_rounds = ?, authority_digest = ?, status = ?,\n next_run_at = ?, updated_at = ? WHERE id = ?").run(i+n,e.authorityDigest,O.active,d,t,s.id),{extended:!0,schedule:this.requireSchedule(s.id)}})}getSchedule(e){const t=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return t?ae(t,this.db):void 0}listSchedules(){return this.db.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all().map(e=>ae(e,this.db))}setScheduleStatus(e,t,n=Date.now()){return this.transaction(()=>{const i=this.db.prepare("SELECT status FROM schedules WHERE id = ?").get(e);if(void 0===i)return!1;if(i.status===O.completed){if(t===O.active)throw new C(M.invalid,"任务已结束,轮数已用尽;请用 roll schedule extend <id> --rounds N 追加轮数");return!0}if(t===O.completed)throw new C(M.invalid,"结束状态由调度器在轮数用尽且运行结算后设置");if(1!==this.db.prepare("UPDATE schedules SET status = ?, updated_at = ? WHERE id = ?").run(t,n,e).changes)return!1;if(t===O.paused){const t=this.db.prepare("SELECT id FROM invocations WHERE schedule_id = ? AND mode = ? AND status = ?").all(e,A.scheduled,I.retry);for(const e of t)this.finishInvocationAsFailedInTransaction(e.id,"任务已暂停,放弃重试",n)}return this.reconcileRoundsInTransaction(e,n),!0})}probeExecutor(e){return this.executorLiveness(e)}listRunningInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status = ? ORDER BY started_at ASC").all(I.running).map(se)}listActiveWorkerInvocations(){return this.db.prepare("SELECT * FROM invocations WHERE status IN (?, ?)\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC").all(I.claimed,I.running).map(se)}listOccupyingInvocations(){return this.db.prepare(`SELECT * FROM invocations WHERE ${te}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...ne).map(se)}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,I.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,I.running).map(e=>{if(null===e.ownership_token)throw new C(M.invalid,`running invocation ${e.id} 缺少 ownership token`);const t=this.loadClaim(e.id,e.ownership_token);if(void 0===t)throw new C(M.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 ${te} LIMIT 1`).get(e,...ne);if(void 0!==t)throw new C(M.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=d();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,A.manual,I.pending,t+r,i,t).changes)return this.requireInvocation(n)}throw new C(M.invalid,`schedule ${e} 的手动触发入队失败`)})}claimPendingInvocation(e,t,n=Date.now()){return this.transaction(()=>{const i=d();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 ${te})`).run(I.claimed,t,i,n+this.claimLeaseMs,e,I.pending,e,e,...ne).changes?this.loadClaim(e,i):void 0})}findLiveRun(e){const t=this.db.prepare(`SELECT * FROM invocations WHERE schedule_id = ? AND ${te}\n ORDER BY started_at ASC, created_at ASC LIMIT 1`).get(e,...ne);return t?se(t):void 0}discardPendingInvocation(e){return 1===this.db.prepare("DELETE FROM invocations WHERE id = ? AND status = ?").run(e,I.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&&J(t.tree_tracked_pgids).kind===k.invalid)return R.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&&ie.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 R.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!==I.running||t)return;const n=re(e),i=void 0===n?S.unknown:this.executorLiveness(n);return i===S.alive||i===S.descendants?R.executorAlive:i===S.unknown?R.executorUnknown:void 0}revisionMismatch(e,t){return void 0!==e&&$(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?R.ownershipChanged:void 0:R.notFound}cancelInvocationInTransaction(e,t,n,i,r={}){const s=this.revisionMismatch(e,i);if(void 0!==s)return s;if(void 0===e||!$(e.status))return R.notFound;if(!g.includes(e.status))return R.terminal;const d=!0===r.executorChecked?void 0:this.executorBlocksCancel(e,!0===i.abandon);if(void 0!==d)return d;const a=!0===i.abandon;return!a&&this.treeBlocksTerminal(e)?R.treeUnsettled:this.finishInvocationAsFailedInTransaction(e.id,t,n,{force:a})?R.cancelled:R.treeUnsettled}deferLiveRowInTransaction(e,t){const n=e.status===I.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(O.paused,`任务配置无法解析,已暂停:${t instanceof Error?t.message:String(t)}`,n,e)}decodeScheduleForClaimInTransaction(e,t){try{return ae(e,this.db)}catch(n){return void this.pauseInvalidScheduleInTransaction(e.id,n,t)}}loadScheduleForClaimInTransaction(e,t){const n=this.db.prepare("SELECT * FROM schedules WHERE id = ?").get(e);return void 0===n?void 0:this.decodeScheduleForClaimInTransaction(n,t)}hasOtherLiveRunInTransaction(e,t){return void 0!==this.db.prepare(`SELECT 1 AS present FROM invocations\n WHERE schedule_id = ? AND id != ? AND ${te} LIMIT 1`).get(e,t,...ne)}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(I.pending,I.retry,e.nowMs,I.claimed,I.running,e.nowMs),r=[],s=i.filter(e=>e.status===I.running&&!t.has(e.id)&&void 0!==re(e));s.sort((e,t)=>(e.executor_probed_at??-1)-(t.executor_probed_at??-1));const a=new Map;let o=e.limit<=0?0:this.maxLivenessProbesPerClaim;for(const t of s){const n=re(t);void 0!==n&&(o<=0?a.set(t.id,U.deferred):(o-=1,this.db.prepare("UPDATE invocations SET executor_probed_at = ? WHERE id = ?").run(e.nowMs,t.id),a.set(t.id,this.executorLiveness(n)===S.dead?U.dead:U.notDead)))}for(const n of i){const i=n.status===I.claimed||n.status===I.running,s=a.get(n.id);!i||!t.has(n.id)&&s!==U.notDead?s!==U.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(J(t.tree_tracked_pgids).kind===k.invalid){this.deferLiveRowInTransaction(t,e.nowMs);continue}const i=this.loadScheduleForClaimInTransaction(t.schedule_id,e.nowMs);if(void 0===i){t.status!==I.pending&&this.deferLiveRowInTransaction(t,e.nowMs);continue}if(t.mode===A.scheduled&&i.status===O.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===I.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 s=d();this.db.prepare("UPDATE invocations\n SET status = ?, claimed_by = ?, ownership_token = ?, lease_until = ?,\n attempt = ?, retry_at = NULL\n WHERE id = ?").run(I.claimed,e.workerId,s,e.nowMs+this.claimLeaseMs,r,t.id);const a=this.loadClaim(t.id,s);void 0!==a&&n.push(a)}if(n.length>=e.limit)return n;const u=this.db.prepare("SELECT s.* FROM schedules s\n WHERE s.status = ? AND s.next_run_at IS NOT NULL AND s.next_run_at <= ?\n AND (s.max_rounds IS NULL OR s.rounds_started < s.max_rounds)\n AND NOT EXISTS (\n SELECT 1 FROM invocations i\n WHERE i.schedule_id = s.id AND i.status IN (?, ?, ?, ?))\n ORDER BY s.next_run_at ASC, s.created_at ASC\n LIMIT ?").all(O.active,e.nowMs,I.pending,I.claimed,I.running,I.retry,this.maxSchedules);for(const t of u){if(n.length>=e.limit)break;const i=this.decodeScheduleForClaimInTransaction(t,e.nowMs);if(void 0===i)continue;const r=d(),s=d(),a=this.db.prepare("INSERT OR IGNORE INTO invocations\n (id, schedule_id, mode, status, scheduled_for, attempt, max_attempts, claimed_by,\n ownership_token, lease_until, created_at)\n VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)").run(r,t.id,A.scheduled,I.claimed,t.next_run_at??e.nowMs,this.retryBudget,e.workerId,s,e.nowMs+this.claimLeaseMs,e.nowMs),o=Number(a.changes);if(this.db.prepare("UPDATE schedules SET rounds_started = rounds_started + ?,\n next_run_at = CASE WHEN max_rounds IS NOT NULL AND rounds_started + ? >= max_rounds\n THEN NULL ELSE ? END, updated_at = ? WHERE id = ?").run(o,o,m(i.trigger,e.nowMs),e.nowMs,t.id),1===a.changes){const e=this.loadClaim(r,s);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(I.running,n,n+this.claimLeaseMs,i?.pid??null,i?.startToken??null,e,t,I.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,I.claimed,I.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!==I.claimed&&t.status!==I.running)return L.lostClaim;if(this.treeBlocksTerminal(t))return L.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,I.claimed,I.running).changes?L.lostClaim:(this.db.prepare("UPDATE schedules SET last_run_at = ?, last_error = NULL, updated_at = ?\n WHERE id = (SELECT schedule_id FROM invocations WHERE id = ?)").run(e.nowMs,e.nowMs,e.id),t.mode===A.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),L.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!==I.completed&&t.status!==I.needsConfirmation)return f.lostClaim;if(this.treeBlocksTerminal(t))return f.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,I.completed,I.needsConfirmation).changes?f.lostClaim:(this.markTerminalFailureInTransaction(t,e.error,e.nowMs),t.mode===A.scheduled&&this.getSchedule(t.schedule_id)?.status===O.paused?f.terminalPaused:f.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(I.retry,e.nowMs+this.retryBackoffMs,e.error,e.id,e.expectedAttempt,I.completed,I.needsConfirmation).changes?f.lostClaim:(this.db.prepare("UPDATE schedules SET last_error = ?, updated_at = ? WHERE id = ?").run(e.error,e.nowMs,t.schedule_id),t.mode===A.scheduled&&this.reconcileRoundsInTransaction(t.schedule_id,e.nowMs),f.retryScheduled)})}recordInvocationTree(e){return this.writeInvocationTreeInTransaction(e)}writeInvocationTreeInTransaction(e){const t=e.unsettled?e.trackedGroups??[]:[],n=t.length>0?Z(t):null,i=ee(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,I.claimed,I.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,I.claimed,I.running,I.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!==I.claimed&&s.status!==I.running?f.lostClaim:!0===r.terminal||s.attempt>=s.max_attempts?this.treeBlocksTerminal(s)?f.treeUnsettled:(this.markTerminalFailureInTransaction(s,n,i),s.mode===A.scheduled&&this.getSchedule(s.schedule_id)?.status===O.paused?f.terminalPaused:f.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(I.retry,i+this.retryBackoffMs,n,e),f.retryScheduled)})}registerThreadReference(e,t=Date.now()){return P(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,I.running);return void 0===n?void 0:this.insertThreadReference(n,e,t)})}backfillThreadReference(e,t=Date.now()){return P(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 C(M.invalid,`invocation ${t.invocationId} attempt ${String(t.expectedAttempt)} 已关联其他会话`);return W(i)}const r=this.getSchedule(e.schedule_id);if(void 0!==r){if(!X(e.mode))throw new C(M.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?se(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(se)}pruneInvocations(e=Date.now()){return this.transaction(()=>{const t=this.db.prepare(`DELETE FROM invocations\n WHERE status IN (${y})\n AND finished_at IS NOT NULL AND finished_at < ?`).run(...x,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 (${y}))\n WHERE rn > ?)`).run(...x,this.invocationRetentionPerSchedule);return Number(t.changes)+Number(n.changes)})}nextWakeAtMs(){return this.db.prepare("SELECT MIN(t) AS wake FROM (\n SELECT MIN(next_run_at) AS t FROM schedules\n WHERE status = ? AND next_run_at IS NOT NULL\n AND (max_rounds IS NULL OR rounds_started < max_rounds)\n UNION ALL SELECT MIN(retry_at) FROM invocations WHERE status = ?\n UNION ALL SELECT MIN(lease_until) FROM invocations WHERE status IN (?, ?)\n UNION ALL SELECT MIN(scheduled_for) FROM invocations WHERE status = ?)").get(O.active,I.retry,I.claimed,I.running,I.pending).wake??void 0}treeBlocksTerminal(e){const t=J(e.tree_tracked_pgids);return t.kind===k.invalid||(1===e.tree_unsettled||0!==t.groups.length)&&(void 0===this.treeLiveness||this.treeLiveness(se(e))!==w.settled)}reconcileRoundsInTransaction(e,t){this.db.prepare("UPDATE schedules\n SET status = CASE\n WHEN NOT EXISTS (SELECT 1 FROM invocations i WHERE i.schedule_id = schedules.id AND i.mode = 'scheduled'\n AND (i.status IN ('pending', 'claimed', 'running', 'retry') OR i.tree_unsettled = 1\n OR (i.tree_tracked_pgids IS NOT NULL AND trim(i.tree_tracked_pgids) != '[]')))\n THEN 'completed'\n WHEN status = 'completed' THEN 'active'\n ELSE status END,\n next_run_at = NULL, updated_at = ?\n WHERE id = ? AND max_rounds IS NOT NULL AND rounds_started >= max_rounds").run(t,e)}finishInvocationAsFailedInTransaction(e,t,n,i={}){const r=this.db.prepare("SELECT * FROM invocations WHERE id = ?").get(e);return void 0!==r&&((!0===i.force||!this.treeBlocksTerminal(r))&&(this.db.prepare("UPDATE invocations\n SET status = ?, error = ?, finished_at = ?, claimed_by = NULL, ownership_token = NULL,\n lease_until = NULL, retry_at = NULL, tree_tracked_pgids = NULL,\n tree_unsettled = 0, tree_survivor_pids = NULL\n WHERE id = ?").run(I.failed,t,n,e),r.mode===A.scheduled&&this.reconcileRoundsInTransaction(r.schedule_id,n),!0))}markTerminalFailureInTransaction(e,t,n){this.finishInvocationAsFailedInTransaction(e.id,t,n)&&(e.mode===A.scheduled?this.db.prepare("UPDATE schedules SET status = CASE WHEN status = 'completed' THEN status ELSE ? END, last_error = ?, updated_at = ? WHERE id = ?").run(O.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 C(M.notFound,`invocation ${e} 不存在`);return t}close(){this.db.close()}requireSchedule(e){const t=this.getSchedule(e);if(void 0===t)throw new C(M.notFound,`schedule ${e} 不存在`);return t}}export const SCHEDULE_LEDGER_READ_STATUSES={ok:"ok",empty:"empty",migrationRequired:"migration-required"};export function readScheduleLedger(e){const t=s(o(e),"schedules.db");if(!n(t))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty,schedules:[]};const i=c(t);let r=!1;try{i.exec(`PRAGMA busy_timeout = ${String(b)}; BEGIN;`),r=!0;const e=i.prepare("PRAGMA user_version").get();if(![5,6,7,8,9,h].includes(e.user_version))return{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version,schedules:[]};const t=i.prepare("SELECT * FROM schedules ORDER BY created_at ASC, rowid ASC").all();return{status:SCHEDULE_LEDGER_READ_STATUSES.ok,schedules:t.map(e=>ae(e,i))}}finally{try{r&&i.exec("ROLLBACK")}finally{i.close()}}}function he(e,t){const i=s(o(e),"schedules.db");if(!n(i))return{status:SCHEDULE_LEDGER_READ_STATUSES.empty};const r=c(i);let d=!1;try{r.exec(`PRAGMA busy_timeout = ${String(b)}; BEGIN;`),d=!0;const e=r.prepare("PRAGMA user_version").get();return[5,6,7,8,9,h].includes(e.user_version)?{status:SCHEDULE_LEDGER_READ_STATUSES.ok,value:t(r,e.user_version>=6)}:{status:SCHEDULE_LEDGER_READ_STATUSES.migrationRequired,schemaVersion:e.user_version}}finally{try{d&&r.exec("ROLLBACK")}finally{r.close()}}}export function readScheduleOccupancy(e){const t=he(e,e=>e.prepare(`SELECT * FROM invocations WHERE ${te}\n ORDER BY COALESCE(started_at, created_at) ASC, created_at ASC`).all(...ne).map(se));if(t.status===SCHEDULE_LEDGER_READ_STATUSES.migrationRequired)throw new Error(`scheduler occupancy 无法读取 schema v${String(t.schemaVersion)},拒绝把未知占用当成空闲`);return t.value??[]}function Ee(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(W):[],s=void 0===i?void 0:se(i),d=s??r[0];return void 0===d?void 0:{invocationId:t,scheduleId:d.scheduleId,scheduledForMs:d.scheduledForMs,mode:d.mode,invocation:s,references:r}}function pe(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 _e(e,t){const n=e.prepare("SELECT * FROM schedules").all().map(t=>ae(t,e)),i=new Map(n.map(e=>[e.id,e])),r=e.prepare("SELECT id FROM schedules"+(t?" UNION SELECT schedule_id AS id FROM schedule_thread_refs":"")).all(),s=[];for(const{id:n}of r){const r=e.prepare(`SELECT id FROM (${pe(t)})\n WHERE schedule_id = ? ORDER BY scheduled_for DESC, id DESC LIMIT 1`).get(n),d=void 0===r?void 0:Ee(e,r.id,t),a=i.get(n),o=d?.references.at(-1),u=a??o;void 0!==u&&s.push({scheduleId:n,name:u.name,cwd:u.cwd,schedule:a,latestRun:d})}return s.sort((e,t)=>Number(void 0===e.schedule)-Number(void 0===t.schedule)||(t.latestRun?.scheduledForMs??t.schedule?.createdAtMs??0)-(e.latestRun?.scheduledForMs??e.schedule?.createdAtMs??0)||e.scheduleId.localeCompare(t.scheduleId))}export function readScheduleHistory(e,t={}){const n=t.limit??20,i=t.offset??0;if(!Number.isSafeInteger(n)||n<1||n>100||!Number.isSafeInteger(i)||i<0)throw new RangeError("History limit must be 1..100 and offset must be nonnegative integers");const r=he(e,(e,r)=>{const s=void 0===t.scheduleId?"":"WHERE schedule_id = ?",d=e.prepare(`SELECT id FROM (${pe(r)}) ${s}\n ORDER BY scheduled_for DESC, id DESC LIMIT ? OFFSET ?`),a=void 0===t.scheduleId?[n+1,i]:[t.scheduleId,n+1,i],o=d.all(...a),u=[];for(const{id:t}of o.slice(0,n)){const n=Ee(e,t,r);void 0!==n&&u.push(n)}return{tasks:_e(e,r),runs:u,hasMore:o.length>n}});return{status:r.status,...void 0===r.schemaVersion?{}:{schemaVersion:r.schemaVersion},tasks:r.value?.tasks??[],runs:r.value?.runs??[],hasMore:r.value?.hasMore??!1}}export function readScheduleRun(e,t){const n=he(e,(e,n)=>Ee(e,t,n));return{status:n.status,...void 0===n.schemaVersion?{}:{schemaVersion:n.schemaVersion},run:n.value}}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScheduleHostContext } from "../host-contracts/schedule-context.ts";
|
|
1
2
|
import type { TriggerSpec } from "./trigger.ts";
|
|
2
3
|
export declare const SCHEDULE_STATUSES: {
|
|
3
4
|
readonly active: "active";
|
|
@@ -101,6 +102,7 @@ export declare const INVOCATION_TREE_LIVENESS: {
|
|
|
101
102
|
};
|
|
102
103
|
export type InvocationTreeLiveness = (typeof INVOCATION_TREE_LIVENESS)[keyof typeof INVOCATION_TREE_LIVENESS];
|
|
103
104
|
export interface ScheduleRecord {
|
|
105
|
+
readonly hostContext?: ScheduleHostContext;
|
|
104
106
|
readonly id: string;
|
|
105
107
|
readonly name: string;
|
|
106
108
|
readonly prompt: string;
|
|
@@ -188,6 +190,7 @@ export interface ClaimedInvocation {
|
|
|
188
190
|
readonly ownershipToken: string;
|
|
189
191
|
}
|
|
190
192
|
export interface CreateScheduleInput {
|
|
193
|
+
readonly hostContext?: ScheduleHostContext;
|
|
191
194
|
readonly name: string;
|
|
192
195
|
readonly prompt: string;
|
|
193
196
|
readonly cwd: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SessionClaims as e,SessionOccupiedError as t,canonicalSessionDatabasePath as n}from"../ownership/session-claims.js";import{SemanticLedger as r,SEMANTIC_LEDGER_SQL as s,contextRecoveryNoteSchema as i}from"./semantic-ledger.js";import{chmodSync as o,existsSync as a,mkdirSync as c,realpathSync as d,statSync as E}from"node:fs";import{homedir as _}from"node:os";import{resolve as h}from"node:path";import{randomUUID as u}from"node:crypto";import{DatabaseSync as p}from"node:sqlite";import{modelMessageSchema as T}from"ai";import{APP_OUTPUT_LIMITS as l,appOutputResultSchema as m,appOutputDescriptorSchema as N,describeAppOutput as R,RUNTIME_V13_MAX_DURABLE_EVENT_RECORD_BYTES as S,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as g,runtimeDurableEventV16Schema as O,runtimeEventCursorSchema as I,runtimeEventIdSchema as v,timestampSchema as L}from"@roll-agent/protocol";import{parsePersistedToolExecutionRecord as x,prepareToolExecutionRecordForPersistence as f,prepareAppOutputForPersistence as A,redactSecretText as C}from"../tool-bridge/tool-execution-record.js";import{TASK_SUMMARY_CHECKPOINT_VERSION as b,COMPACTION_TRANSCRIPT_COMPLETENESS as w,TRANSCRIPT_MESSAGE_PROVENANCES as q,UnsupportedCompactionCheckpointVersionError as M,archivedTranscriptMessageSchema as y,createSummaryCheckpoint as D,buildCompactionToolState as U,parseCompactionCheckpoint as k}from"../engine/compaction-checkpoint.js";import{listSemanticArchiveItems as F,semanticArchiveItemSchema as j}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as W,readCompactionSummaryPayload as X}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as H,repairActiveToolProtocol as P}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as B}from"../engine/explicit-skill-context.js";import{threadOriginSchema as G,threadDerivedFromSchema as Y,scheduledThreadOriginSchema as K}from"./thread-origin.js";import{acceptedTurnInputSchema as J,turnInputIdSchema as $,turnInputSkillCheckpointSchema as V}from"./turn-input.js";export const THREAD_STORE_SCHEMA_VERSION=11;const z=11,Q=100,Z=500,ee=50,te=500,ne=100,re=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const RUNTIME_EVENT_RETENTION_POLICY={maxBytesPerThread:S,maxRecordsPerThread:g,maxAgeMs:2592e6};export class RuntimeEventCursorExpiredError extends Error{cursor;constructor(e){super("Runtime event cursor is older than the retained event prefix"),this.name="RuntimeEventCursorExpiredError",this.cursor=e}}export class RuntimeEventCursorGapError extends Error{cursor;constructor(e){super("Runtime event cursor does not belong to the current Thread event log"),this.name="RuntimeEventCursorGapError",this.cursor=e}}export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution","legacy_context"];export const TOOL_EXECUTION_CONTEXT_REPRESENTATIONS={rawTranscript:"raw_transcript",recoveryEvidence:"recovery_evidence"};const se="legacy_assumed";export function expandTilde(e){return e.startsWith("~/")?h(_(),e.slice(2)):e}export function defaultThreadsDir(){return h(_(),".roll-agent","threads")}function ie(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function oe(e){if(Number.isInteger(e.schema_version)&&e.schema_version>b)throw new M(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=k(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function ae(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at,origin:G.parse(e.origin_json?JSON.parse(e.origin_json):{kind:"interactive"}),...e.derived_from_json?{derivedFrom:Y.parse(JSON.parse(e.derived_from_json))}:{}}}function ce(e,t){return"string"==typeof e?t.get(e)??e:Array.isArray(e)?e.map(e=>ce(e,t)):"object"==typeof e&&null!==e?Object.fromEntries(Object.entries(e).map(([e,n])=>[e,ce(n,t)])):e}function de(){return v.parse(u())}function Ee(e,t,n){return I.parse(`rte1:${e}:${String(t)}:${n}`)}function _e(e){const t=I.safeParse(e);if(!t.success)return;const n=t.data.split(":"),r=Number(n[2]),s=v.safeParse(n[3]);return void 0===n[1]||!Number.isSafeInteger(r)||r<0||!s.success?void 0:{eventLogId:n[1],threadSequence:r,eventId:s.data}}function he(e){return new Date(Date.parse(L.parse(e))).toISOString()}function ue(e,t,n){const r=v.parse(n.event_id),s=O.parse(JSON.parse(n.event_json));return{eventId:r,cursor:Ee(e,n.thread_sequence,r),threadSequence:n.thread_sequence,threadId:t,...null===n.turn_id?{}:{turnId:n.turn_id},timestamp:n.event_timestamp,event:"tool.completed"===s.type&&null!=n.app_output_json?{...s,appOutput:N.parse(JSON.parse(n.app_output_json))}:s}}export class ThreadStore{db;now;readOnly;claims;sessionReservations=new Map;constructor(t=defaultThreadsDir(),n={}){this.now=n.now??(()=>new Date),this.readOnly=n.readOnly??!1;const r=expandTilde(t);if(this.claims=void 0,n.readOnly){this.db=new p(h(r,"threads.db"),{readOnly:!0});try{this.db.exec(`PRAGMA busy_timeout = ${String(re)}; PRAGMA query_only = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>z)throw new Error(`Unsupported ThreadStore schema v${String(e.user_version)}`)}catch(e){throw this.db.close(),e}return}a(r)||c(r,{recursive:!0,mode:448}),"win32"!==process.platform&&o(r,448),this.claims=void 0===n.ownership?void 0:new e(h(r,"threads.db"),n.ownership);const s=this.claims?.databasePath??h(r,"threads.db");let i,d;try{const e=void 0===n.ownership?void 0:inspectThreadLibrary(r);if(void 0!==e&&"current"!==e.status&&"new"!==e.status)throw new ThreadLibraryMigrationRequiredError(e);d="new"===e?.status?this.claims?.maintenance():void 0,this.db=i=new p(s),"win32"!==process.platform&&o(s,384),this.init(void 0!==n.ownership)}catch(e){throw i?.close(),d?.release(),this.claims?.close(),e}finally{d?.release()}}init(e=!1){this.db.exec(`PRAGMA busy_timeout = ${String(re)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const t=this.db.prepare("PRAGMA user_version").get();if(t.user_version>z)throw new Error(`ThreadStore schema v${String(t.user_version)} 高于当前支持的 v${String(z)}`);if(!e||t.user_version!==z){if(e&&0!==t.user_version)throw new Error("Existing history requires explicit backed-up migration");this.db.exec("BEGIN IMMEDIATE");try{if(this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),t.user_version<7){const e=this.db.prepare("PRAGMA table_info(threads)").all();for(const t of["origin_json","derived_from_json"])e.some(e=>e.name===t)||this.db.exec(`ALTER TABLE threads ADD COLUMN ${t} TEXT`)}if(ie(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS operation_app_outputs (\n thread_id TEXT NOT NULL,\n operation_id TEXT NOT NULL,\n result_json TEXT NOT NULL,\n descriptor_json TEXT NOT NULL,\n byte_length INTEGER NOT NULL,\n expires_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, operation_id),\n FOREIGN KEY (thread_id, operation_id) REFERENCES tool_executions(thread_id, id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_turn_inputs (\n thread_id TEXT NOT NULL,\n input_id TEXT NOT NULL,\n transcript_sequence INTEGER NOT NULL,\n skill_checkpoint_json TEXT,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, input_id),\n UNIQUE (thread_id, transcript_sequence),\n FOREIGN KEY (thread_id, transcript_sequence)\n REFERENCES transcript_messages(thread_id, sequence) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS tool_execution_context_coverage (\n thread_id TEXT NOT NULL,\n execution_id TEXT NOT NULL,\n representation TEXT NOT NULL\n CHECK (representation IN ('raw_transcript', 'recovery_evidence', 'legacy_assumed')),\n transcript_sequence INTEGER,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, execution_id),\n CHECK (\n (representation = 'legacy_assumed' AND transcript_sequence IS NULL)\n OR (representation <> 'legacy_assumed' AND transcript_sequence IS NOT NULL)\n ),\n FOREIGN KEY (thread_id, execution_id)\n REFERENCES tool_executions(thread_id, id) ON DELETE CASCADE,\n FOREIGN KEY (thread_id, transcript_sequence)\n REFERENCES transcript_messages(thread_id, sequence) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);\n CREATE TABLE IF NOT EXISTS thread_runtime_event_state (\n thread_id TEXT PRIMARY KEY,\n event_log_id TEXT NOT NULL UNIQUE,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n retained_from_sequence INTEGER NOT NULL\n CHECK (retained_from_sequence >= 0 AND retained_from_sequence <= next_sequence),\n retained_predecessor_event_id TEXT,\n latest_event_id TEXT,\n retained_count INTEGER NOT NULL CHECK (retained_count >= 0),\n retained_bytes INTEGER NOT NULL CHECK (retained_bytes >= 0),\n CHECK (\n (next_sequence = 0\n AND retained_from_sequence = 0\n AND retained_predecessor_event_id IS NULL\n AND latest_event_id IS NULL)\n OR (next_sequence > 0 AND latest_event_id IS NOT NULL)\n ),\n CHECK (\n (retained_from_sequence = 0 AND retained_predecessor_event_id IS NULL)\n OR (retained_from_sequence > 0 AND retained_predecessor_event_id IS NOT NULL)\n ),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS runtime_events (\n thread_id TEXT NOT NULL,\n thread_sequence INTEGER NOT NULL CHECK (thread_sequence >= 0),\n event_id TEXT NOT NULL UNIQUE,\n turn_id TEXT,\n event_timestamp TEXT NOT NULL,\n event_json TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, thread_sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_runtime_events_thread_created\n ON runtime_events(thread_id, created_at, thread_sequence);"),this.db.prepare("PRAGMA table_info(runtime_events)").all().some(e=>"app_output_json"===e.name)||this.db.exec("ALTER TABLE runtime_events ADD COLUMN app_output_json TEXT"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),t.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),t.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),t.user_version<6&&this.db.prepare("INSERT OR IGNORE INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, NULL, created_at\n FROM tool_executions").run(se),this.db.exec(s),t.user_version<9){const e=new r(this.db),t=this.db.prepare("SELECT thread_id, schema_version, checkpoint_json FROM compaction_checkpoints ORDER BY generation").all();for(const n of t){if("string"!=typeof n.thread_id||"string"!=typeof n.checkpoint_json)continue;let t;try{t=oe({schema_version:Number(n.schema_version),checkpoint_json:n.checkpoint_json})}catch(e){if(e instanceof M)throw e;this.db.prepare("UPDATE thread_transcript_state SET completeness = 'legacy_snapshot' WHERE thread_id = ?").run(n.thread_id);continue}if(2!==t.version)continue;const r=this.getLatestCheckpoint(n.thread_id);e.append(n.thread_id,t.generation,2===t.version?t.semanticState.scopeId??t.semanticState.goal?.id??"initial":"legacy",t.transcript.toolExecutions.throughSequence,F(t.semanticState),r?.id!==t.id)}}this.initializeMissingRuntimeEventStatesInTransaction();const e=this.now().toISOString();this.enforceAllToolExecutionRetentionInTransaction(e),this.enforceAllRuntimeEventRetentionInTransaction(e),this.db.exec(`PRAGMA user_version = ${String(z)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=x(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceAppOutputRetention(n),this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=T.parse(JSON.parse(t.message_json)),r=B(e);n.run(JSON.stringify(r),t.storage_rowid)}}}initializeMissingRuntimeEventStatesInTransaction(){const e=this.db.prepare("SELECT threads.id\n FROM threads\n LEFT JOIN thread_runtime_event_state\n ON thread_runtime_event_state.thread_id = threads.id\n WHERE thread_runtime_event_state.thread_id IS NULL").all(),t=this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)");for(const n of e)t.run(n.id,u())}enforceAllRuntimeEventRetentionInTransaction(e){const t=this.db.prepare("SELECT thread_id AS id FROM thread_runtime_event_state").all();for(const n of t)this.enforceRuntimeEventRetentionInTransaction(n.id,e)}createThread(e={}){const t=e.id??u();return this.withThreadWrite(t,()=>this.createThreadOwned({...e,id:t}))}createThreadOwned(e={}){const t=e.id??u();if(this.readOnly)throw new Error("ThreadStore is read-only");const n=this.now().toISOString(),r=G.parse(e.origin??{kind:"interactive"}),s=void 0===e.derivedFrom?void 0:Y.parse(e.derivedFrom);this.db.exec("SAVEPOINT create_thread");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at, origin_json, derived_from_json) VALUES (?, ?, ?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n,JSON.stringify(r),void 0===s?null:JSON.stringify(s)),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,w[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)").run(t,u()),this.db.exec("RELEASE create_thread"),t}catch(e){throw this.db.exec("ROLLBACK TO create_thread; RELEASE create_thread"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?ae(t):void 0}listThreads(e={}){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(ae).filter(t=>void 0===e.origin||t.origin.kind===e.origin)}backfillScheduledOrigin(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ? AND origin_json IS NULL").get(e)&&this.withThreadWrite(e,()=>this.backfillScheduledOriginOwned(e,t))}backfillScheduledOriginOwned(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");const n=K.parse(t);return this.db.prepare("UPDATE threads SET origin_json = ? WHERE id = ? AND origin_json IS NULL").run(JSON.stringify(n),e).changes>0}readSnapshot(e){this.db.exec("BEGIN");try{const t=this.getThread(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在`);const n=this.now().toISOString(),s=this.getMessages(e),i=e=>void 0!==this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(e),o=i("transcript_messages")?this.db.prepare("SELECT sequence, provenance, created_at, message_json FROM transcript_messages WHERE thread_id = ? ORDER BY sequence").all(e).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})):s.map((e,n)=>y.parse({sequence:n,message:e,provenance:"legacy_snapshot",createdAt:t.createdAt})),a=i("tool_executions")?this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? ORDER BY sequence").all(e).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence})):[],c=i("compaction_checkpoints")?this.db.prepare("SELECT schema_version, checkpoint_json FROM compaction_checkpoints WHERE thread_id = ? ORDER BY generation").all(e).map(oe):[],d=i("tool_execution_context_coverage")?this.db.prepare("SELECT execution_id AS executionId, representation, transcript_sequence AS transcriptSequence, created_at AS createdAt FROM tool_execution_context_coverage WHERE thread_id = ?").all(e):a.map(e=>({executionId:e.id,representation:"legacy_assumed",transcriptSequence:null,createdAt:n})),E=i("thread_tool_execution_state")?this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e):void 0,_=i("compaction_recovery")?new r(this.db).recovery(e):void 0,h={...i("compaction_semantic_items")?{semanticLedger:new r(this.db).versions(e)}:{},..._?{contextRecovery:_}:{},...i("compaction_recovery_notes")?{contextRecoveryNotes:new r(this.db).notes(e)}:{},thread:t,capturedAt:n,appOutputs:i("operation_app_outputs")?this.db.prepare("SELECT operation_id, result_json, expires_at FROM operation_app_outputs WHERE thread_id = ?").all(e).map(e=>({operationId:e.operation_id,result:this.readAppOutputRow(e.result_json,e.expires_at),expiresAt:e.expires_at})):[],messages:s,transcript:o,turnInputs:i("thread_turn_inputs")?this.listTurnInputs(e):[],toolExecutions:a,checkpoints:c,toolExecutionCoverage:d,nextToolExecutionSequence:E?.next_sequence??(a.at(-1)?.sequence??-1)+1,transcriptCompleteness:i("thread_transcript_state")?this.getTranscriptCompleteness(e):"legacy_snapshot"};return this.db.exec("COMMIT"),h}catch(e){throw this.db.exec("ROLLBACK"),e}}forkSnapshot(e,t={}){const n=t.id??u();return this.withThreadWrite(n,()=>this.forkSnapshotOwned(e,{...t,id:n}))}forkSnapshotOwned(e,t={}){const n=t.id??u();if(this.readOnly)throw new Error("ThreadStore is read-only");const s=new Map(e.checkpoints.map(e=>[e.id,u()])),o=e.checkpoints.map(e=>{const t=k(ce(e,s));return t.version===b?t:k({...t,runningWork:t.runningWork.map(e=>({...e,recoverability:"unavailable"}))})}),a=e.messages.map(e=>T.parse(ce(e,s))),c=this.now().toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.createThread({id:n,...void 0===t.title?void 0===e.thread.title?{}:{title:e.thread.title}:{title:t.title},...void 0===t.model?{}:{model:t.model},origin:{kind:"interactive"},derivedFrom:{threadId:e.thread.id,origin:e.thread.origin,capturedAt:e.capturedAt}}),this.replaceMessagesInTransaction(n,P(a).messages,c);const d=this.db.prepare("INSERT INTO transcript_messages (thread_id, sequence, role, message_json, provenance, created_at) VALUES (?, ?, ?, ?, ?, ?)");for(const t of e.transcript){const e=y.parse(t);d.run(n,e.sequence,e.message.role,JSON.stringify(e.message),e.provenance,e.createdAt)}const E=this.db.prepare("INSERT INTO thread_turn_inputs (thread_id, input_id, transcript_sequence, skill_checkpoint_json, created_at) VALUES (?, ?, ?, ?, ?)");for(const t of e.turnInputs??[]){const e=J.parse(t);E.run(n,u(),e.transcriptSequence,void 0===e.skillCheckpoint?null:JSON.stringify(e.skillCheckpoint),e.createdAt)}this.db.prepare("UPDATE thread_transcript_state SET completeness = ? WHERE thread_id = ?").run(e.transcriptCompleteness,n);const _=this.db.prepare("INSERT INTO tool_executions (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");for(const{sequence:t,...r}of e.toolExecutions){const e=x(r);_.run(n,t,e.id,e.toolCallId,e.agentName,e.toolName,JSON.stringify(e),e.createdAt)}for(const t of e.appOutputs??[])this.insertAppOutput(n,t.operationId,t.result,t.expiresAt);this.enforceAppOutputRetention(n),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(e.nextToolExecutionSequence,n);const h=this.db.prepare("INSERT INTO tool_execution_context_coverage (thread_id, execution_id, representation, transcript_sequence, created_at) VALUES (?, ?, ?, ?, ?)");for(const t of e.toolExecutionCoverage)h.run(n,t.executionId,t.representation,t.transcriptSequence,t.createdAt);const p=this.db.prepare("INSERT INTO compaction_checkpoints (id, thread_id, generation, schema_version, message_from_sequence, message_through_sequence, tool_from_sequence, tool_through_sequence, checkpoint_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");for(const e of o)p.run(e.id,n,e.generation,e.version,e.transcript.messages.fromSequenceExclusive,e.transcript.messages.throughSequence,e.transcript.toolExecutions.fromSequenceExclusive,e.transcript.toolExecutions.throughSequence,JSON.stringify(e),e.createdAt);const T=new r(this.db);if(e.semanticLedger)T.restore(n,e.semanticLedger);else for(const e of o)2===e.version&&T.append(n,e.generation,2===e.version?e.semanticState.scopeId??e.semanticState.goal?.id??"initial":"legacy",e.transcript.toolExecutions.throughSequence,F(e.semanticState),e.id!==o.at(-1)?.id);if(e.contextRecovery){const t=s.get(e.contextRecovery.checkpointId);t&&this.db.prepare("INSERT INTO compaction_recovery\n (thread_id,checkpoint_id,required,unextracted,memo) VALUES (?,?,1,?,?)").run(n,t,Number(e.contextRecovery.unextracted),e.contextRecovery.memo)}for(const t of e.contextRecoveryNotes??[]){const e=i.parse(t),r=s.get(e.checkpointId);r&&this.db.prepare("INSERT INTO compaction_recovery_notes (thread_id,checkpoint_id,notes) VALUES (?,?,?)").run(n,r,e.notes)}return this.db.exec("COMMIT"),n}catch(e){throw this.db.exec("ROLLBACK"),e}}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){return this.withThreadWrite(e,()=>this.updateTitleOwned(e,t))}updateTitleOwned(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}updateModel(e,t){return this.withThreadWrite(e,()=>this.updateModelOwned(e,t))}updateModelOwned(e,t){this.db.prepare("UPDATE threads SET model = ? WHERE id = ?").run(t,e)}deleteThread(e,t={}){return this.withThreadWrite(e,()=>this.deleteThreadOwned(e,t))}deleteThreadOwned(e,t={}){t.onlyIfEmpty&&(this.countMessages(e)>0||this.countTranscriptMessages(e)>0)||this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendRuntimeEvent(e){return this.withThreadWrite(e.threadId,()=>this.appendRuntimeEventOwned(e))}appendRuntimeEventOwned(e){const t=O.parse(e.event),n="tool.completed"===t.type?(({appOutput:e,...t})=>t)(t):t,r=JSON.stringify(n),s=he(e.timestamp),i=Buffer.byteLength(JSON.stringify(t),"utf8")+Buffer.byteLength(s,"utf8")+Buffer.byteLength(e.turnId??"","utf8");if(i>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread)throw new Error("Runtime event exceeds the per-Thread retained byte limit");this.db.exec("BEGIN IMMEDIATE");try{const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e.threadId);if(void 0===n)throw new Error(`Thread "${e.threadId}" 不存在或缺少 Runtime event state`);const o=de(),a=n.next_sequence;return this.db.prepare("INSERT INTO runtime_events\n (thread_id, thread_sequence, event_id, turn_id, event_timestamp,\n event_json, size_bytes, created_at, app_output_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(e.threadId,a,o,e.turnId??null,s,r,i,s,"tool.completed"===t.type?JSON.stringify(t.appOutput):null),this.db.prepare("UPDATE thread_runtime_event_state\n SET next_sequence = ?, latest_event_id = ?,\n retained_count = retained_count + 1,\n retained_bytes = retained_bytes + ?\n WHERE thread_id = ?").run(a+1,o,i,e.threadId),this.enforceRuntimeEventRetentionInTransaction(e.threadId,s),this.db.exec("COMMIT"),{eventId:o,cursor:Ee(n.event_log_id,a,o),threadSequence:a,threadId:e.threadId,...void 0===e.turnId?{}:{turnId:e.turnId},timestamp:s,event:t}}catch(e){throw this.db.exec("ROLLBACK"),e}}getRuntimeEventCursor(e){const t=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);return 0===t.next_sequence||null===t.latest_event_id?null:Ee(t.event_log_id,t.next_sequence-1,v.parse(t.latest_event_id))}resumeRuntimeEvents(e,t){const n=null===t?void 0:_e(t);if(null!==t&&void 0===n)throw new RuntimeEventCursorGapError(t);const r=!this.readOnly&&void 0===this.claims;this.db.exec(r?"BEGIN IMMEDIATE":"BEGIN");try{r&&this.enforceRuntimeEventRetentionInTransaction(e,this.now().toISOString());const s=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===s)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);if(null===t&&s.retained_from_sequence>0)throw new RuntimeEventCursorExpiredError(t);if(null!==t&&void 0!==n&&this.validateRuntimeEventCursor(s,t,n,e),0===s.next_sequence||null===s.latest_event_id)return this.db.exec("COMMIT"),{events:[],throughCursor:null,replayedCount:0};const i=s.next_sequence-1,o=n?.threadSequence??-1,a=this.db.prepare("SELECT *\n FROM runtime_events\n WHERE thread_id = ?\n AND thread_sequence > ?\n AND thread_sequence <= ?\n ORDER BY thread_sequence ASC").all(e,o,i);if(!r){const n=new Date(this.now().getTime()-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),r=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from FROM runtime_events WHERE thread_id = ? AND created_at >= ?").get(e,n);if(o<Math.max(s.retained_from_sequence,r.keep_from??s.next_sequence)-1)throw new RuntimeEventCursorExpiredError(t)}const c=a.map(t=>ue(s.event_log_id,e,t)),d=Ee(s.event_log_id,i,v.parse(s.latest_event_id));return this.db.exec("COMMIT"),{events:c,throughCursor:d,replayedCount:c.length}}catch(e){throw e instanceof RuntimeEventCursorExpiredError||e instanceof RuntimeEventCursorGapError?this.db.exec("COMMIT"):this.db.exec("ROLLBACK"),e}}countRuntimeEvents(e){return this.db.prepare("SELECT COUNT(*) AS count FROM runtime_events WHERE thread_id = ?").get(e).count}validateRuntimeEventCursor(e,t,n,r){if(n.eventLogId!==e.event_log_id||n.threadSequence>=e.next_sequence)throw new RuntimeEventCursorGapError(t);const s=e.retained_from_sequence-1;if(n.threadSequence<s)throw new RuntimeEventCursorExpiredError(t);if(n.threadSequence===s){if(n.eventId!==e.retained_predecessor_event_id)throw new RuntimeEventCursorGapError(t);return}const i=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(r,n.threadSequence);if(void 0===i||i.event_id!==n.eventId)throw new RuntimeEventCursorGapError(t)}enforceRuntimeEventRetentionInTransaction(e,t){const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===n||0===n.retained_count)return;let r=n.retained_from_sequence;if(n.retained_count>RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread){const t=this.db.prepare("SELECT thread_sequence AS keep_from\n FROM runtime_events\n WHERE thread_id = ?\n ORDER BY thread_sequence DESC\n LIMIT 1 OFFSET ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread-1);r=Math.max(r,t?.keep_from??n.next_sequence)}if(n.retained_bytes>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread){const t=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM (\n SELECT thread_sequence,\n SUM(size_bytes) OVER (\n ORDER BY thread_sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM runtime_events\n WHERE thread_id = ?\n )\n WHERE retained_bytes <= ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread);r=Math.max(r,t.keep_from??n.next_sequence)}const s=new Date(Date.parse(t)-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),i=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM runtime_events\n WHERE thread_id = ? AND created_at >= ?").get(e,s);if(r=Math.max(r,i.keep_from??n.next_sequence),r<=n.retained_from_sequence)return;const o=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(e,r-1);if(void 0===o)throw new Error("Runtime event retention boundary is not a continuous retained prefix");const a=this.db.prepare("SELECT COUNT(*) AS removed_count, SUM(size_bytes) AS removed_bytes\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence < ?").get(e,r);this.db.prepare("DELETE FROM runtime_events WHERE thread_id = ? AND thread_sequence < ?").run(e,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET retained_from_sequence = ?, retained_predecessor_event_id = ?,\n retained_count = retained_count - ?,\n retained_bytes = retained_bytes - ?\n WHERE thread_id = ?").run(r,o.event_id,a.removed_count,a.removed_bytes??0,e)}acceptTurnInput(e,t,n){return this.withThreadWrite(e,()=>this.acceptTurnInputOwned(e,t,n))}acceptTurnInputOwned(e,t,n){if(this.readOnly)throw new Error("ThreadStore is read-only");const r=$.parse(t),s=B(T.parse(n));if("user"!==s.role)throw new Error("Accepted turn input must be a user message");this.db.exec("BEGIN IMMEDIATE");try{if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const t=this.getTurnInput(e,r);if(void 0!==t){const n=this.db.prepare("SELECT message_json FROM transcript_messages WHERE thread_id = ? AND sequence = ?").get(e,t.transcriptSequence);if(n?.message_json!==JSON.stringify(s))throw new Error("Turn input identity conflict");return this.db.exec("COMMIT"),t}const n=this.now().toISOString();this.appendMessagesInTransaction(e,[s],n,void 0);const i=this.db.prepare("SELECT MAX(sequence) AS sequence FROM transcript_messages WHERE thread_id = ?").get(e),o=J.parse({inputId:r,transcriptSequence:i?.sequence,createdAt:n});return this.db.prepare("INSERT INTO thread_turn_inputs (thread_id, input_id, transcript_sequence, created_at) VALUES (?, ?, ?, ?)").run(e,r,o.transcriptSequence,n),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}getTurnInput(e,t){const n=this.db.prepare("SELECT input_id, transcript_sequence, skill_checkpoint_json, created_at FROM thread_turn_inputs WHERE thread_id = ? AND input_id = ?").get(e,$.parse(t));return void 0===n?void 0:J.parse({inputId:n.input_id,transcriptSequence:n.transcript_sequence,createdAt:n.created_at,..."string"==typeof n.skill_checkpoint_json?{skillCheckpoint:JSON.parse(n.skill_checkpoint_json)}:{}})}listTurnInputs(e){return this.db.prepare("SELECT input_id FROM thread_turn_inputs WHERE thread_id = ? ORDER BY transcript_sequence").all(e).map(t=>{const n=this.getTurnInput(e,String(t.input_id));if(void 0===n)throw new Error("Turn input receipt disappeared");return n})}getTurnInputSkillCheckpoint(e,t){const n=this.db.prepare("SELECT skill_checkpoint_json FROM thread_turn_inputs WHERE thread_id = ? AND transcript_sequence = ?").get(e,t);return"string"==typeof n?.skill_checkpoint_json?V.parse(JSON.parse(n.skill_checkpoint_json)):void 0}recordTurnSkillCheckpoint(e,t,n){return this.withThreadWrite(e,()=>this.recordTurnSkillCheckpointOwned(e,t,n))}recordTurnSkillCheckpointOwned(e,t,n){if(this.readOnly)throw new Error("ThreadStore is read-only");const r=V.parse(n),s=JSON.stringify(r);this.db.exec("BEGIN IMMEDIATE");try{const n=this.getTurnInput(e,t);if(void 0===n)throw new Error("Turn input receipt not found");if(void 0!==n.skillCheckpoint&&JSON.stringify(n.skillCheckpoint)!==s)throw new Error("Turn input Skill snapshot conflict");this.db.prepare("UPDATE thread_turn_inputs SET skill_checkpoint_json = ? WHERE thread_id = ? AND input_id = ?").run(s,e,t),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}appendMessages(e,t,n={}){return this.withThreadWrite(e,()=>this.appendMessagesOwned(e,t,n))}appendMessagesOwned(e,t,n={}){const r=n.toolExecutionCoverage;if(0===t.length){if(void 0!==r&&r.executionIds.length>0)throw new Error("Tool execution coverage 必须与至少一条 transcript message 原子写入");return}if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);if(void 0!==r&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.rawTranscript&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence)throw new Error("Tool execution coverage representation 无效");const s=r?.executionIds??[],i=new Set;for(const e of s){if("string"!=typeof e||0===e.length)throw new Error("Tool execution coverage execution id 无效");if(i.has(e))throw new Error(`Tool execution coverage execution id 重复: ${e}`);i.add(e)}const o=t.map(e=>B(T.parse(e))),a=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.appendMessagesInTransaction(e,o,a,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}recoverUncoveredToolExecutions(e,t){return this.withThreadWrite(e,()=>this.recoverUncoveredToolExecutionsOwned(e,t))}recoverUncoveredToolExecutionsOwned(e,t){this.db.exec("BEGIN IMMEDIATE");try{if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=this.uncoveredToolExecutionRecords(e,{afterSequence:-1,throughSequence:Number.MAX_SAFE_INTEGER,toolCallId:void 0,limit:Number.MAX_SAFE_INTEGER});if(0===n.length)return void this.db.exec("COMMIT");const r=B(T.parse(t(n))),s={executionIds:n.map(e=>e.id),representation:TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence};return this.appendMessagesInTransaction(e,[r],(new Date).toISOString(),s),this.db.exec("COMMIT"),r}catch(e){throw this.db.exec("ROLLBACK"),e}}appendMessagesInTransaction(e,t,n,r){const s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),i=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)"),o=this.db.prepare("INSERT INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, ?, ?\n FROM tool_executions\n WHERE thread_id = ?\n AND id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE thread_id = ? AND execution_id = ?\n )"),a=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),c=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let d=a.maxIdx+1,E=c.maxSequence+1;for(const r of t){const t=JSON.stringify(r);s.run(e,d,r.role,t,n),i.run(e,E,r.role,t,q[0],n),d+=1,E+=1}const _=E-1;if(void 0!==r){for(const t of r.executionIds){const s=o.run(r.representation,_,n,e,t,e,t);if(1!==Number(s.changes))throw new Error(`Tool execution "${t}" 不属于 Thread "${e}" 或已被 transcript 覆盖`)}this.enforceToolExecutionRetentionInTransaction(e,n)}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}replaceMessages(e,t){return this.withThreadWrite(e,()=>this.replaceMessagesOwned(e,t))}replaceMessagesOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>T.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>T.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const i=B(t);r.run(e,s,i.role,JSON.stringify(i),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){return this.withThreadWrite(e,()=>this.appendToolExecutionOwned(e,t))}appendToolExecutionOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=f(t),r=JSON.stringify(n),s=this.now().toISOString();this.db.exec("BEGIN IMMEDIATE");try{const i=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===i)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const o=i.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,o,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(o+1,e),void 0!==t.appOutput&&this.insertAppOutput(e,t.id,t.appOutput,new Date(Date.parse(s)+l.maxAgeMs).toISOString()),this.enforceAppOutputRetention(e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}readAppOutputRow(e,t){const n=m.parse(JSON.parse(e));return"available"===n.status&&Date.parse(t)<=this.now().getTime()?{status:"expired"}:n}insertAppOutput(e,t,n,r){const s="available"===n.status&&Date.parse(r)<=this.now().getTime()?{status:"expired"}:A(n),i=JSON.stringify(s);this.db.prepare("INSERT INTO operation_app_outputs\n (thread_id, operation_id, result_json, descriptor_json, byte_length, expires_at) VALUES (?, ?, ?, ?, ?, ?)").run(e,t,i,JSON.stringify(R(s)),"available"===s.status?Buffer.byteLength(i,"utf8"):0,r)}enforceAppOutputRetention(e){const t=JSON.stringify({status:"expired"});this.db.prepare("UPDATE operation_app_outputs SET result_json = ?, descriptor_json = ?, byte_length = 0\n WHERE thread_id = ? AND byte_length > 0 AND expires_at <= ?").run(t,t,e,this.now().toISOString()),this.db.prepare("UPDATE operation_app_outputs SET result_json = ?, descriptor_json = ?, byte_length = 0\n WHERE thread_id = ? AND operation_id IN (\n SELECT operation_id FROM (\n SELECT o.operation_id,\n ROW_NUMBER() OVER (ORDER BY e.sequence DESC) AS rank,\n SUM(o.byte_length) OVER (ORDER BY e.sequence DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS bytes\n FROM operation_app_outputs o JOIN tool_executions e ON e.thread_id = o.thread_id AND e.id = o.operation_id\n WHERE o.thread_id = ? AND o.byte_length > 0\n ) WHERE rank > ? OR bytes > ?\n )").run(t,t,e,e,l.threadRecords,l.threadBytes)}getAppOutputDescriptor(e,t){if(void 0===this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'operation_app_outputs'").get())return void 0===this.db.prepare("SELECT 1 FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t)?void 0:{status:"not_provided"};const n=this.db.prepare("SELECT o.descriptor_json, o.expires_at FROM tool_executions e\n LEFT JOIN operation_app_outputs o ON o.thread_id = e.thread_id AND o.operation_id = e.id\n WHERE e.thread_id = ? AND e.id = ?").get(e,t);if(void 0===n)return;if(null===n.descriptor_json)return{status:"not_provided"};const r=N.parse(JSON.parse(n.descriptor_json));return"available"===r.status&&null!==n.expires_at&&Date.parse(n.expires_at)<=this.now().getTime()?{status:"expired"}:r}getAppOutput(e,t){const n=this.getAppOutputDescriptor(e,t);if(void 0===n||"available"!==n.status)return n;const r=this.db.prepare("SELECT result_json FROM operation_app_outputs WHERE thread_id = ? AND operation_id = ?").get(e,t);return void 0===r?{status:"not_provided"}:m.parse(JSON.parse(r.result_json))}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )").run(e,n);r=Number(s.changes)>0;const i=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(i.length>0){const t=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND sequence = ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )");for(const n of i){const s=t.run(e,n.sequence);r=Number(s.changes)>0||r}}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence}))}listUncoveredToolExecutions(e,t={}){const{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);return r<=n?[]:this.uncoveredToolExecutionRecords(e,{afterSequence:n,throughSequence:r,toolCallId:t.toolCallId,limit:s})}resolveToolExecutionListRange(e){const t=e.afterSequence??-1,n=e.throughSequence??Number.MAX_SAFE_INTEGER,r=e.limit??Q;if(!Number.isInteger(t)||t<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(r)||r<1||r>Z)throw new Error(`limit 必须是 1-${String(Z)} 之间的整数`);if(!Number.isInteger(n)||n<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");return{afterSequence:t,throughSequence:n,limit:r}}uncoveredToolExecutionRecords(e,t){return this.db.prepare("SELECT tool_executions.sequence, tool_executions.record_json\n FROM tool_executions\n LEFT JOIN tool_execution_context_coverage\n ON tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n WHERE tool_executions.thread_id = ?\n AND tool_executions.sequence > ?\n AND tool_executions.sequence <= ?\n AND (? IS NULL OR tool_executions.tool_call_id = ?)\n AND tool_execution_context_coverage.execution_id IS NULL\n ORDER BY tool_executions.sequence ASC\n LIMIT ?").all(e,t.afterSequence,t.throughSequence,t.toolCallId??null,t.toolCallId??null,t.limit).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??Q;this.validateRecentPage(n,r,Z);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),i=s.length>r,o=s.slice(0,r).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:o,nextBeforeSequence:i?o[0]?.sequence:void 0}}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...x(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===w[0]?w[0]:w[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??ee;if(this.validateTranscriptRange(n,r,s,te),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??ee;this.validateRecentPage(n,r,te);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),i=s.length>r,o=s.slice(0,r).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:o,nextBeforeSequence:i?o[0]?.sequence:void 0}}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,oe(n))}catch(e){if(e instanceof M)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=oe(n);if(t.version===b||"valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof M)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,oe(n))}catch(e){if(e instanceof M)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:P(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){return this.withThreadWrite(e,()=>this.commitCompactionOwned(e,t))}commitCompactionOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>T.parse(e)),r=t.expectedActiveMessages.map(e=>T.parse(e)),s=P(n);if(s.status!==H.valid){const e=[...new Set([...s.removedToolCallIds,...s.removedToolResultIds])].sort();throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const i=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=P(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const o=this.getLatestCheckpoint(e);if(o?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const a=this.legacySummaryFragmentsToArchive(e,s,n),c=this.maxTranscriptMessageSequence(e),d=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration FROM compaction_checkpoints WHERE thread_id = ?").get(e),E={messages:{fromSequenceExclusive:o?.transcript.messages.throughSequence??-1,throughSequence:c+a.length},toolExecutions:{fromSequenceExclusive:o?.transcript.toolExecutions.throughSequence??-1,throughSequence:this.maxToolExecutionSequence(e)},completeness:this.getTranscriptCompleteness(e)},_=new Set(r.flatMap(e=>Array.isArray(e.content)?e.content.flatMap(e=>"tool-call"===e.type?[e.toolCallId]:[]):[])),h=this.listRecentToolExecutions(e,{limit:Z}).entries.filter(e=>_.has(e.toolCallId)),u=D({summary:t.summary,generation:d.maxGeneration+1,transcript:E,...void 0===o?{}:{previousCheckpointId:o.id},toolState:U(r,h,32,E.completeness),createdAt:i});t.validateCheckpoint?.(u);for(const[t,n]of a.entries())this.db.prepare("INSERT INTO transcript_messages\n (thread_id,sequence,role,message_json,provenance,created_at) VALUES (?,?,?,?,?,?)").run(e,c+t+1,n.role,JSON.stringify(n),q[1],i);return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version, message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence, checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u.id,e,u.generation,u.version,E.messages.fromSequenceExclusive,E.messages.throughSequence,E.toolExecutions.fromSequenceExclusive,E.toolExecutions.throughSequence,JSON.stringify(u),u.createdAt),this.replaceMessagesInTransaction(e,n,i),this.db.exec("COMMIT"),u}catch(e){throw this.db.exec("ROLLBACK"),e}}legacySummaryFragmentsToArchive(e,t,n){const r=e=>"string"==typeof e.content?X(e.content):void 0,s=new Set(n.map(r)),i=new Map,o=this.db.prepare("SELECT 1 FROM transcript_messages WHERE thread_id=? AND json_extract(message_json, '$.content') IN (?, ?) LIMIT 1");for(const n of t){const t=r(n);if(void 0===t||0===t.length||s.has(t))continue;const a=C(t),c=W(a);o.get(e,String(n.content),String(c.content))||i.set(a,c)}return[...i.values()]}getContextRecovery(e){if(this.getLatestCheckpoint(e)?.version===b)return;const t=new r(this.db),n=t.recovery(e);if(!n)return;const s=this.getCheckpoint(e,n.checkpointId);if(!s)throw new Error("Recovery checkpoint is missing");const i=2===s.version?s.semanticState.goal?.id:void 0;return{...n,requiredItems:t.rows(e,s.generation).flatMap(e=>"active"===e.status&&("constraint"===e.category||"goal"===e.category&&e.item_id===i)?[{itemId:e.item_id,version:e.generation,category:e.category}]:[])}}getSemanticLedgerRows(e){return new r(this.db).rows(e,this.getLatestCheckpoint(e)?.generation??0)}readCheckpointTranscript(e,t){if(!TRANSCRIPT_ENTRY_KINDS.includes(t.kind))throw new Error("kind 必须是 message、tool_execution 或 legacy_context");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??ee,s="legacy_context"===t.kind?{fromSequenceExclusive:-1,throughSequence:Number.MAX_SAFE_INTEGER}:"message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,i=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(i,s.throughSequence,r,ne);const o="legacy_context"===t.kind?this.listLegacyContextEntries(e,n,i,r+1):"message"===t.kind?this.listTranscriptMessages(e,{afterSequence:i,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:i,throughSequence:s.throughSequence,limit:Math.min(r+1,Z)}).map(e=>({kind:"tool_execution",...e})),a=o.length>r,c=a?o.slice(0,r):o,d=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...a&&void 0!==d?{nextAfterSequence:d}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}listLegacyContextEntries(e,t,n,s){if(t.version===b)return[];const i=new r(this.db),o=[];if(n<0){const n=i.recovery(e),r=this.db.prepare("SELECT notes FROM compaction_recovery_notes WHERE thread_id=? AND checkpoint_id=?").get(e,t.id);o.push({kind:"legacy_context",sequence:0,value:{checkpoint:t,...n?.checkpointId===t.id?{memo:n.memo}:{},..."string"==typeof r?.notes?{notes:r.notes}:{}}})}for(const r of i.page(e,t.generation,n,s-o.length))o.push({kind:"legacy_context",sequence:r.sequence,value:{ledgerItem:j.parse({category:r.category,item:JSON.parse(r.item_json)}),generation:r.generation,status:r.status,scope:r.scope,resolution:r.resolution_json??null}});return o}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=w[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:w[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}retainSession(e){if(void 0===this.claims)return;if(this.sessionReservations.has(e))throw new t(e);const n=this.claims.acquire(e);try{if(this.hasThread(e)){this.db.exec("BEGIN IMMEDIATE");try{const t=this.now().toISOString();this.enforceAppOutputRetention(e),this.enforceToolExecutionRetentionInTransaction(e,t),this.enforceRuntimeEventRetentionInTransaction(e,t),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}catch(e){throw n.release(),e}this.sessionReservations.set(e,n.token);let r,s=!1;return{...n,release:()=>{if(void 0!==r)throw r.cause;if(!s){try{n.release()}catch(e){throw r={cause:e},e}s=!0,this.sessionReservations.get(e)===n.token&&this.sessionReservations.delete(e)}}}}retainThread(e){if(this.readOnly)throw new Error("ThreadStore is read-only");return this.claims?.acquire(e)}withThreadWrite(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");const n=this.retainThread(e);try{return n?.assert(),t()}finally{n?.release()}}close(){this.claims?.close(),this.db.close()}}export const THREAD_LIBRARY_STATUSES=["new","current","needs_migration","unsupported"];export class ThreadLibraryMigrationRequiredError extends Error{preflight;constructor(e){super(`会话库需要显式备份升级或受支持的新版本:${e.status} (v${String(e.schemaVersion)})。请停止所有客户端后确认迁移;未修改历史。`),this.name="ThreadLibraryMigrationRequiredError",this.preflight=e}}export function inspectThreadLibrary(e=defaultThreadsDir()){const t=h(expandTilde(e),"threads.db");if(!a(t))return{databasePath:t,schemaVersion:null,supportedSchemaVersion:z,status:"new"};const n=d(t),r=E(n,{bigint:!0}),s=new p(n,{readOnly:!0});try{s.exec(`PRAGMA busy_timeout = ${String(re)}; PRAGMA query_only = ON;`);const e=s.prepare("PRAGMA user_version").get(),t=e?.user_version;if("number"!=typeof t)throw new Error("Invalid ThreadStore schema version");return{databasePath:n,physicalIdentity:`inode:${r.dev}:${r.ino}:${r.birthtimeNs}`,writableAliasSafe:1n===r.nlink,schemaVersion:t,supportedSchemaVersion:z,status:t===z?"current":t>z?"unsupported":"needs_migration"}}finally{s.close()}}export function migrateThreadLibrary(t){if(!0!==t.confirmStoppedLegacyClients)throw new Error("Migration requires explicit confirmation that legacy clients are stopped");const r=inspectThreadLibrary(t.dir);if("needs_migration"!==r.status)throw new ThreadLibraryMigrationRequiredError(r);const s=new e(r.databasePath,t.ownership);let i;try{i=s.maintenance(),i.assert();const e=inspectThreadLibrary(t.dir);if("needs_migration"!==e.status)throw new ThreadLibraryMigrationRequiredError(e);const r=n(e.databasePath),a=`${r}.backup-${u()}.sqlite`,c=new p(r,{readOnly:!0});try{c.prepare("VACUUM INTO ?").run(a)}finally{c.close()}"win32"!==process.platform&&o(a,384);const d=new p(a,{readOnly:!0});try{const t=d.prepare("PRAGMA quick_check").all();if(1!==t.length||"ok"!==t[0]?.quick_check||d.prepare("PRAGMA user_version").get()?.user_version!==e.schemaVersion)throw new Error("SQLite backup verification failed; original history was not migrated")}finally{d.close()}i.assert();return new ThreadStore(t.dir).close(),{backupPath:a,preflight:inspectThreadLibrary(t.dir)}}finally{i?.release(),s.close()}}
|
|
1
|
+
import{SessionClaims as e,SessionOccupiedError as t,canonicalSessionDatabasePath as n}from"../ownership/session-claims.js";import{SemanticLedger as r,SEMANTIC_LEDGER_SQL as s,contextRecoveryNoteSchema as i}from"./semantic-ledger.js";import{chmodSync as o,existsSync as a,mkdirSync as c,realpathSync as d,statSync as E}from"node:fs";import{homedir as _}from"node:os";import{resolve as h}from"node:path";import{randomUUID as u}from"node:crypto";import{DatabaseSync as p}from"node:sqlite";import{modelMessageSchema as T}from"ai";import{APP_OUTPUT_LIMITS as l,appOutputResultSchema as m,appOutputDescriptorSchema as N,describeAppOutput as R,RUNTIME_V13_MAX_DURABLE_EVENT_RECORD_BYTES as S,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as g,runtimeDurableEventV16Schema as O,runtimeEventCursorSchema as I,runtimeEventIdSchema as v,timestampSchema as L}from"@roll-agent/protocol";import{parsePersistedToolExecutionRecord as x,prepareToolExecutionRecordForPersistence as f,prepareAppOutputForPersistence as A,redactSecretText as C}from"../tool-bridge/tool-execution-record.js";import{TASK_SUMMARY_CHECKPOINT_VERSION as b,COMPACTION_TRANSCRIPT_COMPLETENESS as w,TRANSCRIPT_MESSAGE_PROVENANCES as q,UnsupportedCompactionCheckpointVersionError as M,archivedTranscriptMessageSchema as y,createSummaryCheckpoint as D,buildCompactionToolState as U,parseCompactionCheckpoint as k}from"../engine/compaction-checkpoint.js";import{listSemanticArchiveItems as F,semanticArchiveItemSchema as j}from"../engine/compaction-semantic-state.js";import{createLegacyCompactionTranscriptMessage as W,readCompactionSummaryPayload as X}from"../engine/compactor.js";import{ACTIVE_TOOL_PROTOCOL_REPAIR_STATUS as H,repairActiveToolProtocol as P}from"../engine/tool-protocol-repair.js";import{sanitizePersistedExplicitSkillCheckpoint as B}from"../engine/explicit-skill-context.js";import{threadOriginSchema as G,threadDerivedFromSchema as Y,scheduledThreadOriginSchema as K}from"./thread-origin.js";import{acceptedTurnInputSchema as J,turnInputIdSchema as $,turnInputSkillCheckpointSchema as V}from"./turn-input.js";export const THREAD_STORE_SCHEMA_VERSION=11;const z=11,Q=100,Z=500,ee=50,te=500,ne=100,re=15e3;export const TOOL_EXECUTION_RETENTION_POLICY={maxBytesPerThread:16777216,maxRecordsPerThread:2e3,maxAgeMs:2592e6};export const RUNTIME_EVENT_RETENTION_POLICY={maxBytesPerThread:S,maxRecordsPerThread:g,maxAgeMs:2592e6};export class RuntimeEventCursorExpiredError extends Error{cursor;constructor(e){super("Runtime event cursor is older than the retained event prefix"),this.name="RuntimeEventCursorExpiredError",this.cursor=e}}export class RuntimeEventCursorGapError extends Error{cursor;constructor(e){super("Runtime event cursor does not belong to the current Thread event log"),this.name="RuntimeEventCursorGapError",this.cursor=e}}export const TRANSCRIPT_ENTRY_KINDS=["message","tool_execution","legacy_context"];export const TOOL_EXECUTION_CONTEXT_REPRESENTATIONS={rawTranscript:"raw_transcript",recoveryEvidence:"recovery_evidence"};const se="legacy_assumed";export function expandTilde(e){return e.startsWith("~/")?h(_(),e.slice(2)):e}export function defaultThreadsDir(){return h(_(),".roll-agent","threads")}function ie(e){return e.prepare("PRAGMA foreign_key_list(messages)").all().some(e=>"threads"===e.table&&"thread_id"===e.from&&"id"===e.to&&"CASCADE"===e.on_delete.toUpperCase())}function oe(e){if(Number.isInteger(e.schema_version)&&e.schema_version>b)throw new M(e.schema_version);if(!Number.isInteger(e.schema_version))throw new Error("Invalid persisted compaction checkpoint schema version");const t=k(JSON.parse(e.checkpoint_json));if(t.version!==e.schema_version)throw new Error(`Compaction checkpoint schema version mismatch: row v${String(e.schema_version)}, payload v${String(t.version)}`);return t}function ae(e){return{id:e.id,title:e.title??void 0,model:e.model??void 0,createdAt:e.created_at,updatedAt:e.updated_at,origin:G.parse(e.origin_json?JSON.parse(e.origin_json):{kind:"interactive"}),...e.derived_from_json?{derivedFrom:Y.parse(JSON.parse(e.derived_from_json))}:{}}}function ce(e,t){return"string"==typeof e?t.get(e)??e:Array.isArray(e)?e.map(e=>ce(e,t)):"object"==typeof e&&null!==e?Object.fromEntries(Object.entries(e).map(([e,n])=>[e,ce(n,t)])):e}function de(){return v.parse(u())}function Ee(e,t,n){return I.parse(`rte1:${e}:${String(t)}:${n}`)}function _e(e){const t=I.safeParse(e);if(!t.success)return;const n=t.data.split(":"),r=Number(n[2]),s=v.safeParse(n[3]);return void 0===n[1]||!Number.isSafeInteger(r)||r<0||!s.success?void 0:{eventLogId:n[1],threadSequence:r,eventId:s.data}}function he(e){return new Date(Date.parse(L.parse(e))).toISOString()}function ue(e,t,n){const r=v.parse(n.event_id),s=O.parse(JSON.parse(n.event_json));return{eventId:r,cursor:Ee(e,n.thread_sequence,r),threadSequence:n.thread_sequence,threadId:t,...null===n.turn_id?{}:{turnId:n.turn_id},timestamp:n.event_timestamp,event:"tool.completed"===s.type&&null!=n.app_output_json?{...s,appOutput:N.parse(JSON.parse(n.app_output_json))}:s}}export class ThreadStore{db;now;readOnly;claims;sessionReservations=new Map;constructor(t=defaultThreadsDir(),n={}){this.now=n.now??(()=>new Date),this.readOnly=n.readOnly??!1;const r=expandTilde(t);if(this.claims=void 0,n.readOnly){this.db=new p(h(r,"threads.db"),{readOnly:!0});try{this.db.exec(`PRAGMA busy_timeout = ${String(re)}; PRAGMA query_only = ON;`);const e=this.db.prepare("PRAGMA user_version").get();if(e.user_version>z)throw new Error(`Unsupported ThreadStore schema v${String(e.user_version)}`)}catch(e){throw this.db.close(),e}return}a(r)||c(r,{recursive:!0,mode:448}),"win32"!==process.platform&&o(r,448),this.claims=void 0===n.ownership?void 0:new e(h(r,"threads.db"),n.ownership);const s=this.claims?.databasePath??h(r,"threads.db");let i,d;try{d=this.claims?.beginLibraryInitialization(()=>{const e=inspectThreadLibrary(r);if("current"!==e.status&&"new"!==e.status)throw new ThreadLibraryMigrationRequiredError(e);return"new"===e.status}),this.db=i=new p(s),"win32"!==process.platform&&o(s,384),this.init(void 0!==n.ownership)}catch(e){throw i?.close(),d?.(),this.claims?.close(),e}finally{d?.()}}init(e=!1){this.db.exec(`PRAGMA busy_timeout = ${String(re)};\n PRAGMA foreign_keys = ON;\n PRAGMA secure_delete = ON;`);const t=this.db.prepare("PRAGMA user_version").get();if(t.user_version>z)throw new Error(`ThreadStore schema v${String(t.user_version)} 高于当前支持的 v${String(z)}`);if(!e||t.user_version!==z){if(e&&0!==t.user_version)throw new Error("Existing history requires explicit backed-up migration");this.db.exec("BEGIN IMMEDIATE");try{if(this.db.exec("CREATE TABLE IF NOT EXISTS threads (\n id TEXT PRIMARY KEY,\n title TEXT,\n model TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS messages (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );"),t.user_version<7){const e=this.db.prepare("PRAGMA table_info(threads)").all();for(const t of["origin_json","derived_from_json"])e.some(e=>e.name===t)||this.db.exec(`ALTER TABLE threads ADD COLUMN ${t} TEXT`)}if(ie(this.db)||this.db.exec("CREATE TABLE messages_with_thread_fk (\n thread_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n role TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, idx),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n INSERT INTO messages_with_thread_fk\n (thread_id, idx, role, content_json, created_at)\n SELECT messages.thread_id, messages.idx, messages.role,\n messages.content_json, messages.created_at\n FROM messages\n INNER JOIN threads ON threads.id = messages.thread_id;\n DROP TABLE messages;\n ALTER TABLE messages_with_thread_fk RENAME TO messages;"),this.db.exec("CREATE TABLE IF NOT EXISTS tool_executions (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n id TEXT NOT NULL,\n tool_call_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n record_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, id),\n UNIQUE (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS operation_app_outputs (\n thread_id TEXT NOT NULL,\n operation_id TEXT NOT NULL,\n result_json TEXT NOT NULL,\n descriptor_json TEXT NOT NULL,\n byte_length INTEGER NOT NULL,\n expires_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, operation_id),\n FOREIGN KEY (thread_id, operation_id) REFERENCES tool_executions(thread_id, id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_tool_executions_thread_call\n ON tool_executions(thread_id, tool_call_id, sequence);\n CREATE TABLE IF NOT EXISTS thread_tool_execution_state (\n thread_id TEXT PRIMARY KEY,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS transcript_messages (\n thread_id TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n role TEXT NOT NULL,\n message_json TEXT NOT NULL,\n provenance TEXT NOT NULL CHECK (provenance IN ('native', 'legacy_snapshot')),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_turn_inputs (\n thread_id TEXT NOT NULL,\n input_id TEXT NOT NULL,\n transcript_sequence INTEGER NOT NULL,\n skill_checkpoint_json TEXT,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, input_id),\n UNIQUE (thread_id, transcript_sequence),\n FOREIGN KEY (thread_id, transcript_sequence)\n REFERENCES transcript_messages(thread_id, sequence) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS thread_transcript_state (\n thread_id TEXT PRIMARY KEY,\n completeness TEXT NOT NULL CHECK (completeness IN ('complete', 'legacy_snapshot')),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS tool_execution_context_coverage (\n thread_id TEXT NOT NULL,\n execution_id TEXT NOT NULL,\n representation TEXT NOT NULL\n CHECK (representation IN ('raw_transcript', 'recovery_evidence', 'legacy_assumed')),\n transcript_sequence INTEGER,\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, execution_id),\n CHECK (\n (representation = 'legacy_assumed' AND transcript_sequence IS NULL)\n OR (representation <> 'legacy_assumed' AND transcript_sequence IS NOT NULL)\n ),\n FOREIGN KEY (thread_id, execution_id)\n REFERENCES tool_executions(thread_id, id) ON DELETE CASCADE,\n FOREIGN KEY (thread_id, transcript_sequence)\n REFERENCES transcript_messages(thread_id, sequence) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS compaction_checkpoints (\n id TEXT PRIMARY KEY,\n thread_id TEXT NOT NULL,\n generation INTEGER NOT NULL,\n schema_version INTEGER NOT NULL,\n message_from_sequence INTEGER NOT NULL,\n message_through_sequence INTEGER NOT NULL,\n tool_from_sequence INTEGER NOT NULL,\n tool_through_sequence INTEGER NOT NULL,\n checkpoint_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n UNIQUE (thread_id, generation),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_thread_generation\n ON compaction_checkpoints(thread_id, generation DESC);\n CREATE TABLE IF NOT EXISTS thread_runtime_event_state (\n thread_id TEXT PRIMARY KEY,\n event_log_id TEXT NOT NULL UNIQUE,\n next_sequence INTEGER NOT NULL CHECK (next_sequence >= 0),\n retained_from_sequence INTEGER NOT NULL\n CHECK (retained_from_sequence >= 0 AND retained_from_sequence <= next_sequence),\n retained_predecessor_event_id TEXT,\n latest_event_id TEXT,\n retained_count INTEGER NOT NULL CHECK (retained_count >= 0),\n retained_bytes INTEGER NOT NULL CHECK (retained_bytes >= 0),\n CHECK (\n (next_sequence = 0\n AND retained_from_sequence = 0\n AND retained_predecessor_event_id IS NULL\n AND latest_event_id IS NULL)\n OR (next_sequence > 0 AND latest_event_id IS NOT NULL)\n ),\n CHECK (\n (retained_from_sequence = 0 AND retained_predecessor_event_id IS NULL)\n OR (retained_from_sequence > 0 AND retained_predecessor_event_id IS NOT NULL)\n ),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE TABLE IF NOT EXISTS runtime_events (\n thread_id TEXT NOT NULL,\n thread_sequence INTEGER NOT NULL CHECK (thread_sequence >= 0),\n event_id TEXT NOT NULL UNIQUE,\n turn_id TEXT,\n event_timestamp TEXT NOT NULL,\n event_json TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes > 0),\n created_at TEXT NOT NULL,\n PRIMARY KEY (thread_id, thread_sequence),\n FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE\n );\n CREATE INDEX IF NOT EXISTS idx_runtime_events_thread_created\n ON runtime_events(thread_id, created_at, thread_sequence);"),this.db.prepare("PRAGMA table_info(runtime_events)").all().some(e=>"app_output_json"===e.name)||this.db.exec("ALTER TABLE runtime_events ADD COLUMN app_output_json TEXT"),this.db.exec("INSERT OR IGNORE INTO thread_tool_execution_state (thread_id, next_sequence)\n SELECT id, 0 FROM threads;\n UPDATE thread_tool_execution_state\n SET next_sequence = MAX(\n next_sequence,\n COALESCE((\n SELECT MAX(tool_executions.sequence) + 1\n FROM tool_executions\n WHERE tool_executions.thread_id = thread_tool_execution_state.thread_id\n ), 0)\n );"),t.user_version<3&&this.db.exec("INSERT OR IGNORE INTO thread_transcript_state (thread_id, completeness)\n SELECT id, 'legacy_snapshot' FROM threads;\n INSERT OR IGNORE INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n SELECT thread_id, idx, role, content_json, 'legacy_snapshot', created_at\n FROM messages;"),t.user_version<4&&(this.migrateToolExecutionPersistenceInTransaction(),this.migrateExplicitSkillCheckpointPersistenceInTransaction()),t.user_version<6&&this.db.prepare("INSERT OR IGNORE INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, NULL, created_at\n FROM tool_executions").run(se),this.db.exec(s),t.user_version<9){const e=new r(this.db),t=this.db.prepare("SELECT thread_id, schema_version, checkpoint_json FROM compaction_checkpoints ORDER BY generation").all();for(const n of t){if("string"!=typeof n.thread_id||"string"!=typeof n.checkpoint_json)continue;let t;try{t=oe({schema_version:Number(n.schema_version),checkpoint_json:n.checkpoint_json})}catch(e){if(e instanceof M)throw e;this.db.prepare("UPDATE thread_transcript_state SET completeness = 'legacy_snapshot' WHERE thread_id = ?").run(n.thread_id);continue}if(2!==t.version)continue;const r=this.getLatestCheckpoint(n.thread_id);e.append(n.thread_id,t.generation,2===t.version?t.semanticState.scopeId??t.semanticState.goal?.id??"initial":"legacy",t.transcript.toolExecutions.throughSequence,F(t.semanticState),r?.id!==t.id)}}this.initializeMissingRuntimeEventStatesInTransaction();const e=this.now().toISOString();this.enforceAllToolExecutionRetentionInTransaction(e),this.enforceAllRuntimeEventRetentionInTransaction(e),this.db.exec(`PRAGMA user_version = ${String(z)};`),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}migrateToolExecutionPersistenceInTransaction(){const e=this.db.prepare("SELECT rowid AS storage_rowid, record_json\n FROM tool_executions\n WHERE rowid > ?\n ORDER BY rowid ASC\n LIMIT 100"),t=this.db.prepare("UPDATE tool_executions\n SET tool_call_id = ?, agent_name = ?, tool_name = ?, record_json = ?, created_at = ?\n WHERE rowid = ?");let n=0;for(;;){const r=e.all(n);if(0===r.length)break;for(const e of r){const r=x(JSON.parse(e.record_json));t.run(r.toolCallId,r.agentName,r.toolName,JSON.stringify(r),r.createdAt,e.storage_rowid),n=e.storage_rowid}}}enforceAllToolExecutionRetentionInTransaction(e){const t=this.db.prepare("SELECT DISTINCT thread_id AS id FROM tool_executions").all();for(const{id:n}of t)this.enforceAppOutputRetention(n),this.enforceToolExecutionRetentionInTransaction(n,e)}migrateExplicitSkillCheckpointPersistenceInTransaction(){const e=[{table:"messages",column:"content_json"},{table:"transcript_messages",column:"message_json"}];for(const t of e){const e=this.db.prepare(`SELECT rowid AS storage_rowid, ${t.column} AS message_json\n FROM ${t.table}\n WHERE ${t.column} LIKE '%"modelUserContent"%'`).all(),n=this.db.prepare(`UPDATE ${t.table} SET ${t.column} = ? WHERE rowid = ?`);for(const t of e){const e=T.parse(JSON.parse(t.message_json)),r=B(e);n.run(JSON.stringify(r),t.storage_rowid)}}}initializeMissingRuntimeEventStatesInTransaction(){const e=this.db.prepare("SELECT threads.id\n FROM threads\n LEFT JOIN thread_runtime_event_state\n ON thread_runtime_event_state.thread_id = threads.id\n WHERE thread_runtime_event_state.thread_id IS NULL").all(),t=this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)");for(const n of e)t.run(n.id,u())}enforceAllRuntimeEventRetentionInTransaction(e){const t=this.db.prepare("SELECT thread_id AS id FROM thread_runtime_event_state").all();for(const n of t)this.enforceRuntimeEventRetentionInTransaction(n.id,e)}createThread(e={}){const t=e.id??u();return this.withThreadWrite(t,()=>this.createThreadOwned({...e,id:t}))}createThreadOwned(e={}){const t=e.id??u();if(this.readOnly)throw new Error("ThreadStore is read-only");const n=this.now().toISOString(),r=G.parse(e.origin??{kind:"interactive"}),s=void 0===e.derivedFrom?void 0:Y.parse(e.derivedFrom);this.db.exec("SAVEPOINT create_thread");try{return this.db.prepare("INSERT INTO threads (id, title, model, created_at, updated_at, origin_json, derived_from_json) VALUES (?, ?, ?, ?, ?, ?, ?)").run(t,e.title??null,e.model??null,n,n,JSON.stringify(r),void 0===s?null:JSON.stringify(s)),this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness) VALUES (?, ?)").run(t,w[0]),this.db.prepare("INSERT INTO thread_tool_execution_state (thread_id, next_sequence) VALUES (?, 0)").run(t),this.db.prepare("INSERT INTO thread_runtime_event_state\n (thread_id, event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id, retained_count, retained_bytes)\n VALUES (?, ?, 0, 0, NULL, NULL, 0, 0)").run(t,u()),this.db.exec("RELEASE create_thread"),t}catch(e){throw this.db.exec("ROLLBACK TO create_thread; RELEASE create_thread"),e}}hasThread(e){return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ?").get(e)}getThread(e){const t=this.db.prepare("SELECT * FROM threads WHERE id = ?").get(e);return t?ae(t):void 0}listThreads(e={}){return this.db.prepare("SELECT * FROM threads ORDER BY updated_at DESC, rowid DESC").all().map(ae).filter(t=>void 0===e.origin||t.origin.kind===e.origin)}backfillScheduledOrigin(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");return void 0!==this.db.prepare("SELECT 1 FROM threads WHERE id = ? AND origin_json IS NULL").get(e)&&this.withThreadWrite(e,()=>this.backfillScheduledOriginOwned(e,t))}backfillScheduledOriginOwned(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");const n=K.parse(t);return this.db.prepare("UPDATE threads SET origin_json = ? WHERE id = ? AND origin_json IS NULL").run(JSON.stringify(n),e).changes>0}readSnapshot(e){this.db.exec("BEGIN");try{const t=this.getThread(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在`);const n=this.now().toISOString(),s=this.getMessages(e),i=e=>void 0!==this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(e),o=i("transcript_messages")?this.db.prepare("SELECT sequence, provenance, created_at, message_json FROM transcript_messages WHERE thread_id = ? ORDER BY sequence").all(e).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})):s.map((e,n)=>y.parse({sequence:n,message:e,provenance:"legacy_snapshot",createdAt:t.createdAt})),a=i("tool_executions")?this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? ORDER BY sequence").all(e).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence})):[],c=i("compaction_checkpoints")?this.db.prepare("SELECT schema_version, checkpoint_json FROM compaction_checkpoints WHERE thread_id = ? ORDER BY generation").all(e).map(oe):[],d=i("tool_execution_context_coverage")?this.db.prepare("SELECT execution_id AS executionId, representation, transcript_sequence AS transcriptSequence, created_at AS createdAt FROM tool_execution_context_coverage WHERE thread_id = ?").all(e):a.map(e=>({executionId:e.id,representation:"legacy_assumed",transcriptSequence:null,createdAt:n})),E=i("thread_tool_execution_state")?this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e):void 0,_=i("compaction_recovery")?new r(this.db).recovery(e):void 0,h={...i("compaction_semantic_items")?{semanticLedger:new r(this.db).versions(e)}:{},..._?{contextRecovery:_}:{},...i("compaction_recovery_notes")?{contextRecoveryNotes:new r(this.db).notes(e)}:{},thread:t,capturedAt:n,appOutputs:i("operation_app_outputs")?this.db.prepare("SELECT operation_id, result_json, expires_at FROM operation_app_outputs WHERE thread_id = ?").all(e).map(e=>({operationId:e.operation_id,result:this.readAppOutputRow(e.result_json,e.expires_at),expiresAt:e.expires_at})):[],messages:s,transcript:o,turnInputs:i("thread_turn_inputs")?this.listTurnInputs(e):[],toolExecutions:a,checkpoints:c,toolExecutionCoverage:d,nextToolExecutionSequence:E?.next_sequence??(a.at(-1)?.sequence??-1)+1,transcriptCompleteness:i("thread_transcript_state")?this.getTranscriptCompleteness(e):"legacy_snapshot"};return this.db.exec("COMMIT"),h}catch(e){throw this.db.exec("ROLLBACK"),e}}forkSnapshot(e,t={}){const n=t.id??u();return this.withThreadWrite(n,()=>this.forkSnapshotOwned(e,{...t,id:n}))}forkSnapshotOwned(e,t={}){const n=t.id??u();if(this.readOnly)throw new Error("ThreadStore is read-only");const s=new Map(e.checkpoints.map(e=>[e.id,u()])),o=e.checkpoints.map(e=>{const t=k(ce(e,s));return t.version===b?t:k({...t,runningWork:t.runningWork.map(e=>({...e,recoverability:"unavailable"}))})}),a=e.messages.map(e=>T.parse(ce(e,s))),c=this.now().toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.createThread({id:n,...void 0===t.title?void 0===e.thread.title?{}:{title:e.thread.title}:{title:t.title},...void 0===t.model?{}:{model:t.model},origin:{kind:"interactive"},derivedFrom:{threadId:e.thread.id,origin:e.thread.origin,capturedAt:e.capturedAt}}),this.replaceMessagesInTransaction(n,P(a).messages,c);const d=this.db.prepare("INSERT INTO transcript_messages (thread_id, sequence, role, message_json, provenance, created_at) VALUES (?, ?, ?, ?, ?, ?)");for(const t of e.transcript){const e=y.parse(t);d.run(n,e.sequence,e.message.role,JSON.stringify(e.message),e.provenance,e.createdAt)}const E=this.db.prepare("INSERT INTO thread_turn_inputs (thread_id, input_id, transcript_sequence, skill_checkpoint_json, created_at) VALUES (?, ?, ?, ?, ?)");for(const t of e.turnInputs??[]){const e=J.parse(t);E.run(n,u(),e.transcriptSequence,void 0===e.skillCheckpoint?null:JSON.stringify(e.skillCheckpoint),e.createdAt)}this.db.prepare("UPDATE thread_transcript_state SET completeness = ? WHERE thread_id = ?").run(e.transcriptCompleteness,n);const _=this.db.prepare("INSERT INTO tool_executions (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");for(const{sequence:t,...r}of e.toolExecutions){const e=x(r);_.run(n,t,e.id,e.toolCallId,e.agentName,e.toolName,JSON.stringify(e),e.createdAt)}for(const t of e.appOutputs??[])this.insertAppOutput(n,t.operationId,t.result,t.expiresAt);this.enforceAppOutputRetention(n),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(e.nextToolExecutionSequence,n);const h=this.db.prepare("INSERT INTO tool_execution_context_coverage (thread_id, execution_id, representation, transcript_sequence, created_at) VALUES (?, ?, ?, ?, ?)");for(const t of e.toolExecutionCoverage)h.run(n,t.executionId,t.representation,t.transcriptSequence,t.createdAt);const p=this.db.prepare("INSERT INTO compaction_checkpoints (id, thread_id, generation, schema_version, message_from_sequence, message_through_sequence, tool_from_sequence, tool_through_sequence, checkpoint_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");for(const e of o)p.run(e.id,n,e.generation,e.version,e.transcript.messages.fromSequenceExclusive,e.transcript.messages.throughSequence,e.transcript.toolExecutions.fromSequenceExclusive,e.transcript.toolExecutions.throughSequence,JSON.stringify(e),e.createdAt);const T=new r(this.db);if(e.semanticLedger)T.restore(n,e.semanticLedger);else for(const e of o)2===e.version&&T.append(n,e.generation,2===e.version?e.semanticState.scopeId??e.semanticState.goal?.id??"initial":"legacy",e.transcript.toolExecutions.throughSequence,F(e.semanticState),e.id!==o.at(-1)?.id);if(e.contextRecovery){const t=s.get(e.contextRecovery.checkpointId);t&&this.db.prepare("INSERT INTO compaction_recovery\n (thread_id,checkpoint_id,required,unextracted,memo) VALUES (?,?,1,?,?)").run(n,t,Number(e.contextRecovery.unextracted),e.contextRecovery.memo)}for(const t of e.contextRecoveryNotes??[]){const e=i.parse(t),r=s.get(e.checkpointId);r&&this.db.prepare("INSERT INTO compaction_recovery_notes (thread_id,checkpoint_id,notes) VALUES (?,?,?)").run(n,r,e.notes)}return this.db.exec("COMMIT"),n}catch(e){throw this.db.exec("ROLLBACK"),e}}countMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM messages WHERE thread_id = ?").get(e).n}countTranscriptMessages(e){return this.db.prepare("SELECT COUNT(*) AS n FROM transcript_messages WHERE thread_id = ?").get(e).n}updateTitle(e,t){return this.withThreadWrite(e,()=>this.updateTitleOwned(e,t))}updateTitleOwned(e,t){this.db.prepare("UPDATE threads SET title = ? WHERE id = ?").run(t,e)}updateModel(e,t){return this.withThreadWrite(e,()=>this.updateModelOwned(e,t))}updateModelOwned(e,t){this.db.prepare("UPDATE threads SET model = ? WHERE id = ?").run(t,e)}deleteThread(e,t={}){return this.withThreadWrite(e,()=>this.deleteThreadOwned(e,t))}deleteThreadOwned(e,t={}){t.onlyIfEmpty&&(this.countMessages(e)>0||this.countTranscriptMessages(e)>0)||this.db.prepare("DELETE FROM threads WHERE id = ?").run(e)}appendRuntimeEvent(e){return this.withThreadWrite(e.threadId,()=>this.appendRuntimeEventOwned(e))}appendRuntimeEventOwned(e){const t=O.parse(e.event),n="tool.completed"===t.type?(({appOutput:e,...t})=>t)(t):t,r=JSON.stringify(n),s=he(e.timestamp),i=Buffer.byteLength(JSON.stringify(t),"utf8")+Buffer.byteLength(s,"utf8")+Buffer.byteLength(e.turnId??"","utf8");if(i>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread)throw new Error("Runtime event exceeds the per-Thread retained byte limit");this.db.exec("BEGIN IMMEDIATE");try{const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e.threadId);if(void 0===n)throw new Error(`Thread "${e.threadId}" 不存在或缺少 Runtime event state`);const o=de(),a=n.next_sequence;return this.db.prepare("INSERT INTO runtime_events\n (thread_id, thread_sequence, event_id, turn_id, event_timestamp,\n event_json, size_bytes, created_at, app_output_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(e.threadId,a,o,e.turnId??null,s,r,i,s,"tool.completed"===t.type?JSON.stringify(t.appOutput):null),this.db.prepare("UPDATE thread_runtime_event_state\n SET next_sequence = ?, latest_event_id = ?,\n retained_count = retained_count + 1,\n retained_bytes = retained_bytes + ?\n WHERE thread_id = ?").run(a+1,o,i,e.threadId),this.enforceRuntimeEventRetentionInTransaction(e.threadId,s),this.db.exec("COMMIT"),{eventId:o,cursor:Ee(n.event_log_id,a,o),threadSequence:a,threadId:e.threadId,...void 0===e.turnId?{}:{turnId:e.turnId},timestamp:s,event:t}}catch(e){throw this.db.exec("ROLLBACK"),e}}getRuntimeEventCursor(e){const t=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===t)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);return 0===t.next_sequence||null===t.latest_event_id?null:Ee(t.event_log_id,t.next_sequence-1,v.parse(t.latest_event_id))}resumeRuntimeEvents(e,t){const n=null===t?void 0:_e(t);if(null!==t&&void 0===n)throw new RuntimeEventCursorGapError(t);const r=!this.readOnly&&void 0===this.claims;this.db.exec(r?"BEGIN IMMEDIATE":"BEGIN");try{r&&this.enforceRuntimeEventRetentionInTransaction(e,this.now().toISOString());const s=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===s)throw new Error(`Thread "${e}" 不存在或缺少 Runtime event state`);if(null===t&&s.retained_from_sequence>0)throw new RuntimeEventCursorExpiredError(t);if(null!==t&&void 0!==n&&this.validateRuntimeEventCursor(s,t,n,e),0===s.next_sequence||null===s.latest_event_id)return this.db.exec("COMMIT"),{events:[],throughCursor:null,replayedCount:0};const i=s.next_sequence-1,o=n?.threadSequence??-1,a=this.db.prepare("SELECT *\n FROM runtime_events\n WHERE thread_id = ?\n AND thread_sequence > ?\n AND thread_sequence <= ?\n ORDER BY thread_sequence ASC").all(e,o,i);if(!r){const n=new Date(this.now().getTime()-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),r=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from FROM runtime_events WHERE thread_id = ? AND created_at >= ?").get(e,n);if(o<Math.max(s.retained_from_sequence,r.keep_from??s.next_sequence)-1)throw new RuntimeEventCursorExpiredError(t)}const c=a.map(t=>ue(s.event_log_id,e,t)),d=Ee(s.event_log_id,i,v.parse(s.latest_event_id));return this.db.exec("COMMIT"),{events:c,throughCursor:d,replayedCount:c.length}}catch(e){throw e instanceof RuntimeEventCursorExpiredError||e instanceof RuntimeEventCursorGapError?this.db.exec("COMMIT"):this.db.exec("ROLLBACK"),e}}countRuntimeEvents(e){return this.db.prepare("SELECT COUNT(*) AS count FROM runtime_events WHERE thread_id = ?").get(e).count}validateRuntimeEventCursor(e,t,n,r){if(n.eventLogId!==e.event_log_id||n.threadSequence>=e.next_sequence)throw new RuntimeEventCursorGapError(t);const s=e.retained_from_sequence-1;if(n.threadSequence<s)throw new RuntimeEventCursorExpiredError(t);if(n.threadSequence===s){if(n.eventId!==e.retained_predecessor_event_id)throw new RuntimeEventCursorGapError(t);return}const i=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(r,n.threadSequence);if(void 0===i||i.event_id!==n.eventId)throw new RuntimeEventCursorGapError(t)}enforceRuntimeEventRetentionInTransaction(e,t){const n=this.db.prepare("SELECT event_log_id, next_sequence, retained_from_sequence,\n retained_predecessor_event_id, latest_event_id,\n retained_count, retained_bytes\n FROM thread_runtime_event_state\n WHERE thread_id = ?").get(e);if(void 0===n||0===n.retained_count)return;let r=n.retained_from_sequence;if(n.retained_count>RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread){const t=this.db.prepare("SELECT thread_sequence AS keep_from\n FROM runtime_events\n WHERE thread_id = ?\n ORDER BY thread_sequence DESC\n LIMIT 1 OFFSET ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxRecordsPerThread-1);r=Math.max(r,t?.keep_from??n.next_sequence)}if(n.retained_bytes>RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread){const t=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM (\n SELECT thread_sequence,\n SUM(size_bytes) OVER (\n ORDER BY thread_sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM runtime_events\n WHERE thread_id = ?\n )\n WHERE retained_bytes <= ?").get(e,RUNTIME_EVENT_RETENTION_POLICY.maxBytesPerThread);r=Math.max(r,t.keep_from??n.next_sequence)}const s=new Date(Date.parse(t)-RUNTIME_EVENT_RETENTION_POLICY.maxAgeMs).toISOString(),i=this.db.prepare("SELECT MIN(thread_sequence) AS keep_from\n FROM runtime_events\n WHERE thread_id = ? AND created_at >= ?").get(e,s);if(r=Math.max(r,i.keep_from??n.next_sequence),r<=n.retained_from_sequence)return;const o=this.db.prepare("SELECT thread_sequence, event_id\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence = ?").get(e,r-1);if(void 0===o)throw new Error("Runtime event retention boundary is not a continuous retained prefix");const a=this.db.prepare("SELECT COUNT(*) AS removed_count, SUM(size_bytes) AS removed_bytes\n FROM runtime_events\n WHERE thread_id = ? AND thread_sequence < ?").get(e,r);this.db.prepare("DELETE FROM runtime_events WHERE thread_id = ? AND thread_sequence < ?").run(e,r),this.db.prepare("UPDATE thread_runtime_event_state\n SET retained_from_sequence = ?, retained_predecessor_event_id = ?,\n retained_count = retained_count - ?,\n retained_bytes = retained_bytes - ?\n WHERE thread_id = ?").run(r,o.event_id,a.removed_count,a.removed_bytes??0,e)}acceptTurnInput(e,t,n){return this.withThreadWrite(e,()=>this.acceptTurnInputOwned(e,t,n))}acceptTurnInputOwned(e,t,n){if(this.readOnly)throw new Error("ThreadStore is read-only");const r=$.parse(t),s=B(T.parse(n));if("user"!==s.role)throw new Error("Accepted turn input must be a user message");this.db.exec("BEGIN IMMEDIATE");try{if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const t=this.getTurnInput(e,r);if(void 0!==t){const n=this.db.prepare("SELECT message_json FROM transcript_messages WHERE thread_id = ? AND sequence = ?").get(e,t.transcriptSequence);if(n?.message_json!==JSON.stringify(s))throw new Error("Turn input identity conflict");return this.db.exec("COMMIT"),t}const n=this.now().toISOString();this.appendMessagesInTransaction(e,[s],n,void 0);const i=this.db.prepare("SELECT MAX(sequence) AS sequence FROM transcript_messages WHERE thread_id = ?").get(e),o=J.parse({inputId:r,transcriptSequence:i?.sequence,createdAt:n});return this.db.prepare("INSERT INTO thread_turn_inputs (thread_id, input_id, transcript_sequence, created_at) VALUES (?, ?, ?, ?)").run(e,r,o.transcriptSequence,n),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}getTurnInput(e,t){const n=this.db.prepare("SELECT input_id, transcript_sequence, skill_checkpoint_json, created_at FROM thread_turn_inputs WHERE thread_id = ? AND input_id = ?").get(e,$.parse(t));return void 0===n?void 0:J.parse({inputId:n.input_id,transcriptSequence:n.transcript_sequence,createdAt:n.created_at,..."string"==typeof n.skill_checkpoint_json?{skillCheckpoint:JSON.parse(n.skill_checkpoint_json)}:{}})}listTurnInputs(e){return this.db.prepare("SELECT input_id FROM thread_turn_inputs WHERE thread_id = ? ORDER BY transcript_sequence").all(e).map(t=>{const n=this.getTurnInput(e,String(t.input_id));if(void 0===n)throw new Error("Turn input receipt disappeared");return n})}getTurnInputSkillCheckpoint(e,t){const n=this.db.prepare("SELECT skill_checkpoint_json FROM thread_turn_inputs WHERE thread_id = ? AND transcript_sequence = ?").get(e,t);return"string"==typeof n?.skill_checkpoint_json?V.parse(JSON.parse(n.skill_checkpoint_json)):void 0}recordTurnSkillCheckpoint(e,t,n){return this.withThreadWrite(e,()=>this.recordTurnSkillCheckpointOwned(e,t,n))}recordTurnSkillCheckpointOwned(e,t,n){if(this.readOnly)throw new Error("ThreadStore is read-only");const r=V.parse(n),s=JSON.stringify(r);this.db.exec("BEGIN IMMEDIATE");try{const n=this.getTurnInput(e,t);if(void 0===n)throw new Error("Turn input receipt not found");if(void 0!==n.skillCheckpoint&&JSON.stringify(n.skillCheckpoint)!==s)throw new Error("Turn input Skill snapshot conflict");this.db.prepare("UPDATE thread_turn_inputs SET skill_checkpoint_json = ? WHERE thread_id = ? AND input_id = ?").run(s,e,t),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}appendMessages(e,t,n={}){return this.withThreadWrite(e,()=>this.appendMessagesOwned(e,t,n))}appendMessagesOwned(e,t,n={}){const r=n.toolExecutionCoverage;if(0===t.length){if(void 0!==r&&r.executionIds.length>0)throw new Error("Tool execution coverage 必须与至少一条 transcript message 原子写入");return}if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);if(void 0!==r&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.rawTranscript&&r.representation!==TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence)throw new Error("Tool execution coverage representation 无效");const s=r?.executionIds??[],i=new Set;for(const e of s){if("string"!=typeof e||0===e.length)throw new Error("Tool execution coverage execution id 无效");if(i.has(e))throw new Error(`Tool execution coverage execution id 重复: ${e}`);i.add(e)}const o=t.map(e=>B(T.parse(e))),a=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.appendMessagesInTransaction(e,o,a,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}recoverUncoveredToolExecutions(e,t){return this.withThreadWrite(e,()=>this.recoverUncoveredToolExecutionsOwned(e,t))}recoverUncoveredToolExecutionsOwned(e,t){this.db.exec("BEGIN IMMEDIATE");try{if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=this.uncoveredToolExecutionRecords(e,{afterSequence:-1,throughSequence:Number.MAX_SAFE_INTEGER,toolCallId:void 0,limit:Number.MAX_SAFE_INTEGER});if(0===n.length)return void this.db.exec("COMMIT");const r=B(T.parse(t(n))),s={executionIds:n.map(e=>e.id),representation:TOOL_EXECUTION_CONTEXT_REPRESENTATIONS.recoveryEvidence};return this.appendMessagesInTransaction(e,[r],(new Date).toISOString(),s),this.db.exec("COMMIT"),r}catch(e){throw this.db.exec("ROLLBACK"),e}}appendMessagesInTransaction(e,t,n,r){const s=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)"),i=this.db.prepare("INSERT INTO transcript_messages\n (thread_id, sequence, role, message_json, provenance, created_at)\n VALUES (?, ?, ?, ?, ?, ?)"),o=this.db.prepare("INSERT INTO tool_execution_context_coverage\n (thread_id, execution_id, representation, transcript_sequence, created_at)\n SELECT thread_id, id, ?, ?, ?\n FROM tool_executions\n WHERE thread_id = ?\n AND id = ?\n AND NOT EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE thread_id = ? AND execution_id = ?\n )"),a=this.db.prepare("SELECT COALESCE(MAX(idx), -1) AS maxIdx FROM messages WHERE thread_id = ?").get(e),c=this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e);let d=a.maxIdx+1,E=c.maxSequence+1;for(const r of t){const t=JSON.stringify(r);s.run(e,d,r.role,t,n),i.run(e,E,r.role,t,q[0],n),d+=1,E+=1}const _=E-1;if(void 0!==r){for(const t of r.executionIds){const s=o.run(r.representation,_,n,e,t,e,t);if(1!==Number(s.changes))throw new Error(`Tool execution "${t}" 不属于 Thread "${e}" 或已被 transcript 覆盖`)}this.enforceToolExecutionRetentionInTransaction(e,n)}this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}replaceMessages(e,t){return this.withThreadWrite(e,()=>this.replaceMessagesOwned(e,t))}replaceMessagesOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.map(e=>T.parse(e)),r=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{this.replaceMessagesInTransaction(e,n,r),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}getMessages(e){return this.db.prepare("SELECT content_json FROM messages WHERE thread_id = ? ORDER BY idx ASC").all(e).map(e=>T.parse(JSON.parse(e.content_json)))}replaceMessagesInTransaction(e,t,n){const r=this.db.prepare("INSERT INTO messages (thread_id, idx, role, content_json, created_at) VALUES (?, ?, ?, ?, ?)");this.db.prepare("DELETE FROM messages WHERE thread_id = ?").run(e),t.forEach((t,s)=>{const i=B(t);r.run(e,s,i.role,JSON.stringify(i),n)}),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(n,e)}appendToolExecution(e,t){return this.withThreadWrite(e,()=>this.appendToolExecutionOwned(e,t))}appendToolExecutionOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=f(t),r=JSON.stringify(n),s=this.now().toISOString();this.db.exec("BEGIN IMMEDIATE");try{const i=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);if(void 0===i)throw new Error(`Thread "${e}" 缺少 Tool execution sequence state`);const o=i.next_sequence;return this.db.prepare("INSERT INTO tool_executions\n (thread_id, sequence, id, tool_call_id, agent_name, tool_name, record_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,o,n.id,n.toolCallId,n.agentName,n.toolName,r,n.createdAt),this.db.prepare("UPDATE thread_tool_execution_state SET next_sequence = ? WHERE thread_id = ?").run(o+1,e),void 0!==t.appOutput&&this.insertAppOutput(e,t.id,t.appOutput,new Date(Date.parse(s)+l.maxAgeMs).toISOString()),this.enforceAppOutputRetention(e),this.enforceToolExecutionRetentionInTransaction(e,s),this.db.prepare("UPDATE threads SET updated_at = ? WHERE id = ?").run(s,e),this.db.exec("COMMIT"),o}catch(e){throw this.db.exec("ROLLBACK"),e}}readAppOutputRow(e,t){const n=m.parse(JSON.parse(e));return"available"===n.status&&Date.parse(t)<=this.now().getTime()?{status:"expired"}:n}insertAppOutput(e,t,n,r){const s="available"===n.status&&Date.parse(r)<=this.now().getTime()?{status:"expired"}:A(n),i=JSON.stringify(s);this.db.prepare("INSERT INTO operation_app_outputs\n (thread_id, operation_id, result_json, descriptor_json, byte_length, expires_at) VALUES (?, ?, ?, ?, ?, ?)").run(e,t,i,JSON.stringify(R(s)),"available"===s.status?Buffer.byteLength(i,"utf8"):0,r)}enforceAppOutputRetention(e){const t=JSON.stringify({status:"expired"});this.db.prepare("UPDATE operation_app_outputs SET result_json = ?, descriptor_json = ?, byte_length = 0\n WHERE thread_id = ? AND byte_length > 0 AND expires_at <= ?").run(t,t,e,this.now().toISOString()),this.db.prepare("UPDATE operation_app_outputs SET result_json = ?, descriptor_json = ?, byte_length = 0\n WHERE thread_id = ? AND operation_id IN (\n SELECT operation_id FROM (\n SELECT o.operation_id,\n ROW_NUMBER() OVER (ORDER BY e.sequence DESC) AS rank,\n SUM(o.byte_length) OVER (ORDER BY e.sequence DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS bytes\n FROM operation_app_outputs o JOIN tool_executions e ON e.thread_id = o.thread_id AND e.id = o.operation_id\n WHERE o.thread_id = ? AND o.byte_length > 0\n ) WHERE rank > ? OR bytes > ?\n )").run(t,t,e,e,l.threadRecords,l.threadBytes)}getAppOutputDescriptor(e,t){if(void 0===this.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'operation_app_outputs'").get())return void 0===this.db.prepare("SELECT 1 FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t)?void 0:{status:"not_provided"};const n=this.db.prepare("SELECT o.descriptor_json, o.expires_at FROM tool_executions e\n LEFT JOIN operation_app_outputs o ON o.thread_id = e.thread_id AND o.operation_id = e.id\n WHERE e.thread_id = ? AND e.id = ?").get(e,t);if(void 0===n)return;if(null===n.descriptor_json)return{status:"not_provided"};const r=N.parse(JSON.parse(n.descriptor_json));return"available"===r.status&&null!==n.expires_at&&Date.parse(n.expires_at)<=this.now().getTime()?{status:"expired"}:r}getAppOutput(e,t){const n=this.getAppOutputDescriptor(e,t);if(void 0===n||"available"!==n.status)return n;const r=this.db.prepare("SELECT result_json FROM operation_app_outputs WHERE thread_id = ? AND operation_id = ?").get(e,t);return void 0===r?{status:"not_provided"}:m.parse(JSON.parse(r.result_json))}enforceToolExecutionRetentionInTransaction(e,t){const n=new Date(Date.parse(t)-TOOL_EXECUTION_RETENTION_POLICY.maxAgeMs).toISOString();let r=!1;const s=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND created_at < ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )").run(e,n);r=Number(s.changes)>0;const i=this.db.prepare("SELECT sequence\n FROM (\n SELECT sequence,\n ROW_NUMBER() OVER (ORDER BY sequence DESC) AS retained_rank,\n SUM(length(CAST(record_json AS BLOB))) OVER (\n ORDER BY sequence DESC\n ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n ) AS retained_bytes\n FROM tool_executions\n WHERE thread_id = ?\n )\n WHERE retained_rank > ? OR retained_bytes > ?").all(e,TOOL_EXECUTION_RETENTION_POLICY.maxRecordsPerThread,TOOL_EXECUTION_RETENTION_POLICY.maxBytesPerThread);if(i.length>0){const t=this.db.prepare("DELETE FROM tool_executions\n WHERE thread_id = ?\n AND sequence = ?\n AND EXISTS (\n SELECT 1\n FROM tool_execution_context_coverage\n WHERE tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n )");for(const n of i){const s=t.run(e,n.sequence);r=Number(s.changes)>0||r}}r&&this.db.prepare("INSERT INTO thread_transcript_state (thread_id, completeness)\n VALUES (?, 'legacy_snapshot')\n ON CONFLICT(thread_id) DO UPDATE SET completeness = 'legacy_snapshot'").run(e)}listToolExecutions(e,t={}){const{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);if(r<=n)return[];return this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n AND (? IS NULL OR tool_call_id = ?)\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,t.toolCallId??null,t.toolCallId??null,s).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence}))}listUncoveredToolExecutions(e,t={}){const{afterSequence:n,throughSequence:r,limit:s}=this.resolveToolExecutionListRange(t);return r<=n?[]:this.uncoveredToolExecutionRecords(e,{afterSequence:n,throughSequence:r,toolCallId:t.toolCallId,limit:s})}resolveToolExecutionListRange(e){const t=e.afterSequence??-1,n=e.throughSequence??Number.MAX_SAFE_INTEGER,r=e.limit??Q;if(!Number.isInteger(t)||t<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(r)||r<1||r>Z)throw new Error(`limit 必须是 1-${String(Z)} 之间的整数`);if(!Number.isInteger(n)||n<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");return{afterSequence:t,throughSequence:n,limit:r}}uncoveredToolExecutionRecords(e,t){return this.db.prepare("SELECT tool_executions.sequence, tool_executions.record_json\n FROM tool_executions\n LEFT JOIN tool_execution_context_coverage\n ON tool_execution_context_coverage.thread_id = tool_executions.thread_id\n AND tool_execution_context_coverage.execution_id = tool_executions.id\n WHERE tool_executions.thread_id = ?\n AND tool_executions.sequence > ?\n AND tool_executions.sequence <= ?\n AND (? IS NULL OR tool_executions.tool_call_id = ?)\n AND tool_execution_context_coverage.execution_id IS NULL\n ORDER BY tool_executions.sequence ASC\n LIMIT ?").all(e,t.afterSequence,t.throughSequence,t.toolCallId??null,t.toolCallId??null,t.limit).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence}))}listRecentToolExecutions(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??Q;this.validateRecentPage(n,r,Z);const s=this.db.prepare("SELECT sequence, record_json\n FROM tool_executions\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),i=s.length>r,o=s.slice(0,r).map(e=>({...x(JSON.parse(e.record_json)),sequence:e.sequence})).reverse();return{entries:o,nextBeforeSequence:i?o[0]?.sequence:void 0}}getToolExecution(e,t){const n=this.db.prepare("SELECT sequence, record_json FROM tool_executions WHERE thread_id = ? AND id = ?").get(e,t);if(n)return{...x(JSON.parse(n.record_json)),sequence:n.sequence}}getTranscriptCompleteness(e){const t=this.db.prepare("SELECT completeness FROM thread_transcript_state WHERE thread_id = ?").get(e);return t?.completeness===w[0]?w[0]:w[1]}listTranscriptMessages(e,t={}){const n=t.afterSequence??-1,r=t.throughSequence??Number.MAX_SAFE_INTEGER,s=t.limit??ee;if(this.validateTranscriptRange(n,r,s,te),r<=n)return[];return this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence > ?\n AND sequence <= ?\n ORDER BY sequence ASC\n LIMIT ?").all(e,n,r,s).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)}))}listRecentTranscriptMessages(e,t={}){const n=t.beforeSequence??Number.MAX_SAFE_INTEGER,r=t.limit??ee;this.validateRecentPage(n,r,te);const s=this.db.prepare("SELECT sequence, provenance, created_at, message_json\n FROM transcript_messages\n WHERE thread_id = ?\n AND sequence < ?\n ORDER BY sequence DESC\n LIMIT ?").all(e,n,r+1),i=s.length>r,o=s.slice(0,r).map(e=>y.parse({sequence:e.sequence,provenance:e.provenance,createdAt:e.created_at,message:JSON.parse(e.message_json)})).reverse();return{entries:o,nextBeforeSequence:i?o[0]?.sequence:void 0}}getLatestCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{return this.withCurrentTranscriptCompleteness(e,oe(n))}catch(e){if(e instanceof M)throw e}}getLatestSummaryCheckpoint(e){const t=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ?\n ORDER BY generation DESC").all(e);for(const n of t)try{const t=oe(n);if(t.version===b||"valid"===t.summary.status)return this.withCurrentTranscriptCompleteness(e,t)}catch(e){if(e instanceof M)throw e}}getCheckpoint(e,t){const n=this.db.prepare("SELECT schema_version, checkpoint_json\n FROM compaction_checkpoints\n WHERE thread_id = ? AND id = ?").get(e,t);if(void 0!==n)try{return this.withCurrentTranscriptCompleteness(e,oe(n))}catch(e){if(e instanceof M)throw e;return}}loadSessionState(e){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);return{messages:P(this.getMessages(e)).messages,checkpoint:this.getLatestCheckpoint(e),transcriptCompleteness:this.getTranscriptCompleteness(e)}}commitCompaction(e,t){return this.withThreadWrite(e,()=>this.commitCompactionOwned(e,t))}commitCompactionOwned(e,t){if(!this.hasThread(e))throw new Error(`Thread "${e}" 不存在`);const n=t.messages.map(e=>T.parse(e)),r=t.expectedActiveMessages.map(e=>T.parse(e)),s=P(n);if(s.status!==H.valid){const e=[...new Set([...s.removedToolCallIds,...s.removedToolResultIds])].sort();throw new Error(`Compaction active projection contains malformed Tool protocol: ${e.join(", ")}`)}const i=(new Date).toISOString();this.db.exec("BEGIN IMMEDIATE");try{const s=P(this.getMessages(e)).messages;if(JSON.stringify(s)!==JSON.stringify(r))throw new Error("Compaction active projection changed after the draft snapshot was captured");const o=this.getLatestCheckpoint(e);if(o?.id!==t.expectedLatestCheckpointId)throw new Error("Compaction checkpoint ancestry changed after the draft snapshot was captured");const a=this.legacySummaryFragmentsToArchive(e,s,n),c=this.maxTranscriptMessageSequence(e),d=this.db.prepare("SELECT COALESCE(MAX(generation), 0) AS maxGeneration FROM compaction_checkpoints WHERE thread_id = ?").get(e),E={messages:{fromSequenceExclusive:o?.transcript.messages.throughSequence??-1,throughSequence:c+a.length},toolExecutions:{fromSequenceExclusive:o?.transcript.toolExecutions.throughSequence??-1,throughSequence:this.maxToolExecutionSequence(e)},completeness:this.getTranscriptCompleteness(e)},_=new Set(r.flatMap(e=>Array.isArray(e.content)?e.content.flatMap(e=>"tool-call"===e.type?[e.toolCallId]:[]):[])),h=this.listRecentToolExecutions(e,{limit:Z}).entries.filter(e=>_.has(e.toolCallId)),u=D({summary:t.summary,generation:d.maxGeneration+1,transcript:E,...void 0===o?{}:{previousCheckpointId:o.id},toolState:U(r,h,32,E.completeness),createdAt:i});t.validateCheckpoint?.(u);for(const[t,n]of a.entries())this.db.prepare("INSERT INTO transcript_messages\n (thread_id,sequence,role,message_json,provenance,created_at) VALUES (?,?,?,?,?,?)").run(e,c+t+1,n.role,JSON.stringify(n),q[1],i);return this.db.prepare("INSERT INTO compaction_checkpoints\n (id, thread_id, generation, schema_version, message_from_sequence, message_through_sequence,\n tool_from_sequence, tool_through_sequence, checkpoint_json, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(u.id,e,u.generation,u.version,E.messages.fromSequenceExclusive,E.messages.throughSequence,E.toolExecutions.fromSequenceExclusive,E.toolExecutions.throughSequence,JSON.stringify(u),u.createdAt),this.replaceMessagesInTransaction(e,n,i),this.db.exec("COMMIT"),u}catch(e){throw this.db.exec("ROLLBACK"),e}}legacySummaryFragmentsToArchive(e,t,n){const r=e=>"string"==typeof e.content?X(e.content):void 0,s=new Set(n.map(r)),i=new Map,o=this.db.prepare("SELECT 1 FROM transcript_messages WHERE thread_id=? AND json_extract(message_json, '$.content') IN (?, ?) LIMIT 1");for(const n of t){const t=r(n);if(void 0===t||0===t.length||s.has(t))continue;const a=C(t),c=W(a);o.get(e,String(n.content),String(c.content))||i.set(a,c)}return[...i.values()]}getContextRecovery(e){if(this.getLatestCheckpoint(e)?.version===b)return;const t=new r(this.db),n=t.recovery(e);if(!n)return;const s=this.getCheckpoint(e,n.checkpointId);if(!s)throw new Error("Recovery checkpoint is missing");const i=2===s.version?s.semanticState.goal?.id:void 0;return{...n,requiredItems:t.rows(e,s.generation).flatMap(e=>"active"===e.status&&("constraint"===e.category||"goal"===e.category&&e.item_id===i)?[{itemId:e.item_id,version:e.generation,category:e.category}]:[])}}getSemanticLedgerRows(e){return new r(this.db).rows(e,this.getLatestCheckpoint(e)?.generation??0)}readCheckpointTranscript(e,t){if(!TRANSCRIPT_ENTRY_KINDS.includes(t.kind))throw new Error("kind 必须是 message、tool_execution 或 legacy_context");const n=this.getCheckpoint(e,t.checkpointId);if(void 0===n)throw new Error("checkpoint 不存在、已损坏或不属于当前 thread");const r=t.limit??ee,s="legacy_context"===t.kind?{fromSequenceExclusive:-1,throughSequence:Number.MAX_SAFE_INTEGER}:"message"===t.kind?n.transcript.messages:n.transcript.toolExecutions,i=Math.max(t.afterSequence??s.fromSequenceExclusive,s.fromSequenceExclusive);this.validateTranscriptRange(i,s.throughSequence,r,ne);const o="legacy_context"===t.kind?this.listLegacyContextEntries(e,n,i,r+1):"message"===t.kind?this.listTranscriptMessages(e,{afterSequence:i,throughSequence:s.throughSequence,limit:r+1}).map(e=>({kind:"message",...e})):this.listToolExecutions(e,{afterSequence:i,throughSequence:s.throughSequence,limit:Math.min(r+1,Z)}).map(e=>({kind:"tool_execution",...e})),a=o.length>r,c=a?o.slice(0,r):o,d=c.at(-1)?.sequence;return{checkpointId:n.id,kind:t.kind,entries:c,...a&&void 0!==d?{nextAfterSequence:d}:{},...void 0!==n.previousCheckpointId?{previousCheckpointId:n.previousCheckpointId}:{},completeness:n.transcript.completeness}}listLegacyContextEntries(e,t,n,s){if(t.version===b)return[];const i=new r(this.db),o=[];if(n<0){const n=i.recovery(e),r=this.db.prepare("SELECT notes FROM compaction_recovery_notes WHERE thread_id=? AND checkpoint_id=?").get(e,t.id);o.push({kind:"legacy_context",sequence:0,value:{checkpoint:t,...n?.checkpointId===t.id?{memo:n.memo}:{},..."string"==typeof r?.notes?{notes:r.notes}:{}}})}for(const r of i.page(e,t.generation,n,s-o.length))o.push({kind:"legacy_context",sequence:r.sequence,value:{ledgerItem:j.parse({category:r.category,item:JSON.parse(r.item_json)}),generation:r.generation,status:r.status,scope:r.scope,resolution:r.resolution_json??null}});return o}maxTranscriptMessageSequence(e){return this.db.prepare("SELECT COALESCE(MAX(sequence), -1) AS maxSequence FROM transcript_messages WHERE thread_id = ?").get(e).maxSequence}maxToolExecutionSequence(e){const t=this.db.prepare("SELECT next_sequence FROM thread_tool_execution_state WHERE thread_id = ?").get(e);return(t?.next_sequence??0)-1}withCurrentTranscriptCompleteness(e,t){const n=w[1],r=t.transcript.completeness===n||this.getTranscriptCompleteness(e)===n?n:w[0],s=r===n&&"valid"===t.toolState.integrityStatus?{...t.toolState,integrityStatus:"sanitized"}:t.toolState;return t.transcript.completeness===r&&t.toolState===s?t:{...t,toolState:s,transcript:{...t.transcript,completeness:r}}}validateTranscriptRange(e,t,n,r){if(!Number.isInteger(e)||e<-1)throw new Error("afterSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(t)||t<-1)throw new Error("throughSequence 必须是大于等于 -1 的整数");if(!Number.isInteger(n)||n<1||n>r)throw new Error(`limit 必须是 1-${String(r)} 之间的整数`)}validateRecentPage(e,t,n){if(!Number.isSafeInteger(e)||e<0||e>Number.MAX_SAFE_INTEGER)throw new Error("beforeSequence 必须是非负安全整数");if(!Number.isInteger(t)||t<1||t>n)throw new Error(`limit 必须是 1-${String(n)} 之间的整数`)}retainSession(e){if(void 0===this.claims)return;if(this.sessionReservations.has(e))throw new t(e);const n=this.claims.acquire(e);try{if(this.hasThread(e)){this.db.exec("BEGIN IMMEDIATE");try{const t=this.now().toISOString();this.enforceAppOutputRetention(e),this.enforceToolExecutionRetentionInTransaction(e,t),this.enforceRuntimeEventRetentionInTransaction(e,t),this.db.exec("COMMIT")}catch(e){throw this.db.exec("ROLLBACK"),e}}}catch(e){throw n.release(),e}this.sessionReservations.set(e,n.token);let r,s=!1;return{...n,release:()=>{if(void 0!==r)throw r.cause;if(!s){try{n.release()}catch(e){throw r={cause:e},e}s=!0,this.sessionReservations.get(e)===n.token&&this.sessionReservations.delete(e)}}}}retainThread(e){if(this.readOnly)throw new Error("ThreadStore is read-only");return this.claims?.acquire(e)}withThreadWrite(e,t){if(this.readOnly)throw new Error("ThreadStore is read-only");const n=this.retainThread(e);try{return n?.assert(),t()}finally{n?.release()}}close(){this.claims?.close(),this.db.close()}}export const THREAD_LIBRARY_STATUSES=["new","current","needs_migration","unsupported"];export class ThreadLibraryMigrationRequiredError extends Error{preflight;constructor(e){super(`会话库需要显式备份升级或受支持的新版本:${e.status} (v${String(e.schemaVersion)})。请停止所有客户端后确认迁移;未修改历史。`),this.name="ThreadLibraryMigrationRequiredError",this.preflight=e}}export function inspectThreadLibrary(e=defaultThreadsDir()){const t=h(expandTilde(e),"threads.db");if(!a(t))return{databasePath:t,schemaVersion:null,supportedSchemaVersion:z,status:"new"};const n=d(t),r=E(n,{bigint:!0}),s=new p(n,{readOnly:!0});try{s.exec(`PRAGMA busy_timeout = ${String(re)}; PRAGMA query_only = ON;`);const e=s.prepare("PRAGMA user_version").get(),t=e?.user_version;if("number"!=typeof t)throw new Error("Invalid ThreadStore schema version");return{databasePath:n,physicalIdentity:`inode:${r.dev}:${r.ino}:${r.birthtimeNs}`,writableAliasSafe:1n===r.nlink,schemaVersion:t,supportedSchemaVersion:z,status:t===z?"current":t>z?"unsupported":"needs_migration"}}finally{s.close()}}export function migrateThreadLibrary(t){if(!0!==t.confirmStoppedLegacyClients)throw new Error("Migration requires explicit confirmation that legacy clients are stopped");const r=inspectThreadLibrary(t.dir);if("needs_migration"!==r.status)throw new ThreadLibraryMigrationRequiredError(r);const s=new e(r.databasePath,t.ownership);let i;try{i=s.maintenance(),i.assert();const e=inspectThreadLibrary(t.dir);if("needs_migration"!==e.status)throw new ThreadLibraryMigrationRequiredError(e);const r=n(e.databasePath),a=`${r}.backup-${u()}.sqlite`,c=new p(r,{readOnly:!0});try{c.prepare("VACUUM INTO ?").run(a)}finally{c.close()}"win32"!==process.platform&&o(a,384);const d=new p(a,{readOnly:!0});try{const t=d.prepare("PRAGMA quick_check").all();if(1!==t.length||"ok"!==t[0]?.quick_check||d.prepare("PRAGMA user_version").get()?.user_version!==e.schemaVersion)throw new Error("SQLite backup verification failed; original history was not migrated")}finally{d.close()}i.assert();return new ThreadStore(t.dir).close(),{backupPath:a,preflight:inspectThreadLibrary(t.dir)}}finally{i?.release(),s.close()}}
|