@roll-agent/runtime 1.1.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/index.js CHANGED
@@ -1 +1 @@
1
- export{ConversationEngine,buildSessionBashSettings,buildSessionExecSettings}from"./engine/conversation-engine.js";export{runtimeSettingsSchema}from"./host-contracts/runtime-settings.js";export{buildScheduleToolset,SCHEDULE_CREATE_TOOL_ID}from"./tool-bridge/schedule-tool.js";export{scheduleCreateInputSchema}from"./tool-bridge/schedule-create-input.js";export{AgentSession}from"./engine/agent-session.js";export{WORKSPACE_INSTRUCTION_FILE_NAMES,WORKSPACE_INSTRUCTIONS_MAX_CHARS,WORKSPACE_INSTRUCTIONS_MODES,createWorkspaceInstructionsSource,findWorkspaceInstructionsPath,parseWorkspaceInstructionsSetting}from"./engine/workspace-instructions.js";export{resolveContextWindow,lookupContextWindow,resolveModelContextWindow,CONTEXT_WINDOW_SOURCES}from"./engine/context-window.js";export{createDefaultModelCatalog}from"./engine/model-catalog-default.js";export{ModelCatalog,defaultModelCatalogCachePath,lookupCatalogContextWindow,trimModelCatalog,MODEL_CATALOG_REFRESH_RESULTS}from"./engine/model-catalog.js";export{compactMessages,findTurnBoundaries,SUMMARY_PREFIX,SUMMARY_ACK}from"./engine/compactor.js";export{ROLL_RESOURCE_HINTS_META_KEY,buildAgentToolset}from"./tool-bridge/build-tools.js";export{COMMAND_CLASSIFICATIONS,unknownCommandClassifier}from"./types/command-classification.js";export{ruleBasedClassifier}from"./bash/classifier/index.js";export{buildPowerShellEncodedCommand,resolveShellProfile,SHELL_PROFILE_IDS,SHELL_TOOL_NAMES}from"./bash/profile.js";export{buildSessionExecToolset,EXEC_COMMAND_ID,EXEC_LIST_ID,EXEC_POLL_ID}from"./tool-bridge/session-exec-tool.js";export{ToolRegistry}from"./tool-bridge/naming.js";export{TOOL_RESOURCE_ACCESS_MODES,TOOL_RESOURCE_HINT_KINDS}from"./tool-bridge/tool-execution-coordinator.js";export{TOOL_CANCELLATION_EXECUTION_STATES,TOOL_OUTCOME_KINDS,normalizeToolResult,readToolOutcome}from"./tool-bridge/normalize-result.js";export{ThreadStore,defaultThreadsDir,expandTilde}from"./store/thread-store.js";export{stripCancelledTurnRecoveryMessages}from"./engine/cancelled-turn-recovery.js";export{THREAD_ORIGIN_KINDS,threadOriginSchema,scheduledThreadOriginSchema,threadDerivedFromSchema}from"./store/thread-origin.js";export{SCHEDULE_LEDGER_READ_STATUSES,ScheduleStore,readScheduleLedger,readScheduleOccupancy}from"./scheduler/schedule-store.js";export{SCHEDULER_LIMITS}from"./scheduler/limits.js";export{SCHEDULER_SCHEMA_VERSION}from"./scheduler/database-version.js";export{TRIGGER_KINDS,ScheduleTriggerError,computeNextRunAtMs,computeFirstRunAtMs,createScheduleTrigger,scheduleTimingInputSchema,createIntervalTrigger,describeTrigger,formatDuration,formatInterval,parseIntervalText,parseMaxRunText,parseTriggerJson,triggerSpecSchema}from"./scheduler/trigger.js";export{calendarScheduleSchema,calendarScheduleInputSchema,systemTimeZone}from"./scheduler/calendar.js";export{CANCEL_INVOCATION_OUTCOMES,COMPLETE_INVOCATION_OUTCOMES,EXECUTOR_LIVENESS,INVOCATION_FAILURE_OUTCOMES,INVOCATION_LIVE_STATUSES,INVOCATION_MODES,INVOCATION_STATUSES,INVOCATION_TERMINAL_STATUSES,INVOCATION_TREE_LIVENESS,TRACKED_LEADER_STATES,SCHEDULE_STATUSES,SCHEDULE_STORE_ERROR_CODES,ScheduleStoreError}from"./scheduler/types.js";export{DefaultToolPolicy}from"./policy/default-policy.js";export{ConfigurableToolPolicy}from"./policy/configurable-policy.js";export{UNATTENDED_CONFIRMATION_DENIED_REASON,UnattendedToolPolicy}from"./policy/unattended-policy.js";export{CAPABILITY_HOST_MODES,CAPABILITY_TURN_ORIGIN_KINDS}from"./engine/capability-manifest.js";export{ApprovalGate}from"./approval/approval-gate.js";export{RuntimeServer}from"./server/runtime-server.js";export{RuntimeClientRequestCancelledError,RuntimeClientRequestCoordinator,RuntimeClientRequestError,RuntimeClientRequestExpiredError,createRuntimeClientResponderId}from"./server/runtime-client-request-coordinator.js";export{RuntimeService,RuntimeServiceError}from"./service/runtime-service.js";export{AttachmentStore}from"./service/attachment-store.js";export{createStdioConnection}from"./server/transport/stdio.js";export{RpcMethod,EVENT_NOTIFICATION,isRequest}from"./server/protocol.js";export{createTurnCancellationMessage,readTurnCancellationReason,SESSION_CANCELLATION_REASONS,USER_CANCELLATION_ABORT_REASON}from"./types/cancellation.js";export{APPROVAL_DIFF_PREVIEW_KEY,fileChangeDiffSchema,fileChangeDisplaySchema,getApprovalDiffPreview,getFileChangeDisplay}from"@roll-agent/protocol";export{readScheduleHistory,readScheduleRun}from"./scheduler/schedule-store.js";
1
+ export{ConversationEngine,buildSessionBashSettings,buildSessionExecSettings}from"./engine/conversation-engine.js";export{runtimeSettingsSchema}from"./host-contracts/runtime-settings.js";export{buildScheduleToolset,SCHEDULE_CREATE_TOOL_ID}from"./tool-bridge/schedule-tool.js";export{scheduleCreateInputSchema}from"./tool-bridge/schedule-create-input.js";export{AgentSession}from"./engine/agent-session.js";export{WORKSPACE_INSTRUCTION_FILE_NAMES,WORKSPACE_INSTRUCTIONS_MAX_CHARS,WORKSPACE_INSTRUCTIONS_MODES,createWorkspaceInstructionsSource,findWorkspaceInstructionsPath,parseWorkspaceInstructionsSetting}from"./engine/workspace-instructions.js";export{resolveContextWindow,lookupContextWindow,resolveModelContextWindow,CONTEXT_WINDOW_SOURCES}from"./engine/context-window.js";export{createDefaultModelCatalog}from"./engine/model-catalog-default.js";export{ModelCatalog,defaultModelCatalogCachePath,lookupCatalogContextWindow,trimModelCatalog,MODEL_CATALOG_REFRESH_RESULTS}from"./engine/model-catalog.js";export{compactMessages,findTurnBoundaries,SUMMARY_PREFIX,SUMMARY_ACK}from"./engine/compactor.js";export{ROLL_RESOURCE_HINTS_META_KEY,buildAgentToolset}from"./tool-bridge/build-tools.js";export{COMMAND_CLASSIFICATIONS,unknownCommandClassifier}from"./types/command-classification.js";export{ruleBasedClassifier}from"./bash/classifier/index.js";export{buildPowerShellEncodedCommand,resolveShellProfile,SHELL_PROFILE_IDS,SHELL_TOOL_NAMES}from"./bash/profile.js";export{buildSessionExecToolset,EXEC_COMMAND_ID,EXEC_LIST_ID,EXEC_POLL_ID}from"./tool-bridge/session-exec-tool.js";export{ToolRegistry}from"./tool-bridge/naming.js";export{TOOL_RESOURCE_ACCESS_MODES,TOOL_RESOURCE_HINT_KINDS}from"./tool-bridge/tool-execution-coordinator.js";export{TOOL_CANCELLATION_EXECUTION_STATES,TOOL_OUTCOME_KINDS,normalizeToolResult,readToolOutcome}from"./tool-bridge/normalize-result.js";export{ThreadStore,defaultThreadsDir,expandTilde}from"./store/thread-store.js";export{stripCancelledTurnRecoveryMessages}from"./engine/cancelled-turn-recovery.js";export{THREAD_ORIGIN_KINDS,threadOriginSchema,scheduledThreadOriginSchema,threadDerivedFromSchema}from"./store/thread-origin.js";export{SCHEDULE_LEDGER_READ_STATUSES,ScheduleStore,readScheduleLedger,readScheduleOccupancy}from"./scheduler/schedule-store.js";export{SCHEDULER_LIMITS}from"./scheduler/limits.js";export{SCHEDULER_SCHEMA_VERSION}from"./scheduler/database-version.js";export{TRIGGER_KINDS,ScheduleTriggerError,computeNextRunAtMs,computeFirstRunAtMs,createScheduleTrigger,scheduleTimingInputSchema,createIntervalTrigger,describeTrigger,formatDuration,formatInterval,parseIntervalText,parseMaxRunText,parseTriggerJson,triggerSpecSchema}from"./scheduler/trigger.js";export{calendarScheduleSchema,calendarScheduleInputSchema,systemTimeZone}from"./scheduler/calendar.js";export{CANCEL_INVOCATION_OUTCOMES,COMPLETE_INVOCATION_OUTCOMES,EXECUTOR_LIVENESS,INVOCATION_FAILURE_OUTCOMES,INVOCATION_LIVE_STATUSES,INVOCATION_MODES,INVOCATION_STATUSES,INVOCATION_TERMINAL_STATUSES,INVOCATION_TREE_LIVENESS,TRACKED_LEADER_STATES,SCHEDULE_STATUSES,SCHEDULE_STORE_ERROR_CODES,ScheduleStoreError}from"./scheduler/types.js";export{DefaultToolPolicy}from"./policy/default-policy.js";export{ConfigurableToolPolicy}from"./policy/configurable-policy.js";export{UNATTENDED_CONFIRMATION_DENIED_REASON,UnattendedToolPolicy}from"./policy/unattended-policy.js";export{CAPABILITY_HOST_MODES,CAPABILITY_TURN_ORIGIN_KINDS}from"./engine/capability-manifest.js";export{ApprovalGate}from"./approval/approval-gate.js";export{RuntimeServer}from"./server/runtime-server.js";export{RuntimeClientRequestCancelledError,RuntimeClientRequestCoordinator,RuntimeClientRequestError,RuntimeClientRequestExpiredError,createRuntimeClientResponderId}from"./server/runtime-client-request-coordinator.js";export{RuntimeService,RuntimeServiceError}from"./service/runtime-service.js";export{AttachmentStore}from"./service/attachment-store.js";export{createStdioConnection}from"./server/transport/stdio.js";export{RpcMethod,EVENT_NOTIFICATION,isRequest}from"./server/protocol.js";export{createTurnCancellationMessage,readTurnCancellationReason,SESSION_CANCELLATION_REASONS,USER_CANCELLATION_ABORT_REASON}from"./types/cancellation.js";export{APPROVAL_DIFF_PREVIEW_KEY,fileChangeDiffSchema,fileChangeDisplaySchema,getApprovalDiffPreview,getFileChangeDisplay}from"@roll-agent/protocol";export{readScheduleHistory,readScheduleRun}from"./scheduler/schedule-store.js";export{SESSION_OCCUPANCY_CAPABILITY,SessionOccupiedError,SessionClaims,canonicalSessionDatabasePath}from"./ownership/session-claims.js";export{inspectThreadLibrary,migrateThreadLibrary,ThreadLibraryMigrationRequiredError,THREAD_STORE_SCHEMA_VERSION}from"./store/thread-store.js";
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import type { SessionOwnershipHost } from "../host-contracts/session-ownership.ts";
3
+ export declare const SESSION_OCCUPANCY_CAPABILITY: {
4
+ readonly version: 1;
5
+ readonly scope: "canonical-library-and-thread";
6
+ readonly claims: "sqlite-sidecar-cas";
7
+ readonly idleOwnership: true;
8
+ readonly forceTakeover: false;
9
+ readonly legacyWriters: "unverified-sequential-only";
10
+ };
11
+ declare const claimSchema: z.ZodObject<{
12
+ thread_id: z.ZodString;
13
+ claim_id: z.ZodString;
14
+ identity: z.ZodString;
15
+ owner_json: z.ZodString;
16
+ }, z.core.$strip>;
17
+ type Claim = z.infer<typeof claimSchema>;
18
+ export declare class SessionOccupiedError extends Error {
19
+ readonly details: {
20
+ readonly reason: string;
21
+ readonly threadId: string;
22
+ readonly ownerKind?: string;
23
+ readonly label?: string;
24
+ readonly workspace?: string;
25
+ readonly action: string;
26
+ };
27
+ constructor(threadId: string, claim?: Claim, reason?: string);
28
+ }
29
+ /** Symlinks share the canonical SQLite journal. Hardlinks do not; reject before opening writable. */
30
+ export declare function canonicalSessionDatabasePath(path: string): string;
31
+ export interface SessionClaim {
32
+ readonly threadId: string;
33
+ readonly token: string;
34
+ assert(): void;
35
+ release(): void;
36
+ }
37
+ /**
38
+ * Cooperative local-process fencing. The sidecar is never unlinked/replaced. A dead process
39
+ * cannot write; a live/unknown process is never reaped. This is not a distributed FS lock and
40
+ * does not constrain old binaries or external effects surviving their parent process.
41
+ */
42
+ export declare class SessionClaims {
43
+ private readonly db;
44
+ private readonly held;
45
+ private closed;
46
+ readonly databasePath: string;
47
+ constructor(path: string, privateHost: SessionOwnershipHost);
48
+ private readonly host;
49
+ private read;
50
+ private checkOpen;
51
+ private reap;
52
+ acquire(threadId: string): SessionClaim;
53
+ private acquireKey;
54
+ /** No writer can enter while maintenance owns this gate; any held writer blocks it. */
55
+ maintenance(): SessionClaim;
56
+ /** Keep first creation and writable preflight under the same cross-process SQLite gate. */
57
+ beginLibraryInitialization(isNew: () => boolean): () => void;
58
+ private assert;
59
+ private handle;
60
+ /** Refuse to convert resource disposal into proof that unsettled sessions are safe. */
61
+ close(): void;
62
+ }
63
+ export {};
@@ -0,0 +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)}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 = 9;
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=9;const e=`roll_scheduler_writer_v${String(9)}`;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
+ 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{RUNTIME_ERROR_CODES as e,jsonValueSchema as s}from"@roll-agent/protocol";import{createSafeCapabilitySnapshot as t}from"../engine/capability-manifest.js";import{isPersistedToolExecutionRecord as o}from"../tool-bridge/tool-execution-record.js";import{RuntimeServiceError as r}from"../service/runtime-service.js";import{RuntimeClientRequestCoordinator as i}from"./runtime-client-request-coordinator.js";import{RuntimeProtocolAdapter as n}from"./runtime-protocol-adapter.js";import{EVENT_NOTIFICATION as a,RpcMethod as c,abortParamsSchema as d,approveParamsSchema as l,capabilitiesParamsSchema as u,closeParamsSchema as p,compactParamsSchema as h,createParamsSchema as m,isRequest as f,messagesParamsSchema as v,rejectParamsSchema as w,resumeParamsSchema as g,sendParamsSchema as y,toolExecutionsParamsSchema as R}from"./protocol.js";function I(e){return e instanceof Error?e.message:String(e)}function S(e){return e.map(e=>{const{providerOptions:s,...t}=e;if("string"==typeof t.content)return t;const o=t.content.filter(e=>"reasoning"!==e.type).map(e=>{if(!("providerOptions"in e))return e;const{providerOptions:s,...t}=e;return t});return{...t,content:o}}).filter(e=>!("assistant"===e.role&&Array.isArray(e.content)&&0===e.content.length))}class MethodNotFoundError extends Error{}class RuntimeTransportWriteError extends Error{}function C(e){return e instanceof Error&&"ZodError"===e.name&&"issues"in e&&Array.isArray(e.issues)}function b(e,t){if(e instanceof r){const t=s.safeParse(e.details);return{code:-32e3,message:e.message,data:{rollCode:e.rollCode,retryable:e.retryable,...t.success?{details:t.data}:{}}}}return C(e)?{code:-32602,message:"Invalid params",data:{rollCode:"INVALID_PARAMS",retryable:!1}}:e instanceof MethodNotFoundError?{code:-32601,message:e.message}:{code:-32603,message:t?"Internal error":I(e),data:{rollCode:"INTERNAL_ERROR",retryable:!1}}}const A={unselected:"unselected",legacy:"legacy",runtime:"runtime"},E=new Set(Object.values(c));export class RuntimeServer{engine;connection;authorizeRawToolEvidence;sessions=new Map;runtimeService;runtimeClientRequests;protocolAdapter;connectionMode=A.unselected;abortPromise;constructor(e,s,t={}){this.engine=e,this.connection=s,this.authorizeRawToolEvidence=t.authorizeRawToolEvidence,this.runtimeService=t.runtimeService,this.runtimeClientRequests=void 0===t.runtimeService?void 0:t.runtimeClientRequests??new i,this.protocolAdapter=void 0===t.runtimeService||void 0===this.runtimeClientRequests?void 0:new n(t.runtimeService,s,this.runtimeClientRequests);try{this.connection.onMessage(e=>this.handleMessage(e))}catch(e){throw this.protocolAdapter?.disposeConstructionFailure(),e}}async abortAll(){this.abortPromise??=this.performAbortAll(),await this.abortPromise}async performAbortAll(){const e=[...this.sessions.values()];this.sessions.clear();const s=this.protocolAdapter?.close();this.runtimeClientRequests?.cancelAll("Runtime Server 已关闭");try{await Promise.all([...e.map(e=>e.close()),...void 0===s?[]:[s],...void 0===this.runtimeService?[]:[this.runtimeService.close()]])}finally{this.protocolAdapter?.releaseServiceControl()}}handleMessage(e){if(!0===this.protocolAdapter?.handleResponse(e))return;if(!f(e))return;const s=!0===this.protocolAdapter?.handles(e.method);try{this.selectConnectionMode(e.method,s)}catch(t){return void this.sendOrClose({jsonrpc:"2.0",id:e.id,error:b(t,s)})}this.dispatch(e).then(s=>{const t=this.sendOrClose({jsonrpc:"2.0",id:e.id,result:s.result});s.afterResponse?.(t)},t=>{t instanceof RuntimeTransportWriteError||this.sendOrClose({jsonrpc:"2.0",id:e.id,error:b(t,s)})})}sendOrClose(e){try{return this.connection.send(e),!0}catch{try{this.connection.close()}catch{}return!1}}sendNotificationOrThrow(e){if(!this.sendOrClose(e))throw new RuntimeTransportWriteError("Runtime transport write failed")}requireSession(e){const s=this.sessions.get(e);if(!s)throw new Error(`Session "${e}" 不存在`);return s}selectConnectionMode(s,t){if(t){if(this.connectionMode===A.legacy)throw new r(e.capabilityUnavailable,"当前连接已选择 legacy session.* 协议,不能切换到 Runtime Protocol");this.connectionMode=A.runtime}else if(E.has(s)){if(this.connectionMode===A.runtime)throw new r(e.capabilityUnavailable,"当前连接已选择 Runtime Protocol,不能调用 legacy session.* 方法");this.connectionMode=A.legacy}}async dispatch(e){return!0===this.protocolAdapter?.handles(e.method)?this.protocolAdapter.dispatch(e):{result:await this.dispatchLegacy(e)}}async dispatchLegacy(e){switch(e.method){case c.Create:{const s=m.parse(e.params),t=await this.engine.createSession(s.title?{title:s.title}:{});return this.sessions.set(t.id,t),{sessionId:t.id}}case c.Resume:{const s=g.parse(e.params),t=await this.engine.resumeSession(s.threadId);return this.sessions.set(t.id,t),{sessionId:t.id}}case c.Send:{const s=y.parse(e.params),t=this.requireSession(s.sessionId);for await(const e of t.send(s.input))"reasoning-delta"!==e.type&&this.sendNotificationOrThrow({jsonrpc:"2.0",method:a,params:{sessionId:s.sessionId,event:"confirmation-required"===e.type?{...e,fullDiff:void 0}:e}});return{status:"completed"}}case c.Approve:{const s=l.parse(e.params);return{resolved:this.requireSession(s.sessionId).approve(s.approvalId)}}case c.Reject:{const s=w.parse(e.params);return{resolved:this.requireSession(s.sessionId).reject(s.approvalId,s.reason)}}case c.Abort:{const s=d.parse(e.params);return{ok:!0,cancelled:this.requireSession(s.sessionId).cancel()}}case c.Close:{const s=p.parse(e.params),t=this.requireSession(s.sessionId);return await t.close(),this.sessions.delete(s.sessionId),{closed:!0}}case c.Messages:{const s=v.parse(e.params);return{messages:S(this.requireSession(s.sessionId).getMessages())}}case c.ToolExecutions:{const s=R.parse(e.params),t=this.requireSession(s.sessionId);if(s.includeRaw&&!await(this.authorizeRawToolEvidence?.({sessionId:s.sessionId,...void 0!==s.executionId?{executionId:s.executionId}:{}})))throw new Error("Raw Tool evidence access denied by RuntimeServer policy");if(void 0!==s.executionId){const e=t.getToolExecution(s.executionId,s.includeRaw);if(s.includeRaw&&void 0!==e&&!o(e))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{record:e}}const r=t.getToolExecutions({...void 0!==s.afterSequence?{afterSequence:s.afterSequence}:{},...void 0!==s.limit?{limit:s.limit}:{},...void 0!==s.toolCallId?{toolCallId:s.toolCallId}:{}},s.includeRaw);if(s.includeRaw&&r.some(e=>!o(e)))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{records:r}}case c.Capabilities:{const s=u.parse(e.params),o=this.requireSession(s.sessionId);return t(o.getCapabilityManifest(),o.getCapabilityTurnContext())}case c.Compact:{const s=h.parse(e.params),t=this.requireSession(s.sessionId);for await(const e of t.compact("manual"))this.sendNotificationOrThrow({jsonrpc:"2.0",method:a,params:{sessionId:s.sessionId,event:e}});return{status:"completed"}}default:throw new MethodNotFoundError(`Method not found: ${e.method}`)}}}
1
+ import{SessionOccupiedError as e}from"../ownership/session-claims.js";import{RUNTIME_ERROR_CODES as s,jsonValueSchema as t}from"@roll-agent/protocol";import{createSafeCapabilitySnapshot as o}from"../engine/capability-manifest.js";import{isPersistedToolExecutionRecord as r}from"../tool-bridge/tool-execution-record.js";import{RuntimeServiceError as i}from"../service/runtime-service.js";import{RuntimeClientRequestCoordinator as n}from"./runtime-client-request-coordinator.js";import{RuntimeProtocolAdapter as a}from"./runtime-protocol-adapter.js";import{EVENT_NOTIFICATION as c,RpcMethod as d,abortParamsSchema as l,approveParamsSchema as u,capabilitiesParamsSchema as p,closeParamsSchema as h,compactParamsSchema as m,createParamsSchema as f,isRequest as v,messagesParamsSchema as w,rejectParamsSchema as g,resumeParamsSchema as y,sendParamsSchema as R,toolExecutionsParamsSchema as I}from"./protocol.js";function S(e){return e instanceof Error?e.message:String(e)}function C(e){return e.map(e=>{const{providerOptions:s,...t}=e;if("string"==typeof t.content)return t;const o=t.content.filter(e=>"reasoning"!==e.type).map(e=>{if(!("providerOptions"in e))return e;const{providerOptions:s,...t}=e;return t});return{...t,content:o}}).filter(e=>!("assistant"===e.role&&Array.isArray(e.content)&&0===e.content.length))}class MethodNotFoundError extends Error{}class RuntimeTransportWriteError extends Error{}function b(e){return e instanceof Error&&"ZodError"===e.name&&"issues"in e&&Array.isArray(e.issues)}function A(s,o){if(s instanceof i||s instanceof e){const o=t.safeParse(s.details);return{code:-32e3,message:s.message,data:{rollCode:s instanceof e?"THREAD_BUSY":s.rollCode,retryable:s instanceof e||s.retryable,...o.success?{details:o.data}:{}}}}return b(s)?{code:-32602,message:"Invalid params",data:{rollCode:"INVALID_PARAMS",retryable:!1}}:s instanceof MethodNotFoundError?{code:-32601,message:s.message}:{code:-32603,message:o?"Internal error":S(s),data:{rollCode:"INTERNAL_ERROR",retryable:!1}}}const E={unselected:"unselected",legacy:"legacy",runtime:"runtime"},q=new Set(Object.values(d));export class RuntimeServer{engine;connection;authorizeRawToolEvidence;sessions=new Map;runtimeService;runtimeClientRequests;protocolAdapter;connectionMode=E.unselected;abortPromise;constructor(e,s,t={}){this.engine=e,this.connection=s,this.authorizeRawToolEvidence=t.authorizeRawToolEvidence,this.runtimeService=t.runtimeService,this.runtimeClientRequests=void 0===t.runtimeService?void 0:t.runtimeClientRequests??new n,this.protocolAdapter=void 0===t.runtimeService||void 0===this.runtimeClientRequests?void 0:new a(t.runtimeService,s,this.runtimeClientRequests);try{this.connection.onMessage(e=>this.handleMessage(e))}catch(e){throw this.protocolAdapter?.disposeConstructionFailure(),e}}async abortAll(){this.abortPromise??=this.performAbortAll(),await this.abortPromise}async performAbortAll(){const e=[...this.sessions.values()];this.sessions.clear();const s=this.protocolAdapter?.close();this.runtimeClientRequests?.cancelAll("Runtime Server 已关闭");try{await Promise.all([...e.map(e=>e.close()),...void 0===s?[]:[s],...void 0===this.runtimeService?[]:[this.runtimeService.close()]])}finally{this.protocolAdapter?.releaseServiceControl()}}handleMessage(e){if(!0===this.protocolAdapter?.handleResponse(e))return;if(!v(e))return;const s=!0===this.protocolAdapter?.handles(e.method);try{this.selectConnectionMode(e.method,s)}catch(t){return void this.sendOrClose({jsonrpc:"2.0",id:e.id,error:A(t,s)})}this.dispatch(e).then(s=>{const t=this.sendOrClose({jsonrpc:"2.0",id:e.id,result:s.result});s.afterResponse?.(t)},t=>{t instanceof RuntimeTransportWriteError||this.sendOrClose({jsonrpc:"2.0",id:e.id,error:A(t,s)})})}sendOrClose(e){try{return this.connection.send(e),!0}catch{try{this.connection.close()}catch{}return!1}}sendNotificationOrThrow(e){if(!this.sendOrClose(e))throw new RuntimeTransportWriteError("Runtime transport write failed")}requireSession(e){const s=this.sessions.get(e);if(!s)throw new Error(`Session "${e}" 不存在`);return s}selectConnectionMode(e,t){if(t){if(this.connectionMode===E.legacy)throw new i(s.capabilityUnavailable,"当前连接已选择 legacy session.* 协议,不能切换到 Runtime Protocol");this.connectionMode=E.runtime}else if(q.has(e)){if(this.connectionMode===E.runtime)throw new i(s.capabilityUnavailable,"当前连接已选择 Runtime Protocol,不能调用 legacy session.* 方法");this.connectionMode=E.legacy}}async dispatch(e){return!0===this.protocolAdapter?.handles(e.method)?this.protocolAdapter.dispatch(e):{result:await this.dispatchLegacy(e)}}async dispatchLegacy(e){switch(e.method){case d.Create:{const s=f.parse(e.params),t=await this.engine.createSession(s.title?{title:s.title}:{});return this.sessions.set(t.id,t),{sessionId:t.id}}case d.Resume:{const s=y.parse(e.params),t=await this.engine.resumeSession(s.threadId);return this.sessions.set(t.id,t),{sessionId:t.id}}case d.Send:{const s=R.parse(e.params),t=this.requireSession(s.sessionId);for await(const e of t.send(s.input))"reasoning-delta"!==e.type&&this.sendNotificationOrThrow({jsonrpc:"2.0",method:c,params:{sessionId:s.sessionId,event:"confirmation-required"===e.type?{...e,fullDiff:void 0}:e}});return{status:"completed"}}case d.Approve:{const s=u.parse(e.params);return{resolved:this.requireSession(s.sessionId).approve(s.approvalId)}}case d.Reject:{const s=g.parse(e.params);return{resolved:this.requireSession(s.sessionId).reject(s.approvalId,s.reason)}}case d.Abort:{const s=l.parse(e.params);return{ok:!0,cancelled:this.requireSession(s.sessionId).cancel()}}case d.Close:{const s=h.parse(e.params),t=this.requireSession(s.sessionId);return await t.close(),this.sessions.delete(s.sessionId),{closed:!0}}case d.Messages:{const s=w.parse(e.params);return{messages:C(this.requireSession(s.sessionId).getMessages())}}case d.ToolExecutions:{const s=I.parse(e.params),t=this.requireSession(s.sessionId);if(s.includeRaw&&!await(this.authorizeRawToolEvidence?.({sessionId:s.sessionId,...void 0!==s.executionId?{executionId:s.executionId}:{}})))throw new Error("Raw Tool evidence access denied by RuntimeServer policy");if(void 0!==s.executionId){const e=t.getToolExecution(s.executionId,s.includeRaw);if(s.includeRaw&&void 0!==e&&!r(e))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{record:e}}const o=t.getToolExecutions({...void 0!==s.afterSequence?{afterSequence:s.afterSequence}:{},...void 0!==s.limit?{limit:s.limit}:{},...void 0!==s.toolCallId?{toolCallId:s.toolCallId}:{}},s.includeRaw);if(s.includeRaw&&o.some(e=>!r(e)))throw new Error("Raw Tool evidence is unavailable without a durable ledger projection");return{records:o}}case d.Capabilities:{const s=p.parse(e.params),t=this.requireSession(s.sessionId);return o(t.getCapabilityManifest(),t.getCapabilityTurnContext())}case d.Compact:{const s=m.parse(e.params),t=this.requireSession(s.sessionId);for await(const e of t.compact("manual"))this.sendNotificationOrThrow({jsonrpc:"2.0",method:c,params:{sessionId:s.sessionId,event:e}});return{status:"completed"}}default:throw new MethodNotFoundError(`Method not found: ${e.method}`)}}}
@@ -1 +1 @@
1
- import{createHash as e,randomUUID as t}from"node:crypto";import{appendFileSync as a,lstatSync as s,mkdirSync as r,readdirSync as i,readFileSync as c,rmSync as d,writeFileSync as n}from"node:fs";import{basename as o,extname as m,isAbsolute as h,join as u}from"node:path";import{attachmentIdSchema as l,attachmentMediaTypeSchema as g,RUNTIME_ERROR_CODES as p,RUNTIME_V14_MAX_ATTACHMENT_BYTES as f,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as y,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as k,SUPPORTED_ATTACHMENT_MEDIA_TYPES as x}from"@roll-agent/protocol";const I=6e5,S=18e5,T={"image/png":["png"],"image/jpeg":["jpg","jpeg"],"image/gif":["gif"],"image/webp":["webp"],"application/pdf":["pdf"],"text/plain":["txt","log","text"],"text/markdown":["md","markdown"]};function v(e){const t=o(e).replace(/[\\/:]+/gu,"").replace(/\p{Cc}+/gu,"").trim();return t.length>0?t:"attachment"}function w(t){return e("sha256").update(t).digest("hex")}function B(e,t){const a=T[t];if(void 0===a)return!1;const s=m(e).slice(1).toLowerCase();return a.includes(s)}function A(e,t){let a;try{a=i(e,{withFileTypes:!0})}catch{return}const r=Date.now()-t;for(const t of a){if(!t.isDirectory())continue;const a=u(e,t.name);try{s(a).mtimeMs<r&&d(a,{recursive:!0,force:!0})}catch{continue}}}export class AttachmentStore{dir;maxAttachmentBytes;maxStagedAttachments;stagedTtlMs;committedTtlMs;now;records=new Map;closed=!1;constructor(e){this.maxAttachmentBytes=e.maxAttachmentBytes??f,this.maxStagedAttachments=e.maxStagedAttachments??k,this.stagedTtlMs=e.stagedTtlMs??I,this.committedTtlMs=e.committedTtlMs??S,this.now=e.now??Date.now,this.dir=u(e.dir,t()),A(e.dir,2*Math.max(this.stagedTtlMs,this.committedTtlMs)),r(this.dir,{recursive:!0})}get limits(){return{maxAttachmentBytes:this.maxAttachmentBytes,maxAttachmentChunkBytes:y,maxStagedAttachments:this.maxStagedAttachments}}stage(e){if(this.sweepExpired(),e.bytes>this.maxAttachmentBytes)return{ok:!1,code:p.attachmentTooLarge,message:`附件超过单文件上限 ${String(this.maxAttachmentBytes)} 字节`};if(!(e.mediaType in T))return{ok:!1,code:p.attachmentTypeUnsupported,message:`不支持的附件类型: ${e.mediaType}(支持 ${x.join(", ")})`};if(!B(e.fileName,e.mediaType))return{ok:!1,code:p.attachmentTypeUnsupported,message:`文件扩展名与 mediaType "${e.mediaType}" 不一致`};if(this.records.size>=this.maxStagedAttachments)return{ok:!1,code:p.attachmentQuotaExceeded,message:`附件数已达上限 ${String(this.maxStagedAttachments)}:附件被 turn.start 引用后不会自动释放,请调用 attachment.release 释放不再需要的附件,或等待 TTL 回收`,retryable:!0};const a=l.parse(t()),s={attachmentId:a,threadId:e.threadId,fileName:e.fileName,displayName:v(e.fileName),mediaType:e.mediaType,declaredBytes:e.bytes,declaredSha256:e.sha256,source:e.source,createdAtMs:this.now(),state:"staged",receivedBytes:0,nextSequence:0,committedAtMs:void 0};if("chunks"===e.source)return n(this.filePath(a),Buffer.alloc(0)),this.records.set(a,s),{ok:!0,attachmentId:a,state:"staged"};const r=this.loadLocalFile(e.sourcePath??"",e.mediaType,e.bytes,e.sha256);return r.ok?(n(this.filePath(a),r.data),s.state="committed",s.receivedBytes=r.data.length,s.committedAtMs=this.now(),this.records.set(a,s),{ok:!0,attachmentId:a,state:"committed",descriptor:this.describe(s)}):r}appendChunk(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("staged"!==t.state||"chunks"!==t.source)return{ok:!1,code:p.invalidParams,message:"附件不处于 chunk 上传状态"};if(e.sequence!==t.nextSequence)return{ok:!1,code:p.invalidParams,message:`chunk sequence 不连续:期望 ${String(t.nextSequence)},收到 ${String(e.sequence)}`};const s=Buffer.from(e.dataBase64,"base64");return 0===s.length||s.length>y?{ok:!1,code:p.invalidParams,message:`chunk 大小必须在 1..${String(y)} 字节内`}:t.receivedBytes+s.length>t.declaredBytes?(this.remove(t.attachmentId),{ok:!1,code:p.attachmentTooLarge,message:"累计上传字节数超过申报大小,附件已回收"}):(a(this.filePath(t.attachmentId),s),t.receivedBytes+=s.length,t.nextSequence+=1,{ok:!0,receivedBytes:t.receivedBytes,nextSequence:t.nextSequence})}commit(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("committed"===t.state)return{ok:!0,descriptor:this.describe(t)};if(t.receivedBytes!==t.declaredBytes)return{ok:!1,code:p.attachmentUploadIncomplete,message:`上传未完成:已收 ${String(t.receivedBytes)} 字节,申报 ${String(t.declaredBytes)} 字节`};return w(c(this.filePath(t.attachmentId)))!==t.declaredSha256?(this.remove(t.attachmentId),{ok:!1,code:p.attachmentHashMismatch,message:"附件内容 sha256 与申报值不匹配,附件已回收"}):(t.state="committed",t.committedAtMs=this.now(),{ok:!0,descriptor:this.describe(t)})}release(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);return void 0===t?{ok:!0,released:!1}:(this.remove(t.attachmentId),{ok:!0,released:!0})}readCommitted(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("committed"!==t.state)return{ok:!1,code:p.attachmentNotCommitted,message:`附件 "${e.attachmentId}" 尚未 commit`};let a;try{a=c(this.filePath(t.attachmentId))}catch{return this.remove(t.attachmentId),{ok:!1,code:p.internalError,message:"附件数据文件缺失,附件已回收"}}return{ok:!0,descriptor:this.describe(t),dataBase64:a.toString("base64")}}releaseThread(e){for(const t of[...this.records.values()])t.threadId===e&&this.remove(t.attachmentId)}close(){this.closed||(this.closed=!0,this.records.clear(),d(this.dir,{recursive:!0,force:!0}))}describe(e){return{attachmentId:e.attachmentId,fileName:e.fileName,displayName:e.displayName,mediaType:g.parse(e.mediaType),bytes:e.declaredBytes,sha256:e.declaredSha256,source:e.source,createdAt:new Date(e.createdAtMs).toISOString()}}loadLocalFile(e,t,a,r){if(!h(e))return{ok:!1,code:p.attachmentPathRejected,message:"sourcePath 必须是绝对路径"};if(!B(e,t))return{ok:!1,code:p.attachmentTypeUnsupported,message:`sourcePath 扩展名与 mediaType "${t}" 不一致`};let i;try{i=s(e)}catch{return{ok:!1,code:p.attachmentPathRejected,message:"sourcePath 不存在或不可读"}}if(i.isSymbolicLink())return{ok:!1,code:p.attachmentPathRejected,message:"sourcePath 不允许是符号链接"};if(!i.isFile())return{ok:!1,code:p.attachmentPathRejected,message:"sourcePath 不是普通文件"};if(i.size>this.maxAttachmentBytes)return{ok:!1,code:p.attachmentTooLarge,message:`文件实际大小 ${String(i.size)} 超过单文件上限 ${String(this.maxAttachmentBytes)} 字节`};if(i.size!==a)return{ok:!1,code:p.invalidParams,message:`文件实际大小 ${String(i.size)} 与申报 ${String(a)} 不一致`};const d=c(e);return w(d)!==r?{ok:!1,code:p.attachmentHashMismatch,message:"文件内容 sha256 与申报值不匹配"}:{ok:!0,data:d}}findOwned(e,t){const a=this.records.get(t);if(void 0!==a&&a.threadId===e)return a}notFound(e){return{ok:!1,code:p.attachmentNotFound,message:`附件 "${e}" 不存在、已过期或不属于该 Thread`}}remove(e){this.records.delete(e),d(this.filePath(e),{force:!0})}sweepExpired(){const e=this.now();for(const t of[...this.records.values()]){const a="staged"===t.state?this.stagedTtlMs:this.committedTtlMs;e-(t.committedAtMs??t.createdAtMs)>a&&this.remove(t.attachmentId)}}filePath(e){return u(this.dir,`${e}.bin`)}}
1
+ import{createHash as e,randomUUID as t}from"node:crypto";import{appendFileSync as a,lstatSync as s,mkdirSync as r,readFileSync as i,rmSync as d,writeFileSync as n}from"node:fs";import{basename as c,extname as o,isAbsolute as m,join as h}from"node:path";import{attachmentIdSchema as u,attachmentMediaTypeSchema as l,RUNTIME_ERROR_CODES as g,RUNTIME_V14_MAX_ATTACHMENT_BYTES as p,RUNTIME_V14_MAX_ATTACHMENT_CHUNK_BYTES as f,RUNTIME_V14_MAX_STAGED_ATTACHMENTS as y,SUPPORTED_ATTACHMENT_MEDIA_TYPES as k}from"@roll-agent/protocol";const x=6e5,I=18e5,S={"image/png":["png"],"image/jpeg":["jpg","jpeg"],"image/gif":["gif"],"image/webp":["webp"],"application/pdf":["pdf"],"text/plain":["txt","log","text"],"text/markdown":["md","markdown"]};function v(e){const t=c(e).replace(/[\\/:]+/gu,"").replace(/\p{Cc}+/gu,"").trim();return t.length>0?t:"attachment"}function T(t){return e("sha256").update(t).digest("hex")}function B(e,t){const a=S[t];if(void 0===a)return!1;const s=o(e).slice(1).toLowerCase();return a.includes(s)}export class AttachmentStore{dir;maxAttachmentBytes;maxStagedAttachments;stagedTtlMs;committedTtlMs;now;records=new Map;closed=!1;constructor(e){this.maxAttachmentBytes=e.maxAttachmentBytes??p,this.maxStagedAttachments=e.maxStagedAttachments??y,this.stagedTtlMs=e.stagedTtlMs??x,this.committedTtlMs=e.committedTtlMs??I,this.now=e.now??Date.now,this.dir=h(e.dir,t()),r(this.dir,{recursive:!0})}get limits(){return{maxAttachmentBytes:this.maxAttachmentBytes,maxAttachmentChunkBytes:f,maxStagedAttachments:this.maxStagedAttachments}}stage(e){if(this.sweepExpired(),e.bytes>this.maxAttachmentBytes)return{ok:!1,code:g.attachmentTooLarge,message:`附件超过单文件上限 ${String(this.maxAttachmentBytes)} 字节`};if(!(e.mediaType in S))return{ok:!1,code:g.attachmentTypeUnsupported,message:`不支持的附件类型: ${e.mediaType}(支持 ${k.join(", ")})`};if(!B(e.fileName,e.mediaType))return{ok:!1,code:g.attachmentTypeUnsupported,message:`文件扩展名与 mediaType "${e.mediaType}" 不一致`};if(this.records.size>=this.maxStagedAttachments)return{ok:!1,code:g.attachmentQuotaExceeded,message:`附件数已达上限 ${String(this.maxStagedAttachments)}:附件被 turn.start 引用后不会自动释放,请调用 attachment.release 释放不再需要的附件,或等待 TTL 回收`,retryable:!0};const a=u.parse(t()),s={attachmentId:a,threadId:e.threadId,fileName:e.fileName,displayName:v(e.fileName),mediaType:e.mediaType,declaredBytes:e.bytes,declaredSha256:e.sha256,source:e.source,createdAtMs:this.now(),state:"staged",receivedBytes:0,nextSequence:0,committedAtMs:void 0};if("chunks"===e.source)return n(this.filePath(a),Buffer.alloc(0)),this.records.set(a,s),{ok:!0,attachmentId:a,state:"staged"};const r=this.loadLocalFile(e.sourcePath??"",e.mediaType,e.bytes,e.sha256);return r.ok?(n(this.filePath(a),r.data),s.state="committed",s.receivedBytes=r.data.length,s.committedAtMs=this.now(),this.records.set(a,s),{ok:!0,attachmentId:a,state:"committed",descriptor:this.describe(s)}):r}appendChunk(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("staged"!==t.state||"chunks"!==t.source)return{ok:!1,code:g.invalidParams,message:"附件不处于 chunk 上传状态"};if(e.sequence!==t.nextSequence)return{ok:!1,code:g.invalidParams,message:`chunk sequence 不连续:期望 ${String(t.nextSequence)},收到 ${String(e.sequence)}`};const s=Buffer.from(e.dataBase64,"base64");return 0===s.length||s.length>f?{ok:!1,code:g.invalidParams,message:`chunk 大小必须在 1..${String(f)} 字节内`}:t.receivedBytes+s.length>t.declaredBytes?(this.remove(t.attachmentId),{ok:!1,code:g.attachmentTooLarge,message:"累计上传字节数超过申报大小,附件已回收"}):(a(this.filePath(t.attachmentId),s),t.receivedBytes+=s.length,t.nextSequence+=1,{ok:!0,receivedBytes:t.receivedBytes,nextSequence:t.nextSequence})}commit(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("committed"===t.state)return{ok:!0,descriptor:this.describe(t)};if(t.receivedBytes!==t.declaredBytes)return{ok:!1,code:g.attachmentUploadIncomplete,message:`上传未完成:已收 ${String(t.receivedBytes)} 字节,申报 ${String(t.declaredBytes)} 字节`};return T(i(this.filePath(t.attachmentId)))!==t.declaredSha256?(this.remove(t.attachmentId),{ok:!1,code:g.attachmentHashMismatch,message:"附件内容 sha256 与申报值不匹配,附件已回收"}):(t.state="committed",t.committedAtMs=this.now(),{ok:!0,descriptor:this.describe(t)})}release(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);return void 0===t?{ok:!0,released:!1}:(this.remove(t.attachmentId),{ok:!0,released:!0})}readCommitted(e){this.sweepExpired();const t=this.findOwned(e.threadId,e.attachmentId);if(void 0===t)return this.notFound(e.attachmentId);if("committed"!==t.state)return{ok:!1,code:g.attachmentNotCommitted,message:`附件 "${e.attachmentId}" 尚未 commit`};let a;try{a=i(this.filePath(t.attachmentId))}catch{return this.remove(t.attachmentId),{ok:!1,code:g.internalError,message:"附件数据文件缺失,附件已回收"}}return{ok:!0,descriptor:this.describe(t),dataBase64:a.toString("base64")}}releaseThread(e){for(const t of[...this.records.values()])t.threadId===e&&this.remove(t.attachmentId)}close(){this.closed||(this.closed=!0,this.records.clear(),d(this.dir,{recursive:!0,force:!0}))}describe(e){return{attachmentId:e.attachmentId,fileName:e.fileName,displayName:e.displayName,mediaType:l.parse(e.mediaType),bytes:e.declaredBytes,sha256:e.declaredSha256,source:e.source,createdAt:new Date(e.createdAtMs).toISOString()}}loadLocalFile(e,t,a,r){if(!m(e))return{ok:!1,code:g.attachmentPathRejected,message:"sourcePath 必须是绝对路径"};if(!B(e,t))return{ok:!1,code:g.attachmentTypeUnsupported,message:`sourcePath 扩展名与 mediaType "${t}" 不一致`};let d;try{d=s(e)}catch{return{ok:!1,code:g.attachmentPathRejected,message:"sourcePath 不存在或不可读"}}if(d.isSymbolicLink())return{ok:!1,code:g.attachmentPathRejected,message:"sourcePath 不允许是符号链接"};if(!d.isFile())return{ok:!1,code:g.attachmentPathRejected,message:"sourcePath 不是普通文件"};if(d.size>this.maxAttachmentBytes)return{ok:!1,code:g.attachmentTooLarge,message:`文件实际大小 ${String(d.size)} 超过单文件上限 ${String(this.maxAttachmentBytes)} 字节`};if(d.size!==a)return{ok:!1,code:g.invalidParams,message:`文件实际大小 ${String(d.size)} 与申报 ${String(a)} 不一致`};const n=i(e);return T(n)!==r?{ok:!1,code:g.attachmentHashMismatch,message:"文件内容 sha256 与申报值不匹配"}:{ok:!0,data:n}}findOwned(e,t){const a=this.records.get(t);if(void 0!==a&&a.threadId===e)return a}notFound(e){return{ok:!1,code:g.attachmentNotFound,message:`附件 "${e}" 不存在、已过期或不属于该 Thread`}}remove(e){this.records.delete(e),d(this.filePath(e),{force:!0})}sweepExpired(){const e=this.now();for(const t of[...this.records.values()]){const a="staged"===t.state?this.stagedTtlMs:this.committedTtlMs;e-(t.committedAtMs??t.createdAtMs)>a&&this.remove(t.attachmentId)}}filePath(e){return h(this.dir,`${e}.bin`)}}
@@ -5,7 +5,7 @@ import type { SessionUserInputRequestId } from "../interaction/user-input-intera
5
5
  import type { ReasoningPresentation } from "../types/events.ts";
6
6
  import { ThreadStore, type StoredRuntimeEvent } from "../store/thread-store.ts";
7
7
  type RollErrorCode = RuntimeProtocolErrorDataV14["rollCode"];
8
- export type RuntimeServiceSession = Pick<AgentSession, "id" | "send" | "approve" | "reject" | "cancel" | "close" | "getCapabilityManifest" | "getCapabilityTurnContext"> & Partial<Pick<AgentSession, "setUserInputAvailable" | "resolveUserInput" | "cancelUserInput">>;
8
+ export type RuntimeServiceSession = Pick<AgentSession, "id" | "send" | "approve" | "reject" | "cancel" | "close" | "getCapabilityManifest" | "getCapabilityTurnContext"> & Partial<Pick<AgentSession, "setUserInputAvailable" | "resolveUserInput" | "cancelUserInput" | "assertWritable" | "addCloseBarrier">>;
9
9
  export interface RuntimeServiceEngine {
10
10
  createSession(input?: {
11
11
  readonly title?: string;
@@ -74,11 +74,17 @@ export declare class MutationRequestCache {
74
74
  private readonly settled;
75
75
  constructor(maxEntries: number);
76
76
  run<TMethod extends MutationRuntimeMethod>(requestId: string, method: TMethod, params: LatestRuntimeMethodParams<TMethod>, action: () => LatestRuntimeMethodResult<TMethod> | Promise<LatestRuntimeMethodResult<TMethod>>): Promise<LatestRuntimeMethodResult<TMethod>>;
77
+ settlePending(): Promise<void>;
77
78
  clear(): void;
78
79
  private assertMatchingRequest;
79
80
  private settle;
80
81
  }
81
82
  export declare class RuntimeService {
83
+ private closePromise;
84
+ private readonly openingSessions;
85
+ private readonly closingThreads;
86
+ private readonly ownership;
87
+ private readonly turnJobs;
82
88
  readonly runtimeInstanceId: RuntimeInstanceId;
83
89
  private readonly engine;
84
90
  private readonly store;
@@ -145,6 +151,9 @@ export declare class RuntimeService {
145
151
  private assertOpen;
146
152
  private requireThread;
147
153
  private requireSession;
154
+ private assertThreadOpen;
155
+ private retainOwnership;
156
+ private finishThread;
148
157
  private rememberSession;
149
158
  private emit;
150
159
  private notifyFatalError;