@x-otto/schedule 0.0.1-alpha.4 → 0.1.0-alpha.9

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.d.ts CHANGED
@@ -1,23 +1,7 @@
1
1
  import { Migration, Persistence } from "@x-otto/persistence";
2
+ import { ThinkingLevel } from "@x-otto/interchange";
2
3
  import { AgentJobRegistry } from "@x-otto/runtime";
3
4
 
4
- //#region ../interchange/dist/index.d.ts
5
- //#endregion
6
- //#region src/model.d.ts
7
- /**
8
- * 推理投入档(reasoning effort)——RFC-235 对齐 Claude Code 词表:`low/medium/high/xhigh/max`。
9
- * 去除历史遗留的 `minimal`(无对应厂商语义,存量配置载入时迁移为 `low`),新增 `max`(Claude
10
- * Code 最高档 + DeepSeek `reasoning_effort:'max'` 顶档)。各模型真实支持的子集由 manifest 的
11
- * `Model.thinkingLevels` 声明,provider 组装请求体时按"≤该档最高支持档"降级(见 RFC-235 §3.3)。
12
- */
13
- type ThinkingLevel = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
14
- /**
15
- * 模型能力优势标签(供大模型为 subagent 选型)。非 /models API 返回——由
16
- * model-capabilities 据「精选家族图 + 启发式」推导。
17
- * planning=方案/架构 · knowledge=知识/通识 · coding=编码 · reasoning=深度推理
18
- * · vision=视觉 · speed=低延迟 · long-context=长上下文
19
- */
20
- //#endregion
21
5
  //#region src/types.d.ts
22
6
  /** A scheduled task persisted to ${OTTO_HOME}/schedules/<id>.json */
23
7
  interface ScheduledTask {
@@ -213,7 +197,7 @@ interface FireOwnership {
213
197
  * Create a local token-based O_EXCL FireOwnership, scoped to `workspaceKey` (RFC-169 D1).
214
198
  * Only the lock holder runs the ticker and fires tasks.
215
199
  */
216
- declare function createLocalFireOwnership(workspaceKey: string): FireOwnership;
200
+ declare function createLocalFireOwnership(workspaceKey: string, clock?: () => number): FireOwnership;
217
201
  //#endregion
218
202
  //#region src/schedule-config.d.ts
219
203
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":["ContextFragment","ContextFragmentId","boundFragment","contextFragmentBytes","AnthropicImageMime","CommonImageMime","NormalizedImage","normalizeImageData","toAnthropicImageMime","A2uiComponent","A2uiContent","Api","AssistantMessage","CONVERSATION_SUMMARY_PREFIX","ContentPart","ImageContent","KnownApi","KnownProvider","Message","Provider","StopReason","SystemNotificationMessage","TextContent","ThinkingContent","ToolCall","ToolResultMessage","Usage","UserMessage","ZodType","getToolCallsByAssistantMessage","hasToolCalls","DismissedSigil","PluginInputRegistry","SigilEntry","SigilKind","SigilPrefix","SigilProvider","SigilResolveContext","SigilResolver","SigilSource","SigilTrigger","SigilTriggerDecision","TextChangeSource","adjustDismissedSigil","applySigilCompletion","applySigilReplacement","evaluateSigilTrigger","extractSigilPrefix","isSigilDismissed","sigilOf","ProviderUsageSnapshot","status","limitType","resetsAt","usedPercent","overageStatus","overageResetsAt","overageDisabledReason","standardLimits","remainingRequests","remainingTokens","limitRequests","limitTokens","resetRequestsMs","resetTokensMs","StreamEvent","Error","type","partial","index","delta","content","toolCall","reason","message","providerUsage","error","ThinkingLevel","ModelStrength","Cost","input","output","cacheRead","cacheWrite","Model","T","id","name","api","provider","baseUrl","reasoning","cost","contextWindow","maxOutputTokens","thinkingLevels","thinkingMode","strengths","imageConstraints","maxImagesPerRequest","maxDimensionPxIfOverLimit","supportsTools","responseBodyPolicy","store","sendMaxOutputTokens","ModelSpec","CapabilityFact","state","value","modelImageCapability","Pick","model","modelSupportsImages","ModelCapabilityFacts","vision","collectModelCapabilityFacts","SystemPromptLane","SystemPromptSectionSpec","priority","lane","budgetBytes","SYSTEM_PROMPT_SECTIONS","toolGuidance","skillLoopGuidance","environment","agentKnowledge","skillCatalog","lessonRelevantHints","internalize","memoryDelta","todoReminder","docSyncReminder","SYSTEM_PROMPT_SECTION_ORDER","applySectionBudget","ToolErrorKind","ToolResult","Record","isError","errorKind","details","pluginSource","extractToolResultText","result","ToolCallContext","Params","AbortSignal","params","signal","sessionId","agentName","depth","parentTaskId","onUpdate","update","ToolCallEvent","arguments","ToolDescriptionContext","projectRoot","AgentTool","Promise","description","ctx","parameters","execute","ToolExecutionPolicy","exposure","readonly","interruptBehavior","resultBudgetChars","isConcurrencySafe","pathParams","resolvePath","rawPath","async","lifecycleAsync","lifecycleAsyncNote","regenerableOutput","AgentToolDefinition","Partial","AskUserQuestion","text","options","GrillOption","label","rationale","recommended","preview","GrillQuestion","header","question","background","recommendation","allowFreeform","multiSelect","evidence","askedBy","GrillAnswer","answer","selectedOptionLabels","acceptedRecommendation","resolvedBy","freeformText","GrillRequest","Omit","AgentSessionEventType","AgentSessionEvent","PromptStopReason","tokenUsage","thinkingLevel","stopReason","errorMessage","round","maxRounds","incompleteCount","event","role","from","to","turnIndex","toolTurnCount","maxToolTurns","promptOutputTokens","promptOutputTokenBudget","maxTurns","extended","steered","progressDenied","dimension","used","budget","windowTurns","maxRepeat","outcome","retryCount","totalCostUSD","budgetUSD","resultText","resultTokensEstimate","messageCount","retainedCount","summary","messagesBefore","messagesAfter","degradedCount","trigger","tokensSaved","prunedCount","clearedCount","removedCount","capValue","lossy","attemptedCount","describedCount","failedCount","lostTurns","lastPersistedAt","lastTraceAt","toolName","risk","semanticVerdict","approved","requestId","planContent","action","componentId","payload","AgentSessionSubscriber","AgentSessionEventMap","E","EVENT_DOMAIN","EventDomain","getEventDomain","eventType","EngineCommand","kind","approvalId","modelId","replacement","todos","files","subagents","count","FileCommand","argumentHint","allowedTools","body","source","scope","pluginId","filePath","TodoStatus","TodoItem","title","ChildProcessConfig","command","args","owner","category","lifecycle","cwd","ProcessTracker","registerChild","config","pid","child","PluginPerfMetricResult"],"sources":["../../interchange/dist/index.d.ts","../src/types.ts","../src/registry.ts","../src/schedule-store.ts","../src/fire-ownership.ts","../src/schedule-config.ts","../src/fire-target.ts","../src/trigger.ts","../src/scheduler.ts","../src/cron-parser.ts","../src/remote-schedule-store.ts","../src/schedule-lease-migrations.ts","../src/remote-fire-ownership.ts","../src/fire-logger.ts","../src/fire-targets/job-fire-target.ts","../src/fire-targets/session-fire-target.ts","../src/fire-targets/subscriber-fire-target.ts","../src/triggers/in-process-event-trigger.ts","../src/schedule-capability.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/registry.ts","../src/schedule-store.ts","../src/fire-ownership.ts","../src/schedule-config.ts","../src/fire-target.ts","../src/trigger.ts","../src/scheduler.ts","../src/cron-parser.ts","../src/remote-schedule-store.ts","../src/schedule-lease-migrations.ts","../src/remote-fire-ownership.ts","../src/fire-logger.ts","../src/fire-targets/job-fire-target.ts","../src/fire-targets/session-fire-target.ts","../src/fire-targets/subscriber-fire-target.ts","../src/triggers/in-process-event-trigger.ts","../src/schedule-capability.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,5 +4,5 @@ import{closeSync as e,mkdirSync as t,openSync as n,readFileSync as r,readdirSync
4
4
  token TEXT NOT NULL,
5
5
  renewed_at INTEGER NOT NULL
6
6
  );
7
- `)}}];function G(e){return f(p,`schedule-owner-${e}.lock`)}function K(i){let u=x(),f=G(i),y=m(),b=!1,S=()=>{try{t(p,{recursive:!0});let r=n(f,`wx`);try{d(r,g(u,process.pid,y))}finally{e(r)}return b=!0,!0}catch(e){if(e.code!==`EEXIST`)throw e;return!1}},C=()=>{try{return h(r(f,`utf-8`).trim())}catch{return null}},w=()=>C()?.token??null,T=e=>e.pid===void 0||e.host===void 0||e.host!==y?!1:!_(e.pid),E=e=>e.pid===void 0||e.host===void 0||e.host!==y?!1:_(e.pid),D=()=>{if(b)return!0;if(v())return!1;if(S())return!0;let e=C(),t;try{t=s(f).mtimeMs}catch{return S()}if(e!==null&&E(e)||!(e!==null&&T(e))&&Date.now()-t<=3e5)return!1;let n=`${f}.stale-${x()}`;try{a(f,n),o(n,{force:!0})}catch{return!1}return S()};return{async tryAcquire(){return D()},async renew(){if(b){if(w()!==u){b=!1;return}try{let e=new Date;l(f,e,e)}catch{b=!1}}},async release(){if(b){if(w()===u)try{c(f)}catch{}b=!1}},isOwner(){return b&&w()===u},describeOwner(){let e=C();if(e!==null&&!(e.pid===void 0&&e.host===void 0))return{...e.pid===void 0?{}:{pid:e.pid},...e.host===void 0?{}:{host:e.host}}}}}function q(e){let t=x(),n=e.fetch??globalThis.fetch,r=e.timeoutMs??1e4,i=new URL(e.sessionUrl).origin,a=e.now??Date.now,o=!1,s=0,c,l,u=async a=>{let o=await e.getAuth(),s=new AbortController,c=setTimeout(()=>s.abort(),r);try{let r=await n(`${i}/api/schedules/${encodeURIComponent(e.wsKey)}/lease/${a}`,{method:`POST`,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${o.token}`},body:JSON.stringify({token:t}),signal:s.signal});return r.ok?{ok:!0,data:await r.json()}:{ok:!1,reason:`network-error`}}catch{return{ok:!1,reason:`network-error`}}finally{clearTimeout(c)}},d=()=>{if(c)return c;let e=(async()=>{s=a();let e=await u(`acquire`);if(!e.ok)return o=!1,l=`network-error`,!1;let t=e.data.acquired===!0;return o=t,l=t?void 0:`occupied`,t})();return c=e,e.finally(()=>{c===e&&(c=void 0)}),e};return{tryAcquire:d,async renew(){if(a()-s<3e4)return;if(!o){await d();return}s=a();let e=await u(`renew`);if(!e.ok){o=!1,l=`network-error`;return}e.data.renewed===!0?l=void 0:(o=!1,l=`occupied`)},async release(){o&&=(await u(`release`),!1)},isOwner(){return o},lastFailureReason(){return o?void 0:l}}}var J=class{type=`job`;runningJobsByTaskId=new Map;cancelRequested=new Set;_nextFireToken=0;constructor(e){this.deps=e}canHandle(e){return!e.subscriber&&e.target!==`session`}async fire(e,t){let n=`failed`,r=!1,i,a=++this._nextFireToken,o=e.origin===`model`?`main`:`user`,s=this.deps.getSessionId?.()??``;for(let c=0;c<=e.maxRetries;c++){if(this.cancelRequested.has(e.id)){this.deps.fireLogger.write(e.id,`canceled by user (attempt ${c+1})`),r=!0;break}let l;try{l=this.deps.startJob({title:e.name,prompt:e.prompt,sessionId:s,origin:o,...e.modelId?{model:{id:e.modelId,thinkingLevel:e.thinkingLevel}}:{}})}catch(t){if(i=t instanceof Error?t.message:String(t),this.deps.fireLogger.write(e.id,`startJob failed (attempt ${c+1}): ${i}`),c<e.maxRetries){let e=this.deps.retryBackoffMs??0;await(this.deps.sleep??(e=>new Promise(t=>setTimeout(t,e))))(e)}continue}this.runningJobsByTaskId.set(e.id,{jobId:l.id,fireToken:a,attempt:c});let u=await this.waitForCompletion(l.id,e.maxDurationMs,t);if(u===`success`){let t=this.deps.jobRegistry.get(l.id),r=t?.status===`ready`&&!!t.diff?.trim();return this.deps.fireLogger.write(e.id,r?`completed (attempt ${c+1}) — diff ready, /job apply ${l.id}`:`completed (attempt ${c+1})`),n=`success`,{outcome:n,jobId:l.id}}if(u===`canceled`){this.deps.fireLogger.write(e.id,`canceled by user (attempt ${c+1})`),r=!0;break}if(i=u===`timeout`?`timed out`:`job failed`,c<e.maxRetries){let e=this.deps.retryBackoffMs??0;await(this.deps.sleep??(e=>new Promise(t=>setTimeout(t,e))))(e)}}return r||this.deps.fireLogger.write(e.id,`failed after ${e.maxRetries+1} attempt(s)`),{outcome:n,cancelled:r,error:i}}requestCancel(e){let t,n=this.runningJobsByTaskId.get(e);if(n)t=n.jobId;else{let e=this.deps.jobRegistry.list().filter(e=>e.status===`running`||e.status===`needs_input`);e.length===1&&(t=e[0].id)}if(!t)return{ok:!1,reason:`not_running`};let r=this.deps.jobRegistry.get(t);return r?r.status===`ready`?{ok:!1,reason:`job_is_ready`}:r.status===`applied`?{ok:!1,reason:`job_already_applied`}:r.status===`failed`?{ok:!1,reason:`job_already_failed`}:r.status===`canceled`?{ok:!1,reason:`job_already_canceled`}:(this.cancelRequested.add(e),this.deps.jobRegistry.cancel(t),{ok:!0}):{ok:!1,reason:`job_not_found`}}cleanup(e){this.runningJobsByTaskId.delete(e),this.cancelRequested.delete(e)}getRunningTaskIds(){return new Set(this.runningJobsByTaskId.keys())}waitForCompletion(e,t,n){let r=this.deps.needsInputMaxExtensionFactor??3;return new Promise(i=>{let a=!1,o,s,c,l=n.now()+t*r,u=()=>{o&&clearTimeout(o),s?.(),c?.()},d=e=>{a||(a=!0,u(),i(e))},f=t=>{o&&clearTimeout(o),o=setTimeout(()=>{this.deps.jobRegistry.cancel(e),d(`failed`)},t)};c=this.deps.jobRegistry.onUpdate(r=>{if(r.id===e){if(r.status===`ready`)d(`success`);else if(r.status===`needs_input`){let r=n.now(),i=Math.min(t,l-r);if(i<=0){this.deps.jobRegistry.cancel(e),d(`failed`);return}f(i)}}}),s=this.deps.jobRegistry.onExit(t=>{t.id===e&&(t.status===`failed`?d(`failed`):t.status===`canceled`?d(`canceled`):t.status===`applied`&&d(`success`))}),f(t)})}},Y=class{type=`session`;constructor(e){this.deps=e}canHandle(e){return e.target===`session`}getSessionId(){return this.deps.getSessionId()}async fire(e,t){if(!e.boundSessionId)return this.deps.fireLogger.write(e.id,`session tick skipped: task has no boundSessionId (invalid state)`),{outcome:`failed`,error:`no boundSessionId`};let n=e.origin===`model`&&e.lastFiredAt==null,r=`⏰ scheduled: ${e.name}`,i=n?`${r} [created by the model — first fire, use /schedule cancel ${e.id} to stop it]\n\n${e.prompt}`:`${r}\n\n${e.prompt}`,a=!1;try{a=this.deps.followUp({prompt:i,boundSessionId:e.boundSessionId,name:e.name,origin:e.origin})}catch(t){return this.deps.fireLogger.write(e.id,`session tick error: ${t instanceof Error?t.message:String(t)}`),{outcome:`failed`,error:t instanceof Error?t.message:String(t)}}return this.deps.fireLogger.write(e.id,a?`session tick delivered (session ${e.boundSessionId})`:`session tick not delivered (session ${e.boundSessionId} unavailable — evicted/disposed/archived/paused/mismatched)`),{outcome:a?`success`:`failed`}}},X=class{type=`plugin-service`;constructor(e){this.deps=e}canHandle(e){return!!e.subscriber}async fire(e,t){let n=e.subscriber;if(!n)return{outcome:`failed`,error:`no subscriber on task`};let r=t.now(),i=`${e.id}@${t.scheduledFor??r}`,a=!1;try{a=this.deps.notify(n,{scheduleId:e.id,occurrenceId:i,occurredAt:r})}catch(t){return this.deps.fireLogger.write(e.id,`subscriber tick error: ${t instanceof Error?t.message:String(t)}`),{outcome:`failed`,error:t instanceof Error?t.message:String(t)}}return this.deps.fireLogger.write(e.id,a?`subscriber tick delivered (${n.pluginId}:${n.serviceId}, occurrence ${i})`:`subscriber tick not delivered (${n.pluginId}:${n.serviceId} not running)`),{outcome:a?`success`:`failed`}}},Z=class{type=`in-process-event`;fireTimestamps=new Map;now;debounceWindowMs;maxFiresPerWindow;constructor(e){this.deps=e,this.now=e.now??Date.now,this.debounceWindowMs=e.debounceWindowMs??6e4,this.maxFiresPerWindow=e.maxFiresPerWindow??5}canHandle(e){return e.eventTrigger?.source===`job`}arm(e,t){let n=e.eventTrigger;return!n||n.source!==`job`?()=>{}:this.deps.jobRegistry.onExit(r=>{if(this.matches(r,n)){if(!this.passesDebounce(e.id)){this.deps.logger?.error(`[schedules] event-trigger ${e.id} debounced (>${this.maxFiresPerWindow} fires / ${this.debounceWindowMs}ms)`);return}t(e)}})}matches(e,t){return!(t.event!==e.status||t.filter?.sessionId&&t.filter.sessionId!==e.sessionId)}passesDebounce(e){let t=this.now(),n=t-this.debounceWindowMs,r=(this.fireTimestamps.get(e)??[]).filter(e=>e>n);return r.length>=this.maxFiresPerWindow?(this.fireTimestamps.set(e,r),!1):(r.push(t),this.fireTimestamps.set(e,r),!0)}};function Q(e,t={}){return{create:n=>{let r=e();if(!r)throw Error(`Schedule scheduler is not available in this session.`);if(n.modelId){let e=t.validateModelId?.(n.modelId);if(e)throw Error(e)}let i=r.add({name:n.name??`schedule`,prompt:n.prompt,cronExpression:n.cron,recurring:n.recurring,origin:`model`,maxDurationMs:n.maxDurationMs,target:n.target,eventTrigger:n.eventTrigger,modelId:n.modelId,thinkingLevel:n.thinkingLevel});return{taskId:i.id,nextFireAt:i.nextFireAt,name:i.name,target:i.target}},list:()=>(e()?.list()??[]).map(e=>({id:e.id,name:e.name,cronExpression:e.cronExpression,enabled:e.enabled,recurring:e.recurring,nextFireAt:e.nextFireAt,lastFiredAt:e.lastFiredAt,lastFireFailed:e.lastFireFailed,lastFireError:e.lastFireError,lastFireCancelled:e.lastFireCancelled})),delete:t=>e()?.remove(t)??!1,run:t=>{e()?.runNow(t)},cancel:t=>e()?.cancel(t)??{ok:!1,reason:`Schedule scheduler is not available.`}}}export{F as DEFAULT_SCHEDULE_CONFIG,P as FireLogger,Z as InProcessEventTrigger,J as JobFireTarget,W as SCHEDULE_LEASE_MIGRATIONS,j as ScheduleRegistry,z as SchedulerService,Y as SessionFireTarget,X as SubscriberFireTarget,K as createLocalFireOwnership,q as createRemoteFireOwnership,U as createRemoteScheduleStore,Q as createScheduleCapability,V as createScheduleStore,S as parseCron};
7
+ `)}}];function G(e){return f(p,`schedule-owner-${e}.lock`)}function K(i,u){let f=u??Date.now,y=x(),b=G(i),S=m(),C=!1,w=()=>{try{t(p,{recursive:!0});let r=n(b,`wx`);try{d(r,g(y,process.pid,S))}finally{e(r)}return C=!0,!0}catch(e){if(e.code!==`EEXIST`)throw e;return!1}},T=()=>{try{return h(r(b,`utf-8`).trim())}catch{return null}},E=()=>T()?.token??null,D=e=>e.pid===void 0||e.host===void 0||e.host!==S?!1:!_(e.pid),O=e=>e.pid===void 0||e.host===void 0||e.host!==S?!1:_(e.pid),k=()=>{if(C)return!0;if(v())return!1;if(w())return!0;let e=T(),t;try{t=s(b).mtimeMs}catch{return w()}if(e!==null&&O(e)||!(e!==null&&D(e))&&f()-t<=3e5)return!1;let n=`${b}.stale-${x()}`;try{a(b,n),o(n,{force:!0})}catch{return!1}return w()};return{async tryAcquire(){return k()},async renew(){if(C){if(E()!==y){C=!1;return}try{let e=new Date;l(b,e,e)}catch{C=!1}}},async release(){if(C){if(E()===y)try{c(b)}catch{}C=!1}},isOwner(){return C&&E()===y},describeOwner(){let e=T();if(e!==null&&!(e.pid===void 0&&e.host===void 0))return{...e.pid===void 0?{}:{pid:e.pid},...e.host===void 0?{}:{host:e.host}}}}}function q(e){let t=x(),n=e.fetch??globalThis.fetch,r=e.timeoutMs??1e4,i=new URL(e.sessionUrl).origin,a=e.now??Date.now,o=!1,s=0,c,l,u=async a=>{let o=await e.getAuth(),s=new AbortController,c=setTimeout(()=>s.abort(),r);try{let r=await n(`${i}/api/schedules/${encodeURIComponent(e.wsKey)}/lease/${a}`,{method:`POST`,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${o.token}`},body:JSON.stringify({token:t}),signal:s.signal});return r.ok?{ok:!0,data:await r.json()}:{ok:!1,reason:`network-error`}}catch{return{ok:!1,reason:`network-error`}}finally{clearTimeout(c)}},d=()=>{if(c)return c;let e=(async()=>{s=a();let e=await u(`acquire`);if(!e.ok)return o=!1,l=`network-error`,!1;let t=e.data.acquired===!0;return o=t,l=t?void 0:`occupied`,t})();return c=e,e.finally(()=>{c===e&&(c=void 0)}),e};return{tryAcquire:d,async renew(){if(a()-s<3e4)return;if(!o){await d();return}s=a();let e=await u(`renew`);if(!e.ok){o=!1,l=`network-error`;return}e.data.renewed===!0?l=void 0:(o=!1,l=`occupied`)},async release(){o&&=(await u(`release`),!1)},isOwner(){return o},lastFailureReason(){return o?void 0:l}}}var J=class{type=`job`;runningJobsByTaskId=new Map;cancelRequested=new Set;_nextFireToken=0;constructor(e){this.deps=e}canHandle(e){return!e.subscriber&&e.target!==`session`}async fire(e,t){let n=`failed`,r=!1,i,a=++this._nextFireToken,o=e.origin===`model`?`main`:`user`,s=this.deps.getSessionId?.()??``;for(let c=0;c<=e.maxRetries;c++){if(this.cancelRequested.has(e.id)){this.deps.fireLogger.write(e.id,`canceled by user (attempt ${c+1})`),r=!0;break}let l;try{l=this.deps.startJob({title:e.name,prompt:e.prompt,sessionId:s,origin:o,...e.modelId?{model:{id:e.modelId,thinkingLevel:e.thinkingLevel}}:{}})}catch(t){if(i=t instanceof Error?t.message:String(t),this.deps.fireLogger.write(e.id,`startJob failed (attempt ${c+1}): ${i}`),c<e.maxRetries){let e=this.deps.retryBackoffMs??0;await(this.deps.sleep??(e=>new Promise(t=>setTimeout(t,e))))(e)}continue}this.runningJobsByTaskId.set(e.id,{jobId:l.id,fireToken:a,attempt:c});let u=await this.waitForCompletion(l.id,e.maxDurationMs,t);if(u===`success`){let t=this.deps.jobRegistry.get(l.id),r=t?.status===`ready`&&!!t.diff?.trim();return this.deps.fireLogger.write(e.id,r?`completed (attempt ${c+1}) — diff ready, /job apply ${l.id}`:`completed (attempt ${c+1})`),n=`success`,{outcome:n,jobId:l.id}}if(u===`canceled`){this.deps.fireLogger.write(e.id,`canceled by user (attempt ${c+1})`),r=!0;break}if(i=u===`timeout`?`timed out`:`job failed`,c<e.maxRetries){let e=this.deps.retryBackoffMs??0;await(this.deps.sleep??(e=>new Promise(t=>setTimeout(t,e))))(e)}}return r||this.deps.fireLogger.write(e.id,`failed after ${e.maxRetries+1} attempt(s)`),{outcome:n,cancelled:r,error:i}}requestCancel(e){let t,n=this.runningJobsByTaskId.get(e);if(n)t=n.jobId;else{let e=this.deps.jobRegistry.list().filter(e=>e.status===`running`||e.status===`needs_input`);e.length===1&&(t=e[0].id)}if(!t)return{ok:!1,reason:`not_running`};let r=this.deps.jobRegistry.get(t);return r?r.status===`ready`?{ok:!1,reason:`job_is_ready`}:r.status===`applied`?{ok:!1,reason:`job_already_applied`}:r.status===`failed`?{ok:!1,reason:`job_already_failed`}:r.status===`canceled`?{ok:!1,reason:`job_already_canceled`}:(this.cancelRequested.add(e),this.deps.jobRegistry.cancel(t),{ok:!0}):{ok:!1,reason:`job_not_found`}}cleanup(e){this.runningJobsByTaskId.delete(e),this.cancelRequested.delete(e)}getRunningTaskIds(){return new Set(this.runningJobsByTaskId.keys())}waitForCompletion(e,t,n){let r=this.deps.needsInputMaxExtensionFactor??3;return new Promise(i=>{let a=!1,o,s,c,l=n.now()+t*r,u=()=>{o&&clearTimeout(o),s?.(),c?.()},d=e=>{a||(a=!0,u(),i(e))},f=t=>{o&&clearTimeout(o),o=setTimeout(()=>{this.deps.jobRegistry.cancel(e),d(`failed`)},t)};c=this.deps.jobRegistry.onUpdate(r=>{if(r.id===e){if(r.status===`ready`)d(`success`);else if(r.status===`needs_input`){let r=n.now(),i=Math.min(t,l-r);if(i<=0){this.deps.jobRegistry.cancel(e),d(`failed`);return}f(i)}}}),s=this.deps.jobRegistry.onExit(t=>{t.id===e&&(t.status===`failed`?d(`failed`):t.status===`canceled`?d(`canceled`):t.status===`applied`&&d(`success`))}),f(t)})}},Y=class{type=`session`;constructor(e){this.deps=e}canHandle(e){return e.target===`session`}getSessionId(){return this.deps.getSessionId()}async fire(e,t){if(!e.boundSessionId)return this.deps.fireLogger.write(e.id,`session tick skipped: task has no boundSessionId (invalid state)`),{outcome:`failed`,error:`no boundSessionId`};let n=e.origin===`model`&&e.lastFiredAt==null,r=`⏰ scheduled: ${e.name}`,i=n?`${r} [created by the model — first fire, use /schedule cancel ${e.id} to stop it]\n\n${e.prompt}`:`${r}\n\n${e.prompt}`,a=!1;try{a=this.deps.followUp({prompt:i,boundSessionId:e.boundSessionId,name:e.name,origin:e.origin})}catch(t){return this.deps.fireLogger.write(e.id,`session tick error: ${t instanceof Error?t.message:String(t)}`),{outcome:`failed`,error:t instanceof Error?t.message:String(t)}}return this.deps.fireLogger.write(e.id,a?`session tick delivered (session ${e.boundSessionId})`:`session tick not delivered (session ${e.boundSessionId} unavailable — evicted/disposed/archived/paused/mismatched)`),{outcome:a?`success`:`failed`}}},X=class{type=`plugin-service`;constructor(e){this.deps=e}canHandle(e){return!!e.subscriber}async fire(e,t){let n=e.subscriber;if(!n)return{outcome:`failed`,error:`no subscriber on task`};let r=t.now(),i=`${e.id}@${t.scheduledFor??r}`,a=!1;try{a=this.deps.notify(n,{scheduleId:e.id,occurrenceId:i,occurredAt:r})}catch(t){return this.deps.fireLogger.write(e.id,`subscriber tick error: ${t instanceof Error?t.message:String(t)}`),{outcome:`failed`,error:t instanceof Error?t.message:String(t)}}return this.deps.fireLogger.write(e.id,a?`subscriber tick delivered (${n.pluginId}:${n.serviceId}, occurrence ${i})`:`subscriber tick not delivered (${n.pluginId}:${n.serviceId} not running)`),{outcome:a?`success`:`failed`}}},Z=class{type=`in-process-event`;fireTimestamps=new Map;now;debounceWindowMs;maxFiresPerWindow;constructor(e){this.deps=e,this.now=e.now??Date.now,this.debounceWindowMs=e.debounceWindowMs??6e4,this.maxFiresPerWindow=e.maxFiresPerWindow??5}canHandle(e){return e.eventTrigger?.source===`job`}arm(e,t){let n=e.eventTrigger;return!n||n.source!==`job`?()=>{}:this.deps.jobRegistry.onExit(r=>{if(this.matches(r,n)){if(!this.passesDebounce(e.id)){this.deps.logger?.error(`[schedules] event-trigger ${e.id} debounced (>${this.maxFiresPerWindow} fires / ${this.debounceWindowMs}ms)`);return}t(e)}})}matches(e,t){return!(t.event!==e.status||t.filter?.sessionId&&t.filter.sessionId!==e.sessionId)}passesDebounce(e){let t=this.now(),n=t-this.debounceWindowMs,r=(this.fireTimestamps.get(e)??[]).filter(e=>e>n);return r.length>=this.maxFiresPerWindow?(this.fireTimestamps.set(e,r),!1):(r.push(t),this.fireTimestamps.set(e,r),!0)}};function Q(e,t={}){return{create:n=>{let r=e();if(!r)throw Error(`Schedule scheduler is not available in this session.`);if(n.modelId){let e=t.validateModelId?.(n.modelId);if(e)throw Error(e)}let i=r.add({name:n.name??`schedule`,prompt:n.prompt,cronExpression:n.cron,recurring:n.recurring,origin:`model`,maxDurationMs:n.maxDurationMs,target:n.target,eventTrigger:n.eventTrigger,modelId:n.modelId,thinkingLevel:n.thinkingLevel});return{taskId:i.id,nextFireAt:i.nextFireAt,name:i.name,target:i.target}},list:()=>(e()?.list()??[]).map(e=>({id:e.id,name:e.name,cronExpression:e.cronExpression,enabled:e.enabled,recurring:e.recurring,nextFireAt:e.nextFireAt,lastFiredAt:e.lastFiredAt,lastFireFailed:e.lastFireFailed,lastFireError:e.lastFireError,lastFireCancelled:e.lastFireCancelled})),delete:t=>e()?.remove(t)??!1,run:t=>{e()?.runNow(t)},cancel:t=>e()?.cancel(t)??{ok:!1,reason:`Schedule scheduler is not available.`}}}export{F as DEFAULT_SCHEDULE_CONFIG,P as FireLogger,Z as InProcessEventTrigger,J as JobFireTarget,W as SCHEDULE_LEASE_MIGRATIONS,j as ScheduleRegistry,z as SchedulerService,Y as SessionFireTarget,X as SubscriberFireTarget,K as createLocalFireOwnership,q as createRemoteFireOwnership,U as createRemoteScheduleStore,Q as createScheduleCapability,V as createScheduleStore,S as parseCron};
8
8
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["SNAPSHOT_VERSION"],"sources":["../src/cron-parser.ts","../src/registry.ts","../src/fire-target.ts","../src/fire-executor.ts","../src/fire-logger.ts","../src/schedule-config.ts","../src/tick-loop.ts","../src/schedule-persistence.ts","../src/scheduler.ts","../src/schedule-store.ts","../src/remote-schedule-store.ts","../src/schedule-lease-migrations.ts","../src/fire-ownership.ts","../src/remote-fire-ownership.ts","../src/fire-targets/job-fire-target.ts","../src/fire-targets/session-fire-target.ts","../src/fire-targets/subscriber-fire-target.ts","../src/triggers/in-process-event-trigger.ts","../src/schedule-capability.ts"],"sourcesContent":["// schedule/src/cron-parser.ts — 5-field cron expression parser.\n// Uses local Date API (DST-safe). Search capped at 366 days forward.\n\ntype FieldSet = Set<number>\n\n/**\n * Parse a 5-field cron expression (minute hour dom month dow) and return the next\n * fire time >= from (inclusive). All times in local timezone.\n *\n * Throws on invalid expressions.\n *\n * Field syntax: wildcard (*), single value (5), step (e.g. \\*\\/15 or 5\\/15),\n * range (1-5), list (1,15,30), range with step (1-10\\/2).\n *\n * Unsupported: L, W, ?, #, name aliases (MON, JAN).\n *\n * ## DoM∧DoW semantics\n *\n * If both day-of-month and day-of-week are constrained (not a bare *),\n * a date matches if **EITHER** field matches (standard vixie-cron OR behaviour).\n *\n * ## Day-of-week mapping\n *\n * 0 = Sunday, 1–6 = Monday–Saturday, 7 = also Sunday.\n */\nexport function parseCron(expr: string, from?: Date): Date {\n const { validMinute, validHour, validDom, validMonth, validDow, domConstrained, dowConstrained } =\n parseExpression(expr)\n\n const startTime = from ? new Date(from.getTime()) : new Date()\n startTime.setSeconds(0, 0)\n\n const maxTime = new Date(startTime.getTime() + 366 * 24 * 60 * 60 * 1000)\n const current = new Date(startTime.getTime())\n\n while (current.getTime() <= maxTime.getTime()) {\n // --- minute ---\n const minute = current.getMinutes()\n const nextMin = nextInSet(validMinute, minute)\n if (nextMin === null) {\n current.setMinutes(firstInSet(validMinute))\n current.setHours(current.getHours() + 1)\n current.setSeconds(0, 0)\n continue\n }\n if (nextMin !== minute) {\n current.setMinutes(nextMin)\n current.setSeconds(0, 0)\n continue\n }\n\n // --- hour ---\n const hour = current.getHours()\n const nextHr = nextInSet(validHour, hour)\n if (nextHr === null) {\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setDate(current.getDate() + 1)\n current.setSeconds(0, 0)\n continue\n }\n if (nextHr !== hour) {\n current.setHours(nextHr)\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // --- day (DoM ∨ DoW) ---\n const dom = current.getDate()\n const dow = current.getDay()\n const domMatch = validDom.has(dom)\n const dowMatch = validDow.has(dow)\n\n let dayMatch: boolean\n if (domConstrained && dowConstrained) {\n dayMatch = domMatch || dowMatch\n } else if (domConstrained) {\n dayMatch = domMatch\n } else if (dowConstrained) {\n dayMatch = dowMatch\n } else {\n dayMatch = true\n }\n\n if (!dayMatch) {\n current.setDate(current.getDate() + 1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // --- month ---\n const month = current.getMonth() + 1 // 1-based\n const nextMo = nextInSet(validMonth, month)\n if (nextMo === null) {\n // Wrap to next year\n current.setMonth(firstInSet(validMonth) - 1)\n current.setFullYear(current.getFullYear() + 1)\n current.setDate(1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n if (nextMo !== month) {\n current.setMonth(nextMo - 1)\n current.setDate(1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // All fields matched\n return current\n }\n\n throw new Error(`No matching time found for cron \"${expr}\" within 366 days of ${startTime.toISOString()}`)\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\ninterface ParsedExpression {\n validMinute: FieldSet\n validHour: FieldSet\n validDom: FieldSet\n validMonth: FieldSet\n validDow: FieldSet\n domConstrained: boolean\n dowConstrained: boolean\n}\n\nfunction parseExpression(expr: string): ParsedExpression {\n if (!expr || !expr.trim()) {\n throw new Error('Invalid cron expression: empty string')\n }\n\n const fields = expr.trim().split(/\\s+/)\n if (fields.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length} (${expr})`)\n }\n\n const [minField, hourField, domField, monthField, dowField] = fields as [string, string, string, string, string]\n\n const validMinute = parseField(minField, 0, 59)\n const validHour = parseField(hourField, 0, 23)\n const validDom = parseField(domField, 1, 31)\n const validMonth = parseField(monthField, 1, 12)\n const validDow = parseField(dowField, 0, 7)\n\n // 7 = Sunday, also maps to 0 for Date.getDay() compatibility\n if (validDow.has(7)) {\n validDow.add(0)\n }\n\n // A field is \"constrained\" if the user explicitly specified values (not bare *)\n // 架构 review 2026-08-12 S12:`*/1` 语义等价 `*`(覆盖全域、步长 1),此前按裸字符串\n // 比较被判为 constrained,触发 DoM∧DoW 的 OR 语义——dow 单侧约束被误升为双约束。\n // 全域等价判定只认两种写法(`*` 与 `*/1`);`1-31`/`0-6` 这类显式全域范围仍判\n // constrained(写全范围的作者意图是显式约束,不替用户臆测)。\n const isWildcardEquivalent = (field: string): boolean => field === '*' || field === '*/1'\n const domConstrained = !isWildcardEquivalent(domField)\n const dowConstrained = !isWildcardEquivalent(dowField)\n\n return { validMinute, validHour, validDom, validMonth, validDow, domConstrained, dowConstrained }\n}\n\n/**\n * Parse a single cron field into a set of valid integer values.\n *\n * Supported sub-syntax:\n * - * -> all values in [min, max]\n * - 5 -> single value\n * - *\\/15 or 5\\/15 -> step (start/step)\n * - 1-5 -> inclusive range\n * - 1,15,30 -> comma-separated list\n * - 1-10/2 -> range with step\n *\n * Throws on unsupported syntax (letters, L, W, ?, # etc.) or out-of-range values.\n */\nfunction parseField(field: string, min: number, max: number): FieldSet {\n validateFieldSyntax(field)\n\n const result = new Set<number>()\n const parts = field.split(',')\n\n for (const part of parts) {\n const { range, step } = splitStep(part)\n validateStep(step, part)\n\n if (range === '*') {\n for (let i = min; i <= max; i += step) {\n result.add(i)\n }\n } else if (range.includes('-')) {\n const [startStr, endStr] = range.split('-') as [string, string]\n const start = parseFieldInt(startStr, part)\n const end = parseFieldInt(endStr, part)\n if (start < min || end > max || start > end) {\n throw new Error(`Invalid cron field \"${field}\": range [${start},${end}] out of bounds [${min},${max}]`)\n }\n for (let i = start; i <= end; i += step) {\n result.add(i)\n }\n } else {\n const val = parseFieldInt(range, part)\n if (val < min || val > max) {\n throw new Error(\n `Invalid cron field \"${field}\": value ${val} out of range [${min},${max}]`,\n )\n }\n if (step > 1) {\n // \"N/step\" — start at N, step to max (vixie-cron semantics, e.g. 5/15 → 5,20,35,50)\n for (let i = val; i <= max; i += step) {\n result.add(i)\n }\n } else {\n result.add(val)\n }\n }\n }\n\n return result\n}\n\nfunction validateFieldSyntax(field: string): void {\n // Allow only digits, *, /, -, comma, and whitespace\n const cleaned = field.replace(/[\\d*/\\-,]/g, '')\n if (cleaned.length > 0) {\n throw new Error(`Invalid cron field \"${field}\": unsupported syntax`)\n }\n}\n\nfunction splitStep(part: string): { range: string; step: number } {\n const slashIdx = part.indexOf('/')\n if (slashIdx === -1) {\n return { range: part, step: 1 }\n }\n return {\n range: part.substring(0, slashIdx),\n step: parseInt(part.substring(slashIdx + 1), 10),\n }\n}\n\nfunction validateStep(step: number, part: string): void {\n if (isNaN(step) || step < 1) {\n throw new Error(`Invalid cron field: invalid step in \"${part}\"`)\n }\n}\n\nfunction parseFieldInt(s: string, contextPart: string): number {\n const n = parseInt(s, 10)\n if (isNaN(n)) {\n throw new Error(`Invalid cron field: expected number, got \"${s}\" in \"${contextPart}\"`)\n }\n return n\n}\n\nfunction nextInSet(set: FieldSet, current: number): number | null {\n let best: number | null = null\n for (const v of set) {\n if (v >= current && (best === null || v < best)) {\n best = v\n }\n }\n return best\n}\n\nfunction firstInSet(set: FieldSet): number {\n let best = Infinity\n for (const v of set) {\n if (v < best) best = v\n }\n return best\n}\n","import type { ScheduledTask } from './types'\n\n// schedule/src/registry.ts — In-memory schedule task registry (Map-backed CRUD).\nexport class ScheduleRegistry {\n private tasks: Map<string, ScheduledTask>\n\n constructor() {\n this.tasks = new Map()\n }\n\n add(task: ScheduledTask): void {\n if (this.tasks.has(task.id)) {\n throw new Error(`ScheduleRegistry: duplicate task id \"${task.id}\"`)\n }\n this.tasks.set(task.id, task)\n }\n\n remove(id: string): boolean {\n return this.tasks.delete(id)\n }\n\n update(\n id: string,\n patch: Partial<\n Pick<\n ScheduledTask,\n | 'enabled'\n | 'lastFiredAt'\n | 'nextFireAt'\n | 'maxDurationMs'\n | 'maxRetries'\n | 'lastFireFailed'\n | 'lastFireError'\n | 'lastFireCancelled'\n >\n >,\n ): boolean {\n const task = this.tasks.get(id)\n if (!task) return false\n\n if (patch.enabled !== undefined) task.enabled = patch.enabled\n if (patch.lastFiredAt !== undefined) task.lastFiredAt = patch.lastFiredAt\n if (patch.nextFireAt !== undefined) task.nextFireAt = patch.nextFireAt\n if (patch.maxDurationMs !== undefined) task.maxDurationMs = patch.maxDurationMs\n if (patch.maxRetries !== undefined) task.maxRetries = patch.maxRetries\n if (patch.lastFireFailed !== undefined) task.lastFireFailed = patch.lastFireFailed\n if (patch.lastFireError !== undefined) task.lastFireError = patch.lastFireError\n if (patch.lastFireCancelled !== undefined) task.lastFireCancelled = patch.lastFireCancelled\n\n return true\n }\n\n get(id: string): ScheduledTask | undefined {\n return this.tasks.get(id)\n }\n\n list(): ScheduledTask[] {\n return [...this.tasks.values()].sort((a, b) => a.nextFireAt - b.nextFireAt)\n }\n\n /**\n * Find tasks that are due for execution.\n * Returns all enabled tasks (recurring or one-shot) whose nextFireAt <= now.\n * The 'firing' guard is a scheduler concern (managed via a separate Set).\n *\n * RFC-423:eventTrigger 任务永不由时间\"到点\"——它们由进程内事件驱动(InProcessEventTrigger\n * arm 监听),不进 cron 轮询。此处排除,使 cron 轮询(tick)与事件监听(arm)\n * 两条路径互斥(D5)。\n */\n findDue(now: number): ScheduledTask[] {\n const due: ScheduledTask[] = []\n for (const task of this.tasks.values()) {\n if (task.eventTrigger) continue\n if (task.enabled && task.nextFireAt <= now) {\n due.push(task)\n }\n }\n due.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return due\n }\n\n count(): number {\n return this.tasks.size\n }\n\n has(id: string): boolean {\n return this.tasks.has(id)\n }\n}\n","/**\n * fire-target.ts —— RFC-406 D1/D2:FireTarget 贡献点接口 + 注册表。\n *\n * 投递目标(job / session / subscriber / 自定义)从 scheduler.ts fire() 的硬编码 if/else\n * 分支拆出为独立 handler。插件可注册新 handler(webhook、email 等),不修改 scheduler.ts。\n *\n * 设计参照 deepseek-harness capability-seams 模式:\n * - FireTarget 接口 = Service Definition(契约)\n * - JobFireTarget / SessionFireTarget / SubscriberFireTarget = 内置 Service Provider\n * - ScheduleCapability = Consumer(model 工具消费方)\n * - 插件注册的新 handler = 外部 Service Provider\n */\nimport type { CancelResult, ScheduledTask } from './types'\n\n// ---------------------------------------------------------------------------\n// FireTarget 接口\n// ---------------------------------------------------------------------------\n\nexport interface FireTarget {\n /** 唯一标识('job' | 'session' | 'plugin-service') */\n readonly type: string\n\n /** 判断此 handler 是否能处理该 task。注册顺序决定优先级——先注册的先匹配。 */\n canHandle(task: ScheduledTask): boolean\n\n /**\n * 执行投递。返回 FireResult——scheduler 不关心 handler 内部如何实现,\n * 只根据 outcome 决定后续行为(one-shot 清理 / recurring 记账)。\n */\n fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult>\n\n /**\n * fire() 退出保障钩子(M1 修复①):无论 fire 正常返回、抛错还是早退,executor 的\n * finally 块都会调用。handler 必须在此清理本次 fire 的运行态记账\n * (runningJobs / cancelRequested 等)——漏清会导致 cancel 意图残留、\n * recurring 任务被永久跳过、运行态 Map 无界泄漏。\n */\n cleanup?(taskId: string): void\n\n /**\n * 可选能力:支持取消正在运行的投递(job 路径实现)。\n * SchedulerService.cancel() 路由到当前 fire 该任务的 handler。\n */\n requestCancel?(taskId: string): CancelResult\n\n /** 可选能力:当前有并发运行态的 handler(job 路径)暴露运行中的 task id 集合。 */\n getRunningTaskIds?(): Set<string>\n\n /**\n * 可选能力:session 投递 handler 暴露\"创建任务时绑定的当前 sessionId\"端口,\n * scheduler.add() 经此填充 boundSessionId(RFC-406 D6)。\n */\n getSessionId?(): string\n}\n\n// ---------------------------------------------------------------------------\n// 上下文与结果\n// ---------------------------------------------------------------------------\n\nexport interface FireContext {\n /** 本次计划触发时刻(ticker 路径传入,runNow 用实际时刻)。用于 occurrenceId 派生。 */\n scheduledFor?: number\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\nexport interface FireResult {\n outcome: 'success' | 'failed'\n /** cancel 信号——JobFireTarget 在 cancel() 路径设置 */\n cancelled?: boolean\n error?: string\n /** job-specific 信号(JobFireTarget 附加,用于 runningJobsByTaskId 记账) */\n jobId?: string\n}\n\n// ---------------------------------------------------------------------------\n// FireTargetRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * FireTarget handler 注册表。注册顺序决定匹配优先级——先注册的先匹配。\n *\n * 内置 handler 注册顺序:subscriber > session > job\n * (保持当前 fire() 的分支优先级:subscriber 优先于 target:session,job 是默认路径)。\n */\nexport class FireTargetRegistry {\n private handlers: FireTarget[] = []\n\n register(handler: FireTarget): void {\n this.handlers.push(handler)\n }\n\n find(task: ScheduledTask): FireTarget | undefined {\n return this.handlers.find((h) => h.canHandle(task))\n }\n}\n","/**\n * fire-executor.ts —— RFC-406 D4:Fire 执行器。\n *\n * 从 scheduler.ts fire() 入口拆出。持有 FireTargetRegistry,路由到匹配的 handler。\n * 处理 fire 前后记账:firing Set(防重复 fire)、registry 更新(lastFiredAt/lastFireFailed)、\n * one-shot 清理(成功删除 / 失败保留 / cancel 保留)、持久化(saveOne)。\n *\n * 原代码位置:scheduler.ts fire() 方法的外层逻辑(firing Set / finally 块 / one-shot 处理)。\n */\nimport type { ScheduleRegistry } from './registry'\nimport type { SchedulePersistence } from './schedule-persistence'\nimport type { FireTarget, FireContext } from './fire-target'\nimport { FireTargetRegistry } from './fire-target'\nimport type { FireLogger } from './fire-logger'\nimport type { TerminationReason } from './scheduler'\nimport type { ScheduledTask } from './types'\n\nexport interface FireExecutorDeps {\n registry: ScheduleRegistry\n persistence: SchedulePersistence\n fireLogger: FireLogger\n fireTargets: FireTarget[]\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /**\n * RFC-433 D1:任务终结咽喉点(`SchedulerService.terminate`)。one-shot 成功后\n * 必须经此而非自行 `registry.remove`——后者不退订事件监听,会导致任务已删除\n * 却仍被事件触发(RFC-433 §1.2 M2)。\n */\n onTerminate: (taskId: string, reason: TerminationReason) => void\n /**\n * RFC-433 D2:fire ownership 准入。cron 路径由 tick 的 renew/isOwner 把关,\n * 但**事件路径不经过 tick**——监听独立于 ticker 存活,`TickLoop.stop()` 停不掉它。\n * 唯一的 fire 入口现场问一次,比在 ownership 状态机每个转换点维护 arm/disarm\n * 对称性更可靠(无状态判定,不会漏)。\n */\n isFireOwner: () => boolean\n}\n\nexport class FireExecutor {\n private readonly registry: FireTargetRegistry = new FireTargetRegistry()\n private readonly firing: Set<string> = new Set()\n private readonly deps: FireExecutorDeps\n\n constructor(deps: FireExecutorDeps) {\n this.deps = deps\n // 注册 handler——顺序决定优先级:subscriber > session > job\n for (const handler of deps.fireTargets) {\n this.registry.register(handler)\n }\n }\n\n /** 当前正在 fire 的 task id 集合(防重复 fire)。 */\n has(taskId: string): boolean {\n return this.firing.has(taskId)\n }\n\n /** 标记 task 正在 fire(同步,在 await 之前调用)。 */\n addFiring(taskId: string): void {\n this.firing.add(taskId)\n }\n\n /**\n * 执行一次 fire:路由到匹配的 FireTarget handler,处理结果记账。\n *\n * 调用方(tick / runNow)必须在调用前将 taskId 加入 firing Set(addFiring)。\n * finally 块负责移除,并调用 handler.cleanup() 清理 handler 侧运行态\n * (M1 修复①——拆分前该清理在旧 fire() 的 finally 里,拆分时被遗漏)。\n */\n async fire(task: ScheduledTask, scheduledFor?: number): Promise<void> {\n // ── 准入闸门(RFC-433 D2)──\n // fire() 是所有触发路径的唯一汇合点(tick / runNow / 事件 arm 回调),\n // 两道准入放这里才能覆盖全部路径:\n // · enabled —— 此前只在 registry.findDue 判,事件路径整条链零检查(M1)\n // · isFireOwner —— 此前只在 tick 判,事件监听独立于 ticker 存活(M7)\n // 上游的 findDue/tick 检查保留:它们负责\"要不要提出候选\"(筛选与触发 recovery),\n // 这里负责\"候选能不能真的执行\"(准入)。职责不同,不是重复。\n if (!task.enabled || !this.deps.isFireOwner()) {\n this.firing.delete(task.id)\n return\n }\n\n const ctx: FireContext = {\n scheduledFor,\n now: this.deps.now,\n logger: this.deps.logger,\n }\n\n let handler: FireTarget | undefined\n try {\n handler = this.registry.find(task)\n if (!handler) {\n this.deps.fireLogger.write(task.id, `no fire target handler for task (subscriber=${!!task.subscriber}, target=${task.target ?? 'default'})`)\n // RFC-435 D3:no-handler 的 **one-shot** 任务必须走终态——tick 在 fire 之前已\n // advance nextFireAt(tick-loop.ts),不终结会无限复燃:每天到点记一条日志、\n // 永不消失。标 lastFireFailed 保留(经咽喉点,用户可见可删),recurring 照旧\n // 每日日志(那是设计内:任务保留、不投递、不崩溃)。\n this.handleOneShotCompletion(task, { outcome: 'failed', error: 'no fire target handler (headless mode)' })\n return\n }\n\n const result = await handler.fire(task, ctx)\n\n // 成功记账(三态统一:session 成功不落盘,其余 saveOne)\n if (result.outcome === 'success' && !result.cancelled) {\n this.deps.registry.update(task.id, {\n lastFiredAt: this.deps.now(),\n lastFireFailed: false,\n lastFireError: undefined,\n })\n if (task.target !== 'session') {\n this.deps.persistence.saveOne(task)\n }\n }\n\n // one-shot 终态处理(原 scheduler.ts fire() finally 块语义,M1 修复②恢复统一保障):\n // success → 删除;cancelled → 标记 lastFireCancelled 保留;failed → 标记 lastFireFailed 保留。\n // session 分支不再早退——一次性 session 任务同样按此处理,否则成功后不删、\n // 失败不标,tick 已 advance 的 nextFireAt 会让它按 cron 点无限复燃。\n this.handleOneShotCompletion(task, result)\n } catch (err) {\n this.deps.logger?.error(`[schedules] fire failed for task ${task.id}:`, err)\n this.deps.fireLogger.write(task.id, `error: ${err instanceof Error ? err.message : String(err)}`)\n // handler 抛错时 one-shot 按失败终态处理(对齐 HEAD 版 catch→finally 行为)\n this.handleOneShotCompletion(task, { outcome: 'failed', error: err instanceof Error ? err.message : String(err) })\n } finally {\n handler?.cleanup?.(task.id)\n this.firing.delete(task.id)\n }\n }\n\n /**\n * one-shot 任务 fire 完成后的终态记账(fire() 内部调用,外部无调用方)。\n *\n * 成功 → 从 registry 和 store 删除。\n * cancel → 标记 lastFireCancelled,保留(不删除)。\n * 失败 → 标记 lastFireFailed + lastFireError,保留(review S6)。\n */\n private handleOneShotCompletion(task: ScheduledTask, result: { outcome: 'success' | 'failed'; cancelled?: boolean; error?: string }): void {\n if (task.recurring) return\n\n if (result.outcome === 'success') {\n // RFC-433 D1:经咽喉点终结——直接 registry.remove 不退订事件监听,\n // 会让已完成的 one-shot 事件任务在此后每次同类事件都重新投递(M2)。\n this.deps.onTerminate(task.id, 'one-shot-complete')\n } else if (result.cancelled) {\n this.deps.registry.update(task.id, {\n lastFireCancelled: true,\n lastFireError: undefined,\n })\n this.deps.persistence.saveOne(task)\n } else {\n this.deps.registry.update(task.id, {\n lastFireFailed: true,\n lastFireError: result.error,\n })\n this.deps.persistence.saveOne(task)\n }\n }\n}\n","/**\n * fire-logger.ts —— RFC-406 D4:Fire 日志写入/清理/读取。\n *\n * 从 scheduler.ts 拆出。每个 fire 结果写到 ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log,\n * 每任务保留最新 maxFireLogsPerTask 条。best-effort——错误静默吞(日志非关键)。\n */\nimport { writeFileSync, readFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nimport { OTTO_HOME, isShadowModeEnabled } from '@x-otto/env'\n\nimport type { ScheduleConfig } from './schedule-config'\n\nexport class FireLogger {\n constructor(private config: ScheduleConfig) {}\n\n /**\n * 写一行 fire log 到 ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log,然后清理超出\n * maxFireLogsPerTask 的旧日志。best-effort——错误静默吞。\n *\n * RFC-345:影子态零磁盘写入——fire log 是纯本机持久化产物,影子进程跳过。\n */\n write(taskId: string, status: string): void {\n if (isShadowModeEnabled()) return\n const logDir = resolve(OTTO_HOME, 'schedule-logs')\n const logFile = resolve(logDir, `${taskId}-${Date.now()}.log`)\n try {\n mkdirSync(logDir, { recursive: true })\n writeFileSync(logFile, `${new Date().toISOString()} ${status}\\n`, 'utf-8')\n this.prune(logDir, taskId)\n } catch {\n // best-effort logging\n }\n }\n\n /** 保留最新 maxFireLogsPerTask 条日志文件。 */\n private prune(logDir: string, taskId: string): void {\n try {\n const prefix = `${taskId}-`\n const files = readdirSync(logDir)\n .filter((f) => f.startsWith(prefix) && f.endsWith('.log'))\n .sort() // filenames embed epoch ms — lexical sort == chronological\n const excess = files.length - this.config.maxFireLogsPerTask\n if (excess <= 0) return\n for (const f of files.slice(0, excess)) {\n try {\n unlinkSync(resolve(logDir, f))\n } catch {\n // best-effort — a single stale/locked file must not block the rest\n }\n }\n } catch {\n // best-effort — directory listing failure is non-critical\n }\n }\n\n /**\n * 读取最近的 fire log 条目。返回最新 maxEntries 条,每条 { timestamp, status }。\n */\n read(taskId: string, maxEntries: number = 20): Array<{ timestamp: string; status: string }> {\n const logDir = resolve(OTTO_HOME, 'schedule-logs')\n const prefix = `${taskId}-`\n try {\n const files = readdirSync(logDir)\n .filter((f) => f.startsWith(prefix) && f.endsWith('.log'))\n .sort()\n .reverse() // newest first\n const entries: Array<{ timestamp: string; status: string }> = []\n for (let i = 0; i < files.length && entries.length < maxEntries; i++) {\n try {\n const content = readFileSync(resolve(logDir, files[i]!), 'utf-8').trim()\n const spaceIdx = content.indexOf(' ')\n const timestamp = spaceIdx > 0 ? content.slice(0, spaceIdx) : content\n const status = spaceIdx > 0 ? content.slice(spaceIdx + 1) : ''\n entries.push({ timestamp, status })\n } catch {\n // skip unreadable file\n }\n }\n return entries\n } catch {\n return []\n }\n }\n}\n","/**\n * schedule-config.ts —— RFC-406 D4:常量配置化。\n *\n * 之前散落在 scheduler.ts 顶部的硬编码常量,提取为可注入的配置接口。\n * 不同部署场景(单用户 vs 团队共享、高频 vs 低频)可调整参数,不需改源码。\n *\n * **不纳入本接口的常量**(刻意游离,非遗漏):\n * - `fire-ownership.ts` 的 `STALE_MS`(5 分钟)——本地锁过期阈值,有不变量约束\n * (必须超过 tick renew 间隔,否则锁在 renew 周期间被误判过期),调错会致\n * 多进程重复 fire。它是 fire-ownership 的领域常量,不是可调运维参数。\n * - `remote-fire-ownership.ts` 的 `HEARTBEAT_MS`(30s)/ `STALE_MS`(180s)——\n * 远程租约心跳与过期,有 1:6 比例约束(过期 > 6 倍心跳,防网络抖动误夺锁)。\n * 调错同样致多终端重复 fire 或锁饥饿。两者已通过模块 export 暴露\n * (`REMOTE_FIRE_OWNERSHIP_HEARTBEAT_MS` / `REMOTE_FIRE_OWNERSHIP_STALE_MS`),\n * 不进 ScheduleConfig 以免暗示可随意调。\n */\n\nexport interface ScheduleConfig {\n /** 最大任务数 */\n maxTasks: number\n /** recurring 任务过期阈值(超过此时间未触发则清除) */\n recurringEvictionMs: number\n /** recurring 任务 jitter 窗口 */\n recurringJitterWindowMs: number\n /** one-shot 任务 jitter 窗口 */\n oneShotJitterWindowMs: number\n /** 默认单次执行超时 */\n defaultMaxDurationMs: number\n /** 默认重试次数 */\n defaultMaxRetries: number\n /** ticker 间隔 */\n tickIntervalMs: number\n /** needs_input 硬上限倍数 */\n needsInputMaxExtensionFactor: number\n /** 每任务最大 fire log 条数 */\n maxFireLogsPerTask: number\n /** owner 从 store 重新同步的间隔(tick 数) */\n reloadIntervalTicks: number\n /** recovery 模式重试间隔 */\n recoveryRetryIntervalMs: number\n}\n\nexport const DEFAULT_SCHEDULE_CONFIG: ScheduleConfig = {\n maxTasks: 50,\n recurringEvictionMs: 7 * 24 * 60 * 60 * 1000, // 7 days\n recurringJitterWindowMs: 30 * 60 * 1000, // 30 minutes\n oneShotJitterWindowMs: 90 * 1000, // 90 seconds\n defaultMaxDurationMs: 300_000, // 5 minutes\n defaultMaxRetries: 0,\n tickIntervalMs: 1000,\n needsInputMaxExtensionFactor: 3,\n maxFireLogsPerTask: 20,\n reloadIntervalTicks: 30,\n recoveryRetryIntervalMs: 30_000,\n}\n\n// ---------------------------------------------------------------------------\n// Jitter — RFC-406 终局自检:收敛 scheduler.ts / tick-loop.ts 两处重复的 djb2 hash。\n// ---------------------------------------------------------------------------\n\n/**\n * 确定性 per-task jitter(djb2 hash of taskId),跨重启可复现。\n * recurring 任务用 30 分钟窗口(分散周期负载);one-shot 用 90 秒窗口(RFC-087 §D3)。\n */\nexport function applyJitter(config: ScheduleConfig, taskId: string, baseTime: Date, recurring: boolean): number {\n let hash = 0\n for (let i = 0; i < taskId.length; i++) {\n hash = ((hash << 5) - hash) + taskId.charCodeAt(i)\n hash |= 0\n }\n const window = recurring ? config.recurringJitterWindowMs : config.oneShotJitterWindowMs\n const jitter = Math.abs(hash) % window\n return baseTime.getTime() + jitter\n}\n","/**\n * tick-loop.ts —— RFC-406 D4:Tick 循环。\n *\n * 从 scheduler.ts tick() 拆出。1 秒间隔心跳:renew ownership → 检查 owner →\n * reloadFromStore(每 N tick)→ 过期清理 → fire due tasks。\n *\n * 原代码位置:scheduler.ts tick() + reloadFromStore() + eviction 逻辑。\n */\nimport { parseCron } from './cron-parser'\nimport { applyJitter } from './schedule-config'\nimport type { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { SchedulePersistence } from './schedule-persistence'\nimport type { FireOwnership } from './fire-ownership'\nimport type { FireExecutor } from './fire-executor'\nimport type { ScheduleConfig } from './schedule-config'\nimport type { TerminationReason } from './scheduler'\nimport type { ScheduledTask } from './types'\n\nexport interface TickLoopDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n persistence: SchedulePersistence\n ownership: FireOwnership\n fireExecutor: FireExecutor\n config: ScheduleConfig\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /**\n * ownership 丢失时的回调(SchedulerService 进入 recovery mode)。\n */\n onOwnershipLost: () => void\n /**\n * RFC-433 D1:任务终结咽喉点(滞留驱逐用)。不可自行 `registry.remove`——\n * 那样不退订事件监听。\n */\n onTerminate: (taskId: string, reason: TerminationReason) => void\n /**\n * RFC-433 D1:只从内存移除、保留 store 快照(差量同步用——store 里已经没有它了,\n * 是别的进程删的,再发一次 delete 是多余 IO)。同样必须退订。\n */\n onEvictFromMemory: (taskId: string) => void\n}\n\nexport class TickLoop {\n private readonly deps: TickLoopDeps\n private ticker: ReturnType<typeof setInterval> | null = null\n private tickInProgress = false\n private tickCount = 0\n\n constructor(deps: TickLoopDeps) {\n this.deps = deps\n }\n\n /** 启动 tick 循环。 */\n start(): void {\n if (this.ticker) return\n this.ticker = setInterval(() => {\n void this.tick()\n }, this.deps.config.tickIntervalMs)\n this.ticker.unref()\n }\n\n /** 停止 tick 循环。 */\n stop(): void {\n if (this.ticker) {\n clearInterval(this.ticker)\n this.ticker = null\n }\n }\n\n /**\n * 手动触发一次 tick(测试用)。\n *\n * **同时供测试与调度器复用**:`lifecycle-regression.test.ts` 用它精确控制\"8 天后\n * 跑一次 tick\"的场景(M3 系列回归),而 `SchedulerService` 也保留同名方法用于\n * 相同目的——两者都走同一个 `tick()` 内部方法,避免平行实现漂移。\n */\n async tickOnce(): Promise<void> {\n await this.tick()\n }\n\n // ── Internal ──\n\n private async tick(): Promise<void> {\n if (this.tickInProgress) return\n this.tickInProgress = true\n\n try {\n // A3: heartbeat the ownership lock\n await this.deps.ownership.renew()\n\n // S4: if renew() detected a takeover, stop firing + enter recovery\n if (!this.deps.ownership.isOwner()) {\n this.stop()\n this.deps.onOwnershipLost()\n return\n }\n\n // S5: periodically re-sync from store\n this.tickCount++\n if (this.tickCount % this.deps.config.reloadIntervalTicks === 0) {\n await this.reloadFromStore()\n }\n\n const now = this.deps.now()\n\n // Evict recurring tasks whose next fire is >7 days stale.\n //\n // RFC-433 D3:滞留判据是 `nextFireAt`,**只对 cron 任务有语义**。事件驱动任务的\n // nextFireAt 是 add() 时的 cron 占位符产物且永不更新(findDue 排除它们,只有\n // fire 路径才 advance),所以每个 recurring 事件任务的 nextFireAt 原地不动,\n // 第 7 天 100% 越过阈值被误删——任务从 UI 消失,用户既看不见也删不掉。\n //\n // TODO(RFC-434): 事件任务因此失去了这道\"最终安全阀\"——永远等不到事件的任务会\n // 永久占用 maxTasks 配额。正确的清理判据是 createdAt/lastFiredAt 而非 nextFireAt,\n // 属于新规则设计,随 trigger/delivery 可辨识联合一起做。\n for (const task of this.deps.registry.list()) {\n if (task.eventTrigger) continue\n if (task.recurring && now - task.nextFireAt > this.deps.config.recurringEvictionMs) {\n this.deps.onTerminate(task.id, 'stale-evicted')\n }\n }\n\n // Fire due tasks\n const due = this.deps.registry.findDue(now)\n for (const task of due) {\n if (this.deps.fireExecutor.has(task.id)) continue\n\n // S6: failed/canceled one-shot guard — no auto-refire\n if (!task.recurring && (task.lastFireFailed || task.lastFireCancelled)) continue\n\n // Advance nextFireAt before dispatching (MF-1); persist\n const scheduledFor = task.nextFireAt\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n const jittered = applyJitter(this.deps.config, task.id, nextBase, task.recurring)\n this.deps.registry.update(task.id, { nextFireAt: jittered })\n this.deps.persistence.saveOne(task)\n\n this.deps.fireExecutor.addFiring(task.id)\n void this.deps.fireExecutor.fire(task, scheduledFor)\n }\n } finally {\n this.tickInProgress = false\n }\n }\n\n private async reloadFromStore(): Promise<void> {\n let tasks: ScheduledTask[]\n try {\n tasks = await this.deps.store.loadAllTasks()\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to reload schedules from store:', err)\n return\n }\n\n const storeIds = new Set(tasks.map((t) => t.id))\n\n for (const task of tasks) {\n if (this.deps.fireExecutor.has(task.id)) continue\n if (!task.recurring && task.lastFiredAt != null && !this.deps.registry.get(task.id)) {\n void this.deps.persistence.deleteOne(task.id).catch((err) => {\n this.deps.logger?.error(`[schedules] Failed to delete zombie one-shot ${task.id}:`, err)\n })\n continue\n }\n const existing = this.deps.registry.get(task.id)\n if (!existing) {\n try { this.deps.registry.add(task) } catch { /* raced */ }\n } else {\n existing.enabled = task.enabled\n existing.name = task.name\n existing.prompt = task.prompt\n existing.cronExpression = task.cronExpression\n existing.maxDurationMs = task.maxDurationMs\n existing.maxRetries = task.maxRetries\n }\n }\n\n for (const task of this.deps.registry.list()) {\n if (task.target === 'session') continue\n if (!storeIds.has(task.id) && !this.deps.fireExecutor.has(task.id)) {\n // RFC-433 D1:只从内存移除(store 里已经没有它了),但仍须退订。\n this.deps.onEvictFromMemory(task.id)\n }\n }\n }\n}\n","/**\n * schedule-persistence.ts —— RFC-406 终局自检:持久化咽喉点收敛。\n *\n * 重构后 saveOne/deleteOne/flushAll 的 session-local 跳过 + 错误吞逻辑在 scheduler.ts /\n * fire-executor.ts / tick-loop.ts 三处重复。收敛为单一真源——所有写盘调用共享此对象,\n * session-local 任务永不落盘(RFC-357 D2)这条防线只有一个判断点。\n */\nimport type { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { ScheduledTask } from './types'\n\nexport interface SchedulePersistenceDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\nexport class SchedulePersistence {\n constructor(private deps: SchedulePersistenceDeps) {}\n\n /**\n * 持久化单个任务(fire-and-forget)。RFC-357 D2:session-local 任务永不落盘——\n * 这是唯一判断点,所有调用方共享此拦截。\n */\n saveOne(task: ScheduledTask): void {\n if (task.target === 'session') return\n void this.deps.store.saveTask(task).catch((err) => {\n this.deps.logger?.error(`[schedules] Failed to save schedule ${task.id}:`, err)\n })\n }\n\n /**\n * 删除单个任务快照(awaited——必须确认删除以防 store reload 时复活为僵尸)。\n * session-local 任务从不落盘,删除时直接视为成功(不发起多余 IO)。\n */\n async deleteOne(id: string): Promise<boolean> {\n const task = this.deps.registry.get(id)\n if (task?.target === 'session') return true\n try {\n return await this.deps.store.deleteTask(id)\n } catch (err) {\n this.deps.logger?.error(`[schedules] Failed to delete schedule ${id}:`, err)\n return false\n }\n }\n\n /**\n * 把所有内存任务写盘(stop 时调用;bounded,无 diff-delete)。\n * RFC-357 D2:跳过 session-local 任务——否则进程正常退出时会把它们写进 store,\n * 下次 start() 从 store 加载后在新进程的新 sessionRef 上\"复活\",击穿\"不落盘\"防线。\n */\n async flushAll(tasks: ScheduledTask[]): Promise<void> {\n try {\n for (const task of tasks) {\n if (task.target === 'session') continue\n await this.deps.store.saveTask(task)\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to flush schedules:', err)\n }\n }\n}\n","// schedule/src/scheduler.ts — RFC-406: Schedule lifecycle orchestrator.\n// Lifecycle: start→tick→fire→stop. Delegates to TickLoop + FireExecutor + FireLogger.\n// Fire logs → ${OTTO_HOME}/schedule-logs/. Clock: Date.now() by default, injectable for testing.\n\nimport { parseCron } from './cron-parser'\nimport { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { FireOwnership } from './fire-ownership'\nimport { FireExecutor } from './fire-executor'\nimport { FireLogger } from './fire-logger'\nimport { TickLoop } from './tick-loop'\nimport { SchedulePersistence } from './schedule-persistence'\nimport { DEFAULT_SCHEDULE_CONFIG, applyJitter, type ScheduleConfig } from './schedule-config'\nimport type { FireTarget } from './fire-target'\nimport { type EventDrivenTrigger } from './trigger'\nimport type { CancelResult, ScheduledTask, ScheduledTaskInput } from './types'\n\n// ---------------------------------------------------------------------------\n// Task termination\n// ---------------------------------------------------------------------------\n\n/**\n * RFC-433 D1:任务终结的来源。仅用于诊断/日志语义——终结动作本身对三者一致\n * (退订 → 删快照 → 删 registry),区分来源是为了让调用点自我说明。\n */\nexport type TerminationReason =\n /** 用户经 `/schedule remove`、面板或 `schedule_delete` 工具删除。 */\n | 'user-removed'\n /** one-shot 任务成功执行完毕(FireExecutor 终态处理)。 */\n | 'one-shot-complete'\n /** recurring cron 任务 7 天未触发,被 TickLoop 判为滞留清除。 */\n | 'stale-evicted'\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface SchedulerDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n ownership: FireOwnership\n /**\n * FireTarget handler 列表。内置 handler(job/session/subscriber)由消费方构造并传入,\n * SchedulerService 不自动注册——消费方完全控制 handler 集合和优先级。\n *\n * 如果传入空数组,fire() 会找不到 handler 并记日志跳过——不会崩溃。\n */\n fireTargets: FireTarget[]\n /**\n * RFC-423:事件驱动 Trigger 列表(如 InProcessEventTrigger)。缺省空数组——只有 cron\n * 触发(cron 任务由 TickLoop 轮询驱动,无需消费方传入)。消费方传入事件 trigger\n * 即启用事件触发能力。\n */\n triggers?: EventDrivenTrigger[]\n /** 可选配置覆盖(常量配置化,RFC-406 D4) */\n config?: Partial<ScheduleConfig>\n now?: () => number\n /**\n * `debug` 可选——生产装配(`interactive-schedule.ts`)注入的 logger 只有 `error`;\n * 有 `debug` 的实现能拿到任务终结的来源(user-removed / one-shot-complete /\n * stale-evicted),排查\"任务怎么没了\"时不必猜。\n */\n logger?: {\n error: (msg: string | Error, ...args: unknown[]) => void\n debug?: (msg: string, ...args: unknown[]) => void\n }\n}\n\n// ---------------------------------------------------------------------------\n// SchedulerService — lifecycle orchestrator\n// ---------------------------------------------------------------------------\n\nexport class SchedulerService {\n private readonly deps: SchedulerDeps\n private readonly registry: ScheduleRegistry\n private readonly persistence: SchedulePersistence\n private readonly fireExecutor: FireExecutor\n private readonly fireLogger: FireLogger\n private readonly tickLoop: TickLoop\n private readonly config: ScheduleConfig\n private recoveryTimer: ReturnType<typeof setInterval> | null = null\n /** RFC-435 D4:从未当过 owner 的非 owner 进程的 acquire 重试循环(TUI 释放锁后本进程可接管)。 */\n private ensureOwnershipTimer: ReturnType<typeof setInterval> | null = null\n private running = false\n /** RFC-423:事件驱动 trigger 列表(消费方传入)。 */\n private readonly triggers: EventDrivenTrigger[]\n /** RFC-423:已 arm 的事件任务 → 退订函数(remove/stop 时调用,防监听泄漏)。 */\n private readonly armedTriggers = new Map<string, () => void>()\n\n constructor(deps: SchedulerDeps) {\n this.deps = deps\n this.registry = deps.registry\n this.config = { ...DEFAULT_SCHEDULE_CONFIG, ...deps.config }\n this.fireLogger = new FireLogger(this.config)\n // RFC-406 终局自检:持久化咽喉点单一真源,三个消费者(scheduler/executor/tick)共享。\n this.persistence = new SchedulePersistence({\n registry: deps.registry,\n store: deps.store,\n logger: deps.logger,\n })\n this.fireExecutor = new FireExecutor({\n registry: deps.registry,\n persistence: this.persistence,\n fireLogger: this.fireLogger,\n fireTargets: deps.fireTargets,\n now: deps.now ?? Date.now,\n logger: deps.logger,\n // RFC-433 D1:one-shot 终结经咽喉点,而非自行 registry.remove(会漏退订)。\n onTerminate: (taskId, reason) => { this.terminate(taskId, reason) },\n // RFC-433 D2:fire 准入的 ownership 一侧。事件路径不经过 tick,\n // tick 里的 ownership 检查管不到它,必须在唯一的 fire 入口独立把关。\n isFireOwner: () => this.deps.ownership.isOwner(),\n })\n this.tickLoop = new TickLoop({\n registry: deps.registry,\n store: deps.store,\n persistence: this.persistence,\n ownership: deps.ownership,\n fireExecutor: this.fireExecutor,\n config: this.config,\n now: deps.now ?? Date.now,\n logger: deps.logger,\n onOwnershipLost: () => this.startRecovery(),\n // RFC-433 D1:eviction 与差量同步两条删除路径同样经咽喉点。\n onTerminate: (taskId, reason) => { this.terminate(taskId, reason) },\n onEvictFromMemory: (taskId) => { this.evictFromMemory(taskId) },\n })\n this.triggers = deps.triggers ?? []\n }\n\n // ── Public: ownership status ──\n\n isFireOwner(): boolean {\n return this.deps.ownership.isOwner()\n }\n\n fireOwnerFailureReason(): 'occupied' | 'network-error' | undefined {\n return this.deps.ownership.lastFailureReason?.()\n }\n\n // ── Lifecycle ──\n\n /** Load persisted tasks and start the ticker (if fire owner). */\n async start(): Promise<void> {\n if (this.running) return\n\n const isOwner = await this.deps.ownership.tryAcquire()\n if (!isOwner) {\n try {\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to load schedules (read-only mode):', err)\n }\n // RFC-435 D4:非 owner 也必须周期尝试 acquire——否则另一进程(TUI)释放锁后\n // 本进程(daemon)永远接管不了,凌晨任务静默不跑,直到进程重启。\n // (此前 start() 非 owner 分支加载一次就休眠;startRecovery 只在\"曾是 owner 后\n // 丢失\"时触发,对\"从未 owner\"的 daemon 无效。)\n this.startEnsureOwnership()\n return\n }\n\n try {\n const now = this.deps.now?.() ?? Date.now()\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n if (task.recurring && task.nextFireAt < now) {\n try {\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n task.nextFireAt = applyJitter(this.config, task.id, nextBase, true)\n } catch {\n /* keep stored nextFireAt if cron no longer parses */\n }\n }\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to load schedules:', err)\n }\n\n // RFC-423:为加载的事件驱动任务建立监听(cron 任务由 tickLoop 轮询,无需 arm)。\n for (const task of this.registry.list()) {\n this.armIfEventDriven(task)\n }\n\n this.running = true\n this.tickLoop.start()\n }\n\n // ── Task termination (RFC-433 D1) ──\n\n /**\n * 任务终结的**唯一入口**(RFC-433 D1 / R2)。三条删除路径——用户 `remove()`、\n * one-shot 完成、7 天滞留驱逐——全部经此,否则事件监听会泄漏:任务从 registry\n * 消失后,`armedTriggers` 里的退订函数仍在,而 arm 回调闭包捕获的是 **task 对象\n * 引用本身**(见 `armIfEventDriven`),registry 里有没有它根本不影响 fire。\n *\n * **三步顺序不可调换(R2b 不变量)**:`persistence.deleteOne()` 内部要\n * `registry.get(id)` 判 session-local(`schedule-persistence.ts:37`)——registry\n * 先删会让它读不到 task、判不出 session-local,对本就不落盘的任务白发一次 store IO。\n */\n private terminate(taskId: string, reason: TerminationReason): boolean {\n // 未知 id 不产生任何副作用(保持 remove() 的既有语义:不存在则 false,不发 store IO)。\n // disarm 仍先做一次——它对未 arm 的 id 是 no-op,但能兜住\"已泄漏\"的历史脏状态。\n this.disarmIfArmed(taskId) // ① 退订事件监听\n if (!this.registry.has(taskId)) return false\n void this.persistence.deleteOne(taskId) // ② 删快照(须先于 ③,见上)\n const removed = this.registry.remove(taskId) // ③ 从内存 registry 移除\n this.deps.logger?.debug?.(`[schedules] terminated ${taskId} (${reason})`)\n return removed\n }\n\n /**\n * 只从内存移除、**保留** store 快照——`reloadFromStore` 的差量同步用(store 里\n * 本来就已经没有它了,是别的进程删的;再发一次 delete 是多余 IO)。\n * 同样必须退订,理由同 `terminate()`。\n */\n private evictFromMemory(taskId: string): boolean {\n this.disarmIfArmed(taskId)\n return this.registry.remove(taskId)\n }\n\n /** Clear ticker, release ownership, and persist current state. */\n async stop(): Promise<void> {\n // RFC-435 D4:先清 acquire 重试循环——非 owner + timer 在跑时若被首行守卫短路,\n // timer 会泄漏(进程常驻则每 30s 空转一次 acquire)。\n this.stopEnsureOwnership()\n if (!this.running && !this.deps.ownership.isOwner()) return\n\n this.stopRecovery()\n this.tickLoop.stop()\n this.disarmAll()\n\n this.running = false\n await this.deps.ownership.release()\n await this.persistence.flushAll(this.registry.list())\n }\n\n // ── Recovery ──\n\n private startRecovery(): void {\n if (this.recoveryTimer) return\n this.deps.logger?.error('[schedules] Lost fire ownership — entering recovery mode')\n this.recoveryTimer = setInterval(() => {\n void this.recoveryAttempt()\n }, this.config.recoveryRetryIntervalMs)\n this.recoveryTimer.unref()\n }\n\n private async recoveryAttempt(): Promise<void> {\n try {\n const acquired = await this.deps.ownership.tryAcquire()\n if (acquired && this.deps.ownership.isOwner()) {\n this.stopRecovery()\n this.running = true\n // RFC-433 D3b:重获 ownership 后补 arm——与 start() 的 owner 分支对齐。\n // 不补的话事件任务在一次锁抖动后**永久假死**:cron 任务随 ticker 自动恢复,\n // 事件任务的监听却可能从未建立(非 owner 期间 start() 只加载不 arm)。\n // armIfEventDriven 内部幂等(先 disarm 再 arm),重复调用安全。\n for (const task of this.registry.list()) this.armIfEventDriven(task)\n this.tickLoop.start()\n this.deps.logger?.error('[schedules] Re-acquired fire ownership — recovery complete')\n }\n } catch {\n // tryAcquire failures during recovery are expected — suppress.\n }\n }\n\n private stopRecovery(): void {\n if (this.recoveryTimer) {\n clearInterval(this.recoveryTimer)\n this.recoveryTimer = null\n }\n }\n\n // ── RFC-435 D4: never-owner acquire retry ──\n\n /**\n * 非 owner 进程的低频 acquire 重试。TUI 先开持锁 → daemon 后起(acquire 失败,\n * 本循环待机)→ TUI 关闭释放锁 → 本循环在下一次 tick 接管。锁的原子性\n * (O_EXCL / 服务端 CAS)保证任一时刻只有一个 owner——重试不会造成双主。\n */\n private startEnsureOwnership(): void {\n if (this.ensureOwnershipTimer) return\n this.ensureOwnershipTimer = setInterval(() => {\n void this.ensureOwnershipAttempt()\n }, this.config.recoveryRetryIntervalMs)\n this.ensureOwnershipTimer.unref()\n }\n\n private async ensureOwnershipAttempt(): Promise<void> {\n try {\n const acquired = await this.deps.ownership.tryAcquire()\n if (acquired && this.deps.ownership.isOwner()) {\n this.stopEnsureOwnership()\n // 与 start() owner 分支对齐:重新 reload(待机期间可能有新任务写入 store)+\n // arm 事件任务 + 启动 ticker。\n try {\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to reload schedules after acquiring ownership:', err)\n }\n for (const task of this.registry.list()) this.armIfEventDriven(task)\n this.running = true\n this.tickLoop.start()\n this.deps.logger?.error('[schedules] Acquired fire ownership (was waiting for release)')\n }\n } catch {\n // tryAcquire failures during wait are expected — suppress.\n }\n }\n\n private stopEnsureOwnership(): void {\n if (this.ensureOwnershipTimer) {\n clearInterval(this.ensureOwnershipTimer)\n this.ensureOwnershipTimer = null\n }\n }\n\n // ── Public CRUD ──\n\n add(input: ScheduledTaskInput): ScheduledTask {\n if (this.registry.count() >= this.config.maxTasks) {\n throw new Error(`Schedule limit reached (max ${this.config.maxTasks}). Remove some tasks before adding more.`)\n }\n\n const wantsSession = input.target === 'session' && !input.subscriber\n\n if (wantsSession && !this.deps.ownership.isOwner()) {\n throw new Error(this.describeSessionOwnershipRefusal())\n }\n\n // RFC-423 D4 闸门2:事件驱动任务默认拒绝 job 投递(会产生新 job → 高死循环风险)。\n // 事件触发只放行 subscriber/session 目标(不产生新 job)。这也匹配\"注入对话继续\"诉求。\n if (input.eventTrigger && !input.subscriber && input.target !== 'session') {\n throw new Error(\n 'Event-triggered tasks must deliver to a session (target:\"session\") or a plugin subscriber, '\n + 'not an isolated job — job delivery from an event trigger risks a self-firing loop (RFC-423 D4).',\n )\n }\n\n const id = crypto.randomUUID().slice(0, 8)\n const now = this.deps.now?.() ?? Date.now()\n const nextBase = parseCron(input.cronExpression, new Date(now + 60_000))\n const nextFireAt = applyJitter(this.config, id, nextBase, input.recurring)\n\n const task: ScheduledTask = {\n id,\n name: input.name,\n prompt: input.prompt,\n cronExpression: input.cronExpression,\n enabled: true,\n recurring: input.recurring,\n origin: input.origin,\n createdAt: now,\n nextFireAt,\n maxDurationMs: input.maxDurationMs ?? this.config.defaultMaxDurationMs,\n maxRetries: input.maxRetries ?? this.config.defaultMaxRetries,\n ...(input.subscriber ? { subscriber: input.subscriber } : {}),\n ...(wantsSession ? { target: 'session' as const, boundSessionId: this.getSessionIdForTask() } : {}),\n ...(input.eventTrigger ? { eventTrigger: input.eventTrigger } : {}),\n ...(input.modelId ? { modelId: input.modelId } : {}),\n ...(input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {}),\n }\n\n this.registry.add(task)\n this.persistence.saveOne(task)\n this.armIfEventDriven(task)\n return task\n }\n\n remove(id: string): boolean {\n return this.terminate(id, 'user-removed')\n }\n\n update(\n id: string,\n patch: Partial<Pick<ScheduledTask, 'name' | 'prompt' | 'cronExpression' | 'enabled'>>,\n ): boolean {\n const task = this.registry.get(id)\n if (!task) return false\n\n if (patch.name !== undefined) task.name = patch.name\n if (patch.prompt !== undefined) task.prompt = patch.prompt\n if (patch.enabled !== undefined) task.enabled = patch.enabled\n if (patch.cronExpression !== undefined) {\n task.cronExpression = patch.cronExpression\n const now = this.deps.now?.() ?? Date.now()\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n task.nextFireAt = applyJitter(this.config, task.id, nextBase, task.recurring)\n }\n\n this.persistence.saveOne(task)\n return true\n }\n\n list(): ScheduledTask[] {\n return this.registry.list()\n }\n\n getRunningTaskIds(): Set<string> {\n // 委托给实现了 getRunningTaskIds() 的 handler(job 路径)\n for (const handler of this.deps.fireTargets) {\n if (handler.getRunningTaskIds) {\n return handler.getRunningTaskIds()\n }\n }\n return new Set()\n }\n\n readFireLogs(taskId: string, maxEntries: number = 20): Array<{ timestamp: string; status: string }> {\n return this.fireLogger.read(taskId, maxEntries)\n }\n\n runNow(id: string): void {\n const task = this.registry.get(id)\n if (!task) {\n throw new Error(`Schedule task \"${id}\" not found`)\n }\n if (!task.enabled) {\n throw new Error(`Schedule task \"${id}\" is disabled`)\n }\n // RFC-435 D1:非 owner 进程(serve/daemon 与交互会话并存时的只读加载方)不能 fire——\n // 否则 fireExecutor 准入静默吞掉,用户以为执行了。显式抛错(面板/toast 可提示\"另一\n // 进程持锁\")。D4 的非 owner acquire 重试只负责\"锁释放后接管\",不代表 runNow 可执行。\n if (!this.deps.ownership.isOwner()) {\n throw new Error(`Schedule task \"${id}\" cannot be run now: another process holds the fire ownership`)\n }\n if (this.fireExecutor.has(task.id)) {\n throw new Error(`Schedule task \"${id}\" is already firing`)\n }\n this.fireExecutor.addFiring(task.id)\n void this.fireExecutor.fire(task)\n }\n\n cancel(taskId: string): CancelResult {\n const task = this.registry.get(taskId)\n if (!task) return { ok: false, reason: 'task_not_found' }\n\n // 委托给实现了 requestCancel() 的 handler(job 路径)\n for (const handler of this.deps.fireTargets) {\n if (handler.requestCancel) {\n return handler.requestCancel(taskId)\n }\n }\n return { ok: false, reason: 'not_running' }\n }\n\n // ── RFC-423: event-trigger arm/disarm ──\n\n /**\n * 若任务是事件驱动(eventTrigger 存在),用匹配的 EventDrivenTrigger arm 监听。\n * fire 回调复用既有 fireExecutor 路径——Trigger 只决定\"何时\",FireTarget 决定\"投哪\"。\n *\n * 死循环闸门2(D4):事件驱动任务默认拒绝 JobFireTarget 投递——只有 subscriber/session\n * 目标(不产生新 job)才放行。配 job 会产生新 job → 高死循环风险,在此拒绝而非静默。\n */\n private armIfEventDriven(task: ScheduledTask): void {\n if (!task.eventTrigger) return\n const trigger = this.triggers.find((t) => t.canHandle(task))\n if (!trigger) {\n this.deps.logger?.error(\n `[schedules] event-trigger task ${task.id} has no matching trigger (source=${task.eventTrigger.source}) — will never fire`,\n )\n return\n }\n // 已 arm 的先退订(幂等,防重复 arm 泄漏)。\n this.disarmIfArmed(task.id)\n const disarm = trigger.arm(task, (t) => {\n if (this.fireExecutor.has(t.id)) return\n this.fireExecutor.addFiring(t.id)\n void this.fireExecutor.fire(t)\n })\n this.armedTriggers.set(task.id, disarm)\n }\n\n private disarmIfArmed(taskId: string): void {\n const disarm = this.armedTriggers.get(taskId)\n if (disarm) {\n disarm()\n this.armedTriggers.delete(taskId)\n }\n }\n\n private disarmAll(): void {\n for (const disarm of this.armedTriggers.values()) disarm()\n this.armedTriggers.clear()\n }\n\n /**\n * 非 owner 进程拒绝创建 session-local 任务时的诊断文案(RFC-357 D2 R3 的执行面)。\n *\n * **拒绝本身不放宽**——它防的是\"任务出现在列表里却永远不触发\"的静默失效:非 owner\n * 进程不跑 ticker,任务又 durable:false 不落盘,owner 进程 loadAllTasks 也看不见它。\n * 本方法只解决\"用户知道被拒了,但不知道该怎么办\":补上**谁持有**(本地实现可从锁\n * 文件解出 pid/host)与**多久能好**(RFC-435 D4 的非 owner acquire 重试会在锁释放后\n * 自动接管,无需重启进程)。\n *\n * 三态退化:远程实现无本地 pid 概念 → 无主体文案;`network-error` 时不能说\"另一终端\n * 持有\"(可能压根没连上服务端,让用户去找一个不存在的第二终端是更糟的误导)——这条\n * 三态区分沿用 `handlers/schedule.ts:105-109` 既有 readOnlyHint 范式,不另造一套判据。\n */\n private describeSessionOwnershipRefusal(): string {\n const base = 'Cannot create a session-local scheduled task: this terminal does not hold fire ownership.'\n\n if (this.deps.ownership.lastFailureReason?.() === 'network-error') {\n return `${base} Could not reach the remote schedule service — check network/persistenced connectivity, then retry.`\n }\n\n const owner = this.deps.ownership.describeOwner?.()\n const holder = owner?.pid !== undefined\n ? ` It is currently held by another otto process (pid ${owner.pid}${owner.host !== undefined ? ` on ${owner.host}` : ''}).`\n : ' It is currently held by another otto process for this workspace.'\n\n return `${base}${holder}`\n + ' Create the task in that terminal instead, or close it — this process retries acquiring'\n + ` ownership every ${Math.round(this.config.recoveryRetryIntervalMs / 1000)}s and will take over automatically once the lease is released (no restart needed).`\n }\n\n // ── Internal: session id for task creation ──\n\n /**\n * session-local 任务创建时绑定的 sessionId。\n * RFC-406 D6:从 SessionFireTarget handler 的 getSessionId() 端口读取(旧 SchedulerDeps.getSessionId\n * 语义迁入此 handler)。add() 用它来设置 boundSessionId。\n *\n * 找不到 SessionFireTarget(消费方未装配 session 投递)时回退空字符串——此时 add() 的\n * wantsSession 分支不会命中(session 任务无 handler 可 fire),boundSessionId 也无意义。\n */\n private getSessionIdForTask(): string {\n for (const handler of this.deps.fireTargets) {\n if (handler.getSessionId) {\n return handler.getSessionId()\n }\n }\n return ''\n }\n}\n","// schedule/src/schedule-store.ts — Snapshot-per-id storage via Persistence<ScheduledTask>.\n// RFC-169 D1: partitioned by workspaceKey. Fail-open: read errors → [].\nimport { createPersistence, type Persistence, type Snapshot } from '@x-otto/persistence'\nimport { OTTO_HOME, isShadowModeEnabled } from '@x-otto/env'\nimport { resolve } from 'node:path'\n\nimport type { ScheduledTask } from './types'\n\nconst SNAPSHOT_VERSION = 1\n\n/** Per-workspace schedules directory: `${OTTO_HOME}/schedules/<workspaceKey>/`. */\nfunction schedulesDir(workspaceKey: string): string {\n return resolve(OTTO_HOME, 'schedules', workspaceKey)\n}\n\nexport interface ScheduleStore {\n saveTask(task: ScheduledTask): Promise<void>\n loadAllTasks(): Promise<ScheduledTask[]>\n deleteTask(id: string): Promise<boolean>\n}\n\nexport interface ScheduleStoreOptions {\n persistence?: Persistence<ScheduledTask>\n /** Logger (injected, replaces bare console.error). */\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\n/**\n * Create a ScheduleStore backed by FilePersistence in ${OTTO_HOME}/schedules/<workspaceKey>/.\n *\n * `workspaceKey` is required (RFC-169 D1) — every call site must explicitly state which\n * workspace's schedules it wants, so a caller can never silently fall back to a shared\n * global path. Remote mode (RemoteScheduleStore, RFC-169 D3) is a separate implementation\n * selected by the caller based on `--session-url`, not by this factory.\n */\nexport function createScheduleStore(workspaceKey: string, options: ScheduleStoreOptions = {}): ScheduleStore {\n // RFC-345:影子模式(OTTO_SHADOW)下默认持久化切到 memory 后端——`/schedule add`\n // 等交互路径只写内存、零磁盘落盘,符合影子态「写静默吞」(§D3)语义。调用方显式\n // 注入 options.persistence 时不受影子态控制。\n //\n // 已知折衷:影子态下 `/schedule list` 只含本进程内存任务,既有磁盘任务不可见。\n // 读穿透叠加 merge 的复杂度不值得为一个短暂影子会话引入,故不做。\n const store =\n options.persistence ??\n (isShadowModeEnabled()\n ? createPersistence<ScheduledTask>({ type: 'memory' })\n : createPersistence<ScheduledTask>({ type: 'file', baseDir: schedulesDir(workspaceKey) }))\n const logger = options.logger\n\n return {\n async saveTask(task: ScheduledTask): Promise<void> {\n const snapshot: Snapshot<ScheduledTask> = {\n version: SNAPSHOT_VERSION,\n id: task.id,\n data: task,\n metadata: {\n createdAt: task.createdAt,\n updatedAt: Date.now(),\n tags: ['schedule'],\n },\n }\n await store.save(snapshot)\n },\n\n async loadAllTasks(): Promise<ScheduledTask[]> {\n let ids: string[]\n try {\n ids = await store.list()\n } catch (err) {\n logger?.error('[schedules] Failed to list schedule snapshots:', err)\n return []\n }\n\n const tasks: ScheduledTask[] = []\n for (const id of ids) {\n try {\n const snapshot = await store.load(id)\n if (snapshot) {\n tasks.push(snapshot.data)\n }\n } catch (err) {\n logger?.error(`[schedules] Failed to load schedule \"${id}\":`, err)\n // skip corrupted record, continue loading rest\n }\n }\n\n tasks.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return tasks\n },\n\n async deleteTask(id: string): Promise<boolean> {\n try {\n return await store.delete(id)\n } catch (err) {\n logger?.error(`[schedules] Failed to delete schedule \"${id}\":`, err)\n return false\n }\n },\n }\n}\n","/**\n * remote-schedule-store.ts —— RFC-169 D3: 远程模式的 ScheduleStore 实现。\n *\n * `extends RemotePersistence<ScheduledTask>`(@x-otto/persistence 既有抽象基类,已有\n * RemoteSessionPersistence/PanelState 两个先例)——复用 wsKey 路径分区、鉴权 header 注入、\n * 超时/连接失败错误文案,不重新实现 HTTP 客户端。`ScheduleStore` 接口形状(saveTask/\n * loadAllTasks/deleteTask)与本地 `createScheduleStore` 一致,`setupScheduleService` 可\n * 按 remoteSessionConfigured 无痛切换两种实现(RFC-169 §4 重要事项规则 6)。\n */\nimport { RemotePersistence } from '@x-otto/persistence'\nimport type { RemotePersistenceOptions } from '@x-otto/persistence'\n\nimport type { ScheduleStore } from './schedule-store'\nimport type { ScheduledTask } from './types'\n\nconst SNAPSHOT_VERSION = 1\n\ninterface ScheduleSnapshot {\n version: number\n id: string\n data: ScheduledTask\n}\n\nclass RemoteScheduleStoreImpl extends RemotePersistence<ScheduleSnapshot> implements ScheduleStore {\n protected override get serviceLabel(): string {\n return 'schedule 远程服务'\n }\n protected override get endpointFlag(): string {\n return '--session-url'\n }\n\n protected extractId(snapshot: ScheduleSnapshot): string {\n return snapshot.id\n }\n\n async saveTask(task: ScheduledTask): Promise<void> {\n await this.save({ version: SNAPSHOT_VERSION, id: task.id, data: task })\n }\n\n /**\n * M3 里程碑完成审核记录的已知架构成本:list()+load() 是 1+N 次 HTTP 请求(先拿 id 列表,\n * 再逐个拉快照),非单次批量端点。可接受——schedule 任务集合天然小(MAX_TASKS=50,\n * scheduler.ts),且 loadAllTasks 只在 SchedulerService.start()/定期 reloadFromStore()\n * 调用,不在 tick 热路径。真要优化需要 persistenced 加批量读端点,超出本 RFC 范围。\n */\n async loadAllTasks(): Promise<ScheduledTask[]> {\n const ids = await this.list()\n const tasks: ScheduledTask[] = []\n for (const id of ids) {\n const snapshot = await this.load(id)\n if (snapshot) tasks.push(snapshot.data)\n }\n tasks.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return tasks\n }\n\n async deleteTask(id: string): Promise<boolean> {\n return this.delete(id)\n }\n}\n\nexport interface RemoteScheduleStoreOptions {\n /**\n * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸\n * origin,该值本身是完整资源路径(对齐 `otto serve` 后端约定)。内部用\n * `new URL(sessionUrl).origin` 提取协议+主机+端口,再拼接 `/api/storage/schedules`——\n * 与 `remote-fire-ownership.ts` 的同一提取逻辑保持一致,不能直接拿 sessionUrl 当前缀用。\n */\n sessionUrl: string\n getAuth: () => Promise<{ token: string }>\n fetch?: typeof globalThis.fetch\n timeoutMs?: number\n wsKey?: string | (() => string | undefined)\n}\n\n/**\n * Create a ScheduleStore backed by the persistenced `/api/storage/schedules` namespace's\n * generic storage route (`storage.ts`, `namespace='schedules'`) — distinct from the\n * lease sub-route (`schedule-lease.ts`, `/api/schedules/...`) which handles fire-ownership\n * arbitration only.\n */\nexport function createRemoteScheduleStore(options: RemoteScheduleStoreOptions): ScheduleStore {\n const origin = new URL(options.sessionUrl).origin\n const remoteOptions: RemotePersistenceOptions<ScheduleSnapshot> = {\n baseUrl: `${origin}/api/storage/schedules`,\n getAuth: options.getAuth,\n fetch: options.fetch ?? globalThis.fetch,\n timeoutMs: options.timeoutMs ?? 10_000,\n wsKey: options.wsKey,\n }\n return new RemoteScheduleStoreImpl(remoteOptions)\n}\n","/**\n * schedule-lease-migrations.ts —— RFC-169 D3a: schedule_lease 表的独立 migration 段.\n *\n * `schedule_lease` is a schedule-domain table (server-arbitrated fire-ownership lease,\n * RFC-169 D3), not a generic blob-snapshot table — it must not join `BLOB_MIGRATIONS`\n * (@x-otto/persistence's generic storage kernel schema), per the \"generic storage kernel\n * does not hardcode domain schema\" principle already established by RFC-074 M5-02b\n * (session tables were carved out of the shared migration set for the same reason).\n *\n * Consumed by `persistenced-app.ts` via `acquireDb(leaseDbPath, wal, SCHEDULE_LEASE_MIGRATIONS)`\n * — deliberately a SEPARATE db file from the blob-snapshot `storage.db` (acquireDb's\n * fail-fast guard rejects injecting two different migration sets into the same db path,\n * `sqlite-db-cache.ts:134-140`).\n */\nimport type { Migration } from '@x-otto/persistence'\n\nexport const SCHEDULE_LEASE_MIGRATIONS: Migration[] = [\n {\n version: 1,\n up: (db) => {\n db.exec(`\n CREATE TABLE IF NOT EXISTS schedule_lease (\n ws_key TEXT PRIMARY KEY,\n token TEXT NOT NULL,\n renewed_at INTEGER NOT NULL\n );\n `)\n },\n },\n]\n","// schedule/src/fire-ownership.ts — Per-fire lease acquisition (async tryAcquire).\n// RFC-169 D1: partitioned by workspaceKey.\n// RFC-171 D2/D5: lease format token:pid:hostname + liveness probe.\nimport {\n closeSync,\n mkdirSync,\n openSync,\n writeSync,\n readFileSync,\n statSync,\n renameSync,\n rmSync,\n unlinkSync,\n utimesSync,\n} from 'node:fs'\nimport { resolve } from 'node:path'\nimport { randomUUID } from 'node:crypto'\n\nimport {\n OTTO_HOME,\n currentHostname,\n decodeLeaseToken,\n encodeLeaseToken,\n isProcessAlive,\n isShadowModeEnabled,\n} from '@x-otto/env'\n\n/** Per-workspace lock file: `${OTTO_HOME}/schedule-owner-<workspaceKey>.lock`. */\nfunction lockFilePath(workspaceKey: string): string {\n return resolve(OTTO_HOME, `schedule-owner-${workspaceKey}.lock`)\n}\n\nconst STALE_MS = 5 * 60_000 // 5-minute stale threshold (must exceed renew interval)\n\nexport interface FireOwnership {\n tryAcquire(): Promise<boolean>\n\n renew(): Promise<void>\n\n release(): Promise<void>\n\n lastFailureReason?(): 'occupied' | 'network-error' | undefined\n\n /**\n * 当前锁持有者的可读描述(诊断用,非语义判定)。返回 `undefined` 表示无法确定\n * (旧格式锁文件、锁已消失、或远程实现——服务端仲裁没有本地 pid 概念)。\n *\n * 为什么是可选端口:`FireOwnership` 是本地 O_EXCL 与远程 HTTP 双实现的公共接口\n * (RFC-169 D3/D3b),\"谁持有\"只在本地实现里有确切答案。消费方必须按可选处理,\n * 拿不到就退回不带主体的提示文案,不得据此做任何准入判定。\n */\n describeOwner?(): { pid?: number; host?: string } | undefined\n\n /** Whether this process currently holds the lock (verified against the token). */\n isOwner(): boolean\n}\n\n/**\n * Create a local token-based O_EXCL FireOwnership, scoped to `workspaceKey` (RFC-169 D1).\n * Only the lock holder runs the ticker and fires tasks.\n */\nexport function createLocalFireOwnership(workspaceKey: string): FireOwnership {\n const token = randomUUID()\n const LOCK_FILE = lockFilePath(workspaceKey)\n const myHostname = currentHostname()\n let owned = false\n\n /** Create the lockfile with our token (encoded as `token:pid:hostname`, RFC-171 D2). Returns false if it already exists. */\n const writeLock = (): boolean => {\n try {\n // OTTO_HOME may not exist yet on a fresh install — otto doesn't self-create it\n // ahead of first write. mkdirSync recursive is a cheap no-op once it does.\n mkdirSync(OTTO_HOME, { recursive: true })\n const fd = openSync(LOCK_FILE, 'wx') // O_EXCL\n try {\n writeSync(fd, encodeLeaseToken(token, process.pid, myHostname))\n } finally {\n closeSync(fd)\n }\n owned = true\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err\n return false\n }\n }\n\n /** Read + decode the lockfile currently written, or null if unreadable. */\n const readDecoded = (): { token: string; pid?: number; host?: string } | null => {\n try {\n const raw = readFileSync(LOCK_FILE, 'utf-8').trim()\n return decodeLeaseToken(raw)\n } catch {\n return null\n }\n }\n\n /** Read only the token field (fencing comparisons). */\n const readToken = (): string | null => readDecoded()?.token ?? null\n\n /** RFC-171 D2: confirmed-dead check — same hostname + pid not alive. Cross-host/malformed → false (safe fallback to stale-timeout). */\n const isConfirmedDead = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return !isProcessAlive(decoded.pid)\n }\n\n /** RFC-171 D2: confirmed-alive check — same hostname + pid alive. Used to reject takeover even past STALE_MS. */\n const isConfirmedAlive = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return isProcessAlive(decoded.pid)\n }\n\n // RFC-169 D3/D3b: FireOwnership methods are Promise-returning so this local O_EXCL\n // implementation and the remote HTTP one (remote-fire-ownership.ts) satisfy the same\n // interface. All work below is genuinely synchronous fs I/O — wrapped in an\n // immediately-resolved Promise, not an actual async boundary.\n const tryAcquireSync = (): boolean => {\n if (owned) return true\n // RFC-345:影子态不参与 fire ownership 竞选(§D9)——不建 schedule-owner-*.lock、\n // 不报失败原因。影子会话加载任务列表只读展示,fire 由真实 owner(或影子会话内部的\n // 其它路径)承担。这样影子进程既零写盘,也不会因竞选失败而降级走 STALE_MS 等\n // 既有的健康检查路径。\n if (isShadowModeEnabled()) return false\n if (writeLock()) return true\n\n // Lock exists — try liveness probe first (RFC-171 D2), fall back to mtime stale-timeout.\n const decoded = readDecoded()\n let mtime: number\n try {\n mtime = statSync(LOCK_FILE).mtimeMs\n } catch {\n // Vanished between calls — retry create.\n return writeLock()\n }\n\n if (decoded !== null && isConfirmedAlive(decoded)) {\n // Confirmed same-host owner is alive — reject even past STALE_MS (hung-process\n // trade-off; see module header + session-write-lease.ts D2 for the rationale).\n return false\n }\n\n const confirmedDead = decoded !== null && isConfirmedDead(decoded)\n if (!confirmedDead && Date.now() - mtime <= STALE_MS) return false\n\n // Stale or confirmed dead: atomically claim it away, then recreate with our token.\n const claimPath = `${LOCK_FILE}.stale-${randomUUID()}`\n try {\n renameSync(LOCK_FILE, claimPath)\n rmSync(claimPath, { force: true })\n } catch {\n return false // raced another taker — let the winner keep it\n }\n return writeLock()\n }\n\n return {\n async tryAcquire(): Promise<boolean> {\n return tryAcquireSync()\n },\n\n async renew(): Promise<void> {\n if (!owned) return\n // Only touch the lock if it is still ours (guards against a takeover).\n if (readToken() !== token) {\n owned = false\n return\n }\n try {\n const t = new Date()\n utimesSync(LOCK_FILE, t, t)\n } catch {\n owned = false // lock vanished under us\n }\n },\n\n async release(): Promise<void> {\n if (!owned) return\n // Only unlink if the lock is still ours (never delete a successor's lock).\n if (readToken() === token) {\n try {\n unlinkSync(LOCK_FILE)\n } catch {\n // best-effort cleanup\n }\n }\n owned = false\n },\n\n isOwner(): boolean {\n return owned && readToken() === token\n },\n\n /**\n * 读当前锁文件、解出持有者 pid/host(纯诊断)。锁不存在/旧格式裸 token/字段非法\n * 时返回 undefined——`decodeLeaseToken` 已对 pid 做合法性校验并安全降级。\n */\n describeOwner(): { pid?: number; host?: string } | undefined {\n const decoded = readDecoded()\n if (decoded === null) return undefined\n if (decoded.pid === undefined && decoded.host === undefined) return undefined\n return { ...(decoded.pid !== undefined ? { pid: decoded.pid } : {}), ...(decoded.host !== undefined ? { host: decoded.host } : {}) }\n },\n }\n}\n","// schedule/src/remote-fire-ownership.ts — Remote fire ownership via HTTP storage host.\n// RFC-169 D3c: 30s heartbeat, 180s stale.\n// D3d: fallback to tryAcquire on renew failure.\n// F1: in-flight dedup via inFlightAcquire.\nimport { randomUUID } from 'node:crypto'\n\nimport type { FireOwnership } from './fire-ownership'\n\nconst HEARTBEAT_MS = 30_000\nconst STALE_MS = 180_000\n\nexport interface RemoteFireOwnershipOptions {\n /**\n * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸\n * origin。该值本身是完整资源路径(对齐 `otto serve` 后端约定,见 RFC-146 §1.3 两条远程\n * 产品线对照表),本构造函数内部用 `new URL(sessionUrl).origin` 提取协议+主机+端口,\n * 再拼接 `/api/schedules/...` lease 路径——不能直接拿 `sessionUrl` 当 baseUrl 使用。\n */\n sessionUrl: string\n wsKey: string\n getAuth: () => Promise<{ token: string }>\n fetch?: typeof globalThis.fetch\n timeoutMs?: number\n now?: () => number\n}\n\ntype LeaseAction = 'acquire' | 'renew' | 'release'\n\n/**\n * F2 (RFC-169 终局 review 2026-07-15): `callLease` 此前把\"服务端明确拒绝\"(HTTP 200 但\n * `acquired:false`/`renewed:false`)和\"请求本身没打到/没打通\"(网络错误/超时/非 2xx HTTP\n * 状态)统一折叠成同一个 `null` 返回值——消费方因此无法区分\"另一个终端真的持有租约\"和\n * \"我这边压根没连上服务端、连判定结果都没拿到\",导致 TUI 只读提示在网络故障时显示一句\n * 具体但错误的诊断(\"另一终端持有触发权\")。现在用一个可辨识返回类型显式区分三态:\n * `{ ok:true, data }`(服务端已应答,无论 acquired/renewed 是 true 还是 false,这本身\n * 就是一次成功的判定往返)、`{ ok:false, reason:'network-error' }`(请求本身失败)。\n * `response.ok===false`(如 403/500)也归为 network-error——那不是\"lease 被占用\"这个\n * 业务语义上的拒绝,是请求层面出了问题(鉴权/服务端错误),同样不该说成\"被占用\"。\n *\n * 已知精度局限(本次 review 复核发现,暂不展开为独立修复):`network-error` 这个名字\n * 把\"真连不上网络\"和\"鉴权失败(403 token 不对)\"两种不同性质的请求层失败合并成一类\n * ——两者的正确排查方向不同(前者查网络连通性,后者查 token 配置),但当前 TUI 提示\n * 文案统一说\"连接远程服务失败\",对鉴权失败场景不够精确。之所以不现在拆出独立的\n * `'auth-error'` 三态:① token 由 `getAuth()` 单一配置源注入,生产场景下 401/403 出现\n * 概率极低(配置错误通常在首次连接就会暴露,不会隐蔽到运行时才触发);② 拆分收益不足以\n * 覆盖新增一个枚举值分支的代码/文档维护成本,属于本次 review 判断\"可保留观察\"的一项。\n */\ntype LeaseCallResult =\n | { ok: true; data: Record<string, unknown> }\n | { ok: false; reason: 'network-error' }\n\n/** Create a server-arbitrated FireOwnership backed by persistenced's lease endpoints. */\nexport function createRemoteFireOwnership(options: RemoteFireOwnershipOptions): FireOwnership {\n const token = randomUUID()\n const fetchImpl = options.fetch ?? globalThis.fetch\n const timeoutMs = options.timeoutMs ?? 10_000\n const baseUrl = new URL(options.sessionUrl).origin\n const now = options.now ?? Date.now\n let owned = false\n /** Last time an actual HTTP renew/acquire heartbeat was sent (throttling — see header). */\n let lastHeartbeatAt = 0\n /** F1: dedupe concurrent tryAcquire() calls onto one in-flight request/state-write. */\n let inFlightAcquire: Promise<boolean> | undefined\n /** F2: reason for the most recent non-owner outcome, for TUI diagnostics. */\n let lastFailureReason: 'occupied' | 'network-error' | undefined\n\n const callLease = async (action: LeaseAction): Promise<LeaseCallResult> => {\n const auth = await options.getAuth()\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const response = await fetchImpl(\n `${baseUrl}/api/schedules/${encodeURIComponent(options.wsKey)}/lease/${action}`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${auth.token}`,\n },\n body: JSON.stringify({ token }),\n signal: controller.signal,\n },\n )\n if (!response.ok) return { ok: false, reason: 'network-error' }\n return { ok: true, data: (await response.json()) as Record<string, unknown> }\n } catch {\n // Network error / timeout / abort — treated as \"did not succeed\", never thrown.\n // The scheduler's tick loop tolerates transient failures and retries next heartbeat.\n return { ok: false, reason: 'network-error' }\n } finally {\n clearTimeout(timeout)\n }\n }\n\n const tryAcquire = (): Promise<boolean> => {\n // F1: if a tryAcquire() is already in flight, share its result instead of firing a\n // second concurrent HTTP request whose (possibly stale-ordered) response could\n // clobber `owned` after the first call already set it. Both callers get the same\n // outcome, which is the only sane semantics for \"try to acquire\" called concurrently.\n if (inFlightAcquire) return inFlightAcquire\n\n const promise = (async (): Promise<boolean> => {\n lastHeartbeatAt = now()\n const result = await callLease('acquire')\n if (!result.ok) {\n owned = false\n lastFailureReason = 'network-error'\n return false\n }\n const acquired = result.data['acquired'] === true\n owned = acquired\n lastFailureReason = acquired ? undefined : 'occupied'\n return acquired\n })()\n\n inFlightAcquire = promise\n promise.finally(() => {\n if (inFlightAcquire === promise) inFlightAcquire = undefined\n })\n return promise\n }\n\n return {\n tryAcquire,\n\n async renew(): Promise<void> {\n // Throttle: SchedulerService.tick() calls renew() every second, but a real HTTP\n // heartbeat only needs to fire every HEARTBEAT_MS (30s, D3c) — everything in between\n // is a free no-op. The very first call after construction always fires (lastHeartbeatAt\n // starts at 0), matching the local implementation's \"renew is safe to call immediately\".\n if (now() - lastHeartbeatAt < HEARTBEAT_MS) return\n\n if (!owned) {\n // D3d: not currently owner (lost it, or never acquired) — every heartbeat is an\n // acquire attempt so a healed network partition or an expired competitor's lease\n // can be reclaimed without waiting for a full process restart.\n await tryAcquire()\n return\n }\n lastHeartbeatAt = now()\n const result = await callLease('renew')\n if (!result.ok) {\n // F2: renew failed at the transport level (not a server-side \"superseded\"\n // rejection) — still downgrade to non-owner (D3d semantics unchanged), but record\n // the accurate reason so the TUI doesn't claim \"another terminal holds it\".\n owned = false\n lastFailureReason = 'network-error'\n return\n }\n const renewed = result.data['renewed'] === true\n if (!renewed) {\n // D3d: lost the lease (superseded, or this renew call itself failed/timed out).\n // Don't distinguish \"genuinely superseded\" from \"network blip\" here — both cases\n // downgrade to non-owner and let the next heartbeat's tryAcquire() sort it out.\n owned = false\n lastFailureReason = 'occupied'\n } else {\n lastFailureReason = undefined\n }\n },\n\n async release(): Promise<void> {\n if (!owned) return\n await callLease('release')\n owned = false\n },\n\n isOwner(): boolean {\n return owned\n },\n\n lastFailureReason(): 'occupied' | 'network-error' | undefined {\n return owned ? undefined : lastFailureReason\n },\n }\n}\n\nexport { HEARTBEAT_MS as REMOTE_FIRE_OWNERSHIP_HEARTBEAT_MS, STALE_MS as REMOTE_FIRE_OWNERSHIP_STALE_MS }\n","/**\n * job-fire-target.ts —— RFC-406 D3:JobFireTarget handler。\n *\n * 从 scheduler.ts fire() 的 job 分支拆出。到点派发 AgentJob(隔离 worktree 执行),\n * 监控完成状态,失败重试(maxRetries 次),支持 cancel。\n *\n * 原代码位置:scheduler.ts fire() 的 else 分支 + waitForCompletion() + cancel() 的\n * runningJobsByTaskId/cancelRequested 记账。\n *\n * **cancel 交互**:SchedulerService.cancel() 调用此 handler 的 requestCancel(),\n * 设置 cancelRequested 标志。fire() 循环在每次 attempt 前检查此标志。\n */\nimport type { AgentJobRegistry } from '@x-otto/runtime'\nimport type { ThinkingLevel } from '@x-otto/interchange'\n\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { CancelResult, ScheduledTask } from '../types'\n\nexport interface JobFireTargetDeps {\n jobRegistry: AgentJobRegistry\n /**\n * Dispatch an agent job. Mirrors AgentJobService.start() shape:\n * synchronously returns a record with at least { id }.\n */\n startJob: (input: {\n title: string\n prompt: string\n sessionId: string\n origin: 'user' | 'main'\n /** RFC-438:per-job 指定模型与思考档位(task.modelId/thinkingLevel 透传)。 */\n model?: { id: string; thinkingLevel?: ThinkingLevel }\n }) => { id: string }\n /**\n * RFC-435 D1:绑定会话 id 端口。serve/daemon 无人值守装配可缺省——\n * job-only 模式无会话,fire 时 sessionId 用空串(无会话绑定,notify 不投递)。\n */\n getSessionId?: () => string\n /** RFC-230:重试之间的固定延迟(ms)。默认 0。 */\n retryBackoffMs?: number\n /** 测试可注入假 sleep 避免真实等待。 */\n sleep?: (ms: number) => Promise<void>\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n /** needs_input 硬上限倍数(对齐 ScheduleConfig.needsInputMaxExtensionFactor,默认 3)。 */\n needsInputMaxExtensionFactor?: number\n}\n\ninterface RunningJobRecord {\n jobId: string\n fireToken: number\n attempt: number\n}\n\nexport class JobFireTarget implements FireTarget {\n readonly type = 'job'\n\n private readonly runningJobsByTaskId: Map<string, RunningJobRecord> = new Map()\n private readonly cancelRequested: Set<string> = new Set()\n private _nextFireToken = 0\n\n constructor(private deps: JobFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n // job 是默认路径——当 task 既无 subscriber 也非 target:session 时命中\n return !task.subscriber && task.target !== 'session'\n }\n\n async fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult> {\n let outcome: 'success' | 'failed' = 'failed'\n let outcomeCancelled = false\n let lastError: string | undefined\n const fireToken = ++this._nextFireToken\n\n const origin: 'user' | 'main' = task.origin === 'model' ? 'main' : 'user'\n // RFC-435 D1:无人值守(无 getSessionId 端口)时用空串——job 无会话绑定,\n // 下游 notify 无匹配消费者即安全旁路(同 fail-open 纪律)。\n const sessionId = this.deps.getSessionId?.() ?? ''\n\n for (let attempt = 0; attempt <= task.maxRetries; attempt++) {\n if (this.cancelRequested.has(task.id)) {\n this.deps.fireLogger.write(task.id, `canceled by user (attempt ${attempt + 1})`)\n outcomeCancelled = true\n break\n }\n\n // RFC-438 评审阻断②:startJob 同步抛错(如 per-job 模型在 fire 时才失效)必须\n // 折叠为一次失败 attempt——否则跳出循环,maxRetries 被跳过、recurring 不标\n // lastFireFailed(= 每晚静默失败仅一行 fire log)。\n let job: { id: string }\n try {\n job = this.deps.startJob({\n title: task.name,\n prompt: task.prompt,\n sessionId,\n origin,\n ...(task.modelId ? { model: { id: task.modelId, thinkingLevel: task.thinkingLevel } } : {}),\n })\n } catch (err) {\n lastError = err instanceof Error ? err.message : String(err)\n this.deps.fireLogger.write(task.id, `startJob failed (attempt ${attempt + 1}): ${lastError}`)\n if (attempt < task.maxRetries) {\n const backoffMs = this.deps.retryBackoffMs ?? 0\n const sleep = this.deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n await sleep(backoffMs)\n }\n continue\n }\n\n this.runningJobsByTaskId.set(task.id, { jobId: job.id, fireToken, attempt })\n\n const result = await this.waitForCompletion(job.id, task.maxDurationMs, ctx)\n\n if (result === 'success') {\n // review I3: when the job produced a diff (status 'ready'), surface the pending review.\n const finishedJob = this.deps.jobRegistry.get(job.id)\n const pendingApply = finishedJob?.status === 'ready' && !!finishedJob.diff?.trim()\n this.deps.fireLogger.write(\n task.id,\n pendingApply\n ? `completed (attempt ${attempt + 1}) — diff ready, /job apply ${job.id}`\n : `completed (attempt ${attempt + 1})`,\n )\n outcome = 'success'\n return { outcome, jobId: job.id }\n }\n\n if (result === 'canceled') {\n this.deps.fireLogger.write(task.id, `canceled by user (attempt ${attempt + 1})`)\n outcomeCancelled = true\n break\n }\n\n lastError = result === 'timeout' ? 'timed out' : 'job failed'\n\n if (attempt < task.maxRetries) {\n const backoffMs = this.deps.retryBackoffMs ?? 0\n const sleep = this.deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n await sleep(backoffMs)\n }\n }\n\n if (!outcomeCancelled) {\n this.deps.fireLogger.write(task.id, `failed after ${task.maxRetries + 1} attempt(s)`)\n }\n\n return {\n outcome,\n cancelled: outcomeCancelled,\n error: lastError,\n }\n }\n\n // ── Cancel support ──\n\n /**\n * 设置 cancel 意图——fire() 循环在下次 attempt 前检查并停止。\n * 同时调用 jobRegistry.cancel() 终止当前正在运行的 job。\n * 返回 CancelResult(与 SchedulerService.cancel() 的返回类型对齐)。\n */\n requestCancel(taskId: string): CancelResult {\n let jobId: string | undefined\n const record = this.runningJobsByTaskId.get(taskId)\n if (record) {\n jobId = record.jobId\n } else {\n // Fallback: scan jobRegistry for a still-running job (best-effort after process restart)\n const runningJobs = this.deps.jobRegistry.list().filter(\n (j) => j.status === 'running' || j.status === 'needs_input',\n )\n if (runningJobs.length === 1) {\n jobId = runningJobs[0]!.id\n }\n }\n if (!jobId) return { ok: false, reason: 'not_running' }\n\n const job = this.deps.jobRegistry.get(jobId)\n if (!job) return { ok: false, reason: 'job_not_found' }\n if (job.status === 'ready') return { ok: false, reason: 'job_is_ready' }\n if (job.status === 'applied') return { ok: false, reason: 'job_already_applied' }\n if (job.status === 'failed') return { ok: false, reason: 'job_already_failed' }\n if (job.status === 'canceled') return { ok: false, reason: 'job_already_canceled' }\n\n this.cancelRequested.add(taskId)\n this.deps.jobRegistry.cancel(jobId)\n return { ok: true }\n }\n\n /** 清理 fire 完成后的记账状态。由 FireExecutor 在 fire().finally 中调用。 */\n cleanup(taskId: string): void {\n this.runningJobsByTaskId.delete(taskId)\n this.cancelRequested.delete(taskId)\n }\n\n /** 当前有正在运行的 job 的 task id 集合。 */\n getRunningTaskIds(): Set<string> {\n return new Set(this.runningJobsByTaskId.keys())\n }\n\n // ── Internal: event-based completion monitor ──\n\n private waitForCompletion(\n jobId: string,\n maxDurationMs: number,\n ctx: FireContext,\n ): Promise<'success' | 'failed' | 'timeout' | 'canceled'> {\n const needsInputMaxExtensionFactor = this.deps.needsInputMaxExtensionFactor ?? 3 // review S8: hard cap on needs_input total wait\n\n return new Promise((resolve) => {\n let settled = false\n let timeout: ReturnType<typeof setTimeout> | undefined\n let unsubExit: (() => void) | undefined\n let unsubUpdate: (() => void) | undefined\n\n const startedAt = ctx.now()\n const hardDeadline = startedAt + maxDurationMs * needsInputMaxExtensionFactor\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout)\n unsubExit?.()\n unsubUpdate?.()\n }\n\n const finish = (result: 'success' | 'failed' | 'timeout' | 'canceled') => {\n if (settled) return\n settled = true\n cleanup()\n resolve(result)\n }\n\n const armTimeout = (ms: number) => {\n if (timeout) clearTimeout(timeout)\n timeout = setTimeout(() => {\n this.deps.jobRegistry.cancel(jobId)\n finish('failed')\n }, ms)\n }\n\n unsubUpdate = this.deps.jobRegistry.onUpdate((job) => {\n if (job.id !== jobId) return\n\n if (job.status === 'ready') {\n finish('success')\n } else if (job.status === 'needs_input') {\n const now = ctx.now()\n const remaining = Math.min(maxDurationMs, hardDeadline - now)\n if (remaining <= 0) {\n this.deps.jobRegistry.cancel(jobId)\n finish('failed')\n return\n }\n armTimeout(remaining)\n }\n })\n\n unsubExit = this.deps.jobRegistry.onExit((job) => {\n if (job.id !== jobId) return\n\n if (job.status === 'failed') {\n finish('failed')\n } else if (job.status === 'canceled') {\n finish('canceled')\n } else if (job.status === 'applied') {\n finish('success')\n }\n })\n\n armTimeout(maxDurationMs)\n })\n }\n}\n","/**\n * session-fire-target.ts —— RFC-406 D3:SessionFireTarget handler。\n *\n * 从 scheduler.ts fireSession() 拆出。把 target==='session' 任务的 prompt 注入\n * boundSessionId 对应的当前会话。不建 worktree、不派 AgentJob、不进重试循环。\n *\n * 原代码位置:scheduler.ts fireSession() 方法。\n */\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { ScheduledTask } from '../types'\n\nexport interface SessionFireTargetDeps {\n /**\n * 把 prompt 注入 boundSessionId 对应的当前会话。\n * 返回 true=已成功注入;false=失效(会话身份不匹配/已销毁/archived/paused 等)。\n */\n followUp: (task: {\n prompt: string\n boundSessionId: string\n name: string\n origin: 'user' | 'model'\n }) => boolean\n /**\n * RFC-406 D6:session-local 任务创建时绑定的当前 sessionId 端口。\n * scheduler.add() 经 SchedulerService.getSessionIdForTask() 读取此值填充 boundSessionId。\n * (旧 SchedulerDeps.getSessionId 的语义迁入此 handler。)\n */\n getSessionId: () => string\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n}\n\nexport class SessionFireTarget implements FireTarget {\n readonly type = 'session'\n\n constructor(private deps: SessionFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n return task.target === 'session'\n }\n\n /** RFC-406 D6:暴露当前 sessionId 供 scheduler.add() 绑定 boundSessionId。 */\n getSessionId(): string {\n return this.deps.getSessionId()\n }\n\n async fire(task: ScheduledTask, _ctx: FireContext): Promise<FireResult> {\n if (!task.boundSessionId) {\n this.deps.fireLogger.write(task.id, 'session tick skipped: task has no boundSessionId (invalid state)')\n return { outcome: 'failed', error: 'no boundSessionId' }\n }\n\n // RFC-357 G5:每次 session-local 投递都带可见来源标记。\n // D3 新增约束1:模型创建的 session-local 任务额外在首次 fire 加一段更详细的提示。\n const isFirstModelFire = task.origin === 'model' && task.lastFiredAt == null\n const marker = `⏰ scheduled: ${task.name}`\n const prompt = isFirstModelFire\n ? `${marker} [created by the model — first fire, use /schedule cancel ${task.id} to stop it]\\n\\n${task.prompt}`\n : `${marker}\\n\\n${task.prompt}`\n\n let delivered = false\n try {\n delivered = this.deps.followUp({\n prompt,\n boundSessionId: task.boundSessionId,\n name: task.name,\n origin: task.origin,\n })\n } catch (err) {\n this.deps.fireLogger.write(\n task.id,\n `session tick error: ${err instanceof Error ? err.message : String(err)}`,\n )\n return { outcome: 'failed', error: err instanceof Error ? err.message : String(err) }\n }\n\n this.deps.fireLogger.write(\n task.id,\n delivered\n ? `session tick delivered (session ${task.boundSessionId})`\n : `session tick not delivered (session ${task.boundSessionId} unavailable — evicted/disposed/archived/paused/mismatched)`,\n )\n return { outcome: delivered ? 'success' : 'failed' }\n }\n}\n","/**\n * subscriber-fire-target.ts —— RFC-406 D3:SubscriberFireTarget handler。\n *\n * 从 scheduler.ts fireSubscriber() 拆出。到点只投递一条通知给已订阅的插件常驻服务,\n * 不派 AgentJob——服务若要产生工作须自己走 capability-gated startJob。\n *\n * 原代码位置:scheduler.ts fireSubscriber() 方法。\n */\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { ScheduleSubscriber, ScheduledTask } from '../types'\n\nexport interface SubscriberFireTargetDeps {\n /**\n * 向订阅者投递到点通知的窄端口。返回是否真的送达——false 表示服务未运行/已停机。\n * 端口刻意只接受纯数据标识:handler 不知道插件服务的存在形式,也不持有其句柄。\n */\n notify: (\n subscriber: ScheduleSubscriber,\n tick: { scheduleId: string; occurrenceId: string; occurredAt: number },\n ) => boolean\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n}\n\nexport class SubscriberFireTarget implements FireTarget {\n readonly type = 'plugin-service'\n\n constructor(private deps: SubscriberFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n return !!task.subscriber\n }\n\n async fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult> {\n const subscriber = task.subscriber\n if (!subscriber) return { outcome: 'failed', error: 'no subscriber on task' }\n\n const occurredAt = ctx.now()\n // occurrenceId 由 {taskId, 本次计划触发时刻} 派生——同一次计划触发的重复投递\n // 得到同一个 id(幂等键)。ticker 路径由调用方传入 scheduledFor;runNow 用实际触发时刻。\n const occurrenceId = `${task.id}@${ctx.scheduledFor ?? occurredAt}`\n\n let delivered = false\n try {\n delivered = this.deps.notify(subscriber, { scheduleId: task.id, occurrenceId, occurredAt })\n } catch (err) {\n this.deps.fireLogger.write(\n task.id,\n `subscriber tick error: ${err instanceof Error ? err.message : String(err)}`,\n )\n return { outcome: 'failed', error: err instanceof Error ? err.message : String(err) }\n }\n\n this.deps.fireLogger.write(\n task.id,\n delivered\n ? `subscriber tick delivered (${subscriber.pluginId}:${subscriber.serviceId}, occurrence ${occurrenceId})`\n : `subscriber tick not delivered (${subscriber.pluginId}:${subscriber.serviceId} not running)`,\n )\n return { outcome: delivered ? 'success' : 'failed' }\n }\n}\n","/**\n * in-process-event-trigger.ts —— 进程内事件触发器(RFC-423 D3/D4,经 RFC-433 D4 收敛)。\n *\n * 监听 AgentJobRegistry 终态事件(onExit:applied/failed/canceled),事件满足\n * EventTriggerSpec 时回调 fire(task)。MVP 唯一的进程内事件源。\n *\n * ## 两重死循环闸门(RFC-357 §7 点名的核心风险)\n *\n * 自触发死循环场景:event(job.applied) × JobFireTarget → 触发的 job apply 后又满足\n * job.applied → 再触发 → 无限循环。两重闸门:\n *\n * 1. **拒绝 job 投递**(闸门A,`SchedulerService.add()`):event × JobFireTarget 组合被\n * **无条件拒绝**——事件任务只能投 session 或 subscriber,两者都不产生新 job,\n * 自触发链在源头就不成立。这也正好匹配\"注入对话继续任务\"的诉求。\n * 2. **频率 debounce**(闸门B,本文件):单任务在窗口 debounceWindowMs 内最多\n * maxFiresPerWindow 次,超限记日志跳过。防高频事件(如连续多个 job 快速 apply)\n * 打爆当前对话。\n *\n * **RFC-433 D4 删除的第三重闸门**:原设计还有\"自产 job 排除\"(`markSelfDerived` +\n * `selfDerivedJobIds`),用于在 event × job 组合下不对自己产出的 job 触发。它从未接线\n * (生产代码零调用方)——因为闸门A 已让该组合根本不可能存在,闸门内的场景不可达。\n * **若将来放开闸门A,必须同时恢复自产 job 排除**(→ RFC-433 §4 R4)。\n */\nimport type { AgentJobRegistry, AgentSessionJob } from '@x-otto/runtime'\n\nimport type { EventDrivenTrigger } from '../trigger'\nimport type { ScheduledTask, EventTriggerSpec } from '../types'\n\nexport interface InProcessEventTriggerDeps {\n jobRegistry: AgentJobRegistry\n now?: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /** 闸门B:debounce 窗口(ms)。缺省 60s。 */\n debounceWindowMs?: number\n /** 闸门B:窗口内最大触发次数。缺省 5。 */\n maxFiresPerWindow?: number\n}\n\nexport class InProcessEventTrigger implements EventDrivenTrigger {\n readonly type = 'in-process-event'\n\n /** 闸门B:每任务的触发时刻队列(滑动窗口)。 */\n private readonly fireTimestamps = new Map<string, number[]>()\n\n private readonly now: () => number\n private readonly debounceWindowMs: number\n private readonly maxFiresPerWindow: number\n\n constructor(private deps: InProcessEventTriggerDeps) {\n this.now = deps.now ?? Date.now\n this.debounceWindowMs = deps.debounceWindowMs ?? 60_000\n this.maxFiresPerWindow = deps.maxFiresPerWindow ?? 5\n }\n\n canHandle(task: ScheduledTask): boolean {\n // 只接管进程内 job 事件源的任务(MVP 唯一支持的 source)。\n return task.eventTrigger?.source === 'job'\n }\n\n arm(task: ScheduledTask, fire: (task: ScheduledTask) => void): () => void {\n const spec = task.eventTrigger\n if (!spec || spec.source !== 'job') return () => {}\n\n // 监听 AgentJobRegistry 终态(applied/failed/canceled)。\n return this.deps.jobRegistry.onExit((job) => {\n if (!this.matches(job, spec)) return\n if (!this.passesDebounce(task.id)) {\n this.deps.logger?.error(\n `[schedules] event-trigger ${task.id} debounced (>${this.maxFiresPerWindow} fires / ${this.debounceWindowMs}ms)`,\n )\n return\n }\n fire(task)\n })\n }\n\n // ── 匹配 + 闸门 ──\n\n private matches(job: AgentSessionJob, spec: EventTriggerSpec): boolean {\n // 事件名匹配 job 终态\n if (spec.event !== job.status) return false\n // 可选 sessionId 过滤\n if (spec.filter?.sessionId && spec.filter.sessionId !== job.sessionId) return false\n return true\n }\n\n /** 闸门B:滑动窗口频率检查。返回 true=放行,false=超限拦截。 */\n private passesDebounce(taskId: string): boolean {\n const now = this.now()\n const cutoff = now - this.debounceWindowMs\n const stamps = (this.fireTimestamps.get(taskId) ?? []).filter((t) => t > cutoff)\n if (stamps.length >= this.maxFiresPerWindow) {\n this.fireTimestamps.set(taskId, stamps) // 保留裁剪后的窗口\n return false\n }\n stamps.push(now)\n this.fireTimestamps.set(taskId, stamps)\n return true\n }\n}\n","/**\n * schedule-capability.ts —— Binds a SchedulerService to the shape the schedule_* model\n * tools consume (ToolCallbacksDeps.schedule).\n *\n * `origin` is pinned to 'model' HERE (the model-vs-user distinction lives in\n * wiring, not tool execute — ToolCallContext has no origin field). The scheduler\n * is resolved lazily so this capability object can exist at tool-registration\n * time (the `when: Boolean(c.schedule)` gate) even before the interactive scheduler\n * is constructed; it is populated via App.setScheduleService during interactive\n * setup (RFC-087 A1).\n */\nimport type { SchedulerService } from './scheduler'\n\nexport interface ScheduleCapability {\n create: (input: {\n cron: string\n prompt: string\n recurring: boolean\n name?: string\n /** review I2: optional override for the default 5-min single-fire timeout. */\n maxDurationMs?: number\n /**\n * RFC-357 D3 (M2): where the prompt fires. Default 'job' (existing behavior, zero\n * change). 'session' injects into the CURRENT conversation via followUp instead of\n * dispatching an isolated worktree AgentJob — see `create.ts`'s tool description for\n * the model-facing guidance on when to pick which.\n */\n target?: 'job' | 'session'\n /**\n * RFC-423:进程内事件触发。存在时任务由事件驱动(如 job diff applied)而非 cron。\n * 必须配 target:'session'(或 subscriber)——事件触发不允许 job 投递(死循环闸门)。\n */\n eventTrigger?: { source: string; event: string; filter?: { sessionId?: string } }\n /** RFC-438:到点执行时使用的模型 id(如 \"deepseek/deepseek-v4-flash\")。缺省 = 当前会话模型。 */\n modelId?: string\n /** RFC-438:思考档位(配 modelId 使用)。 */\n thinkingLevel?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'\n }) => {\n taskId: string\n nextFireAt: number\n name: string\n /** RFC-357 D3: echoes the resolved target so callers (e.g. first-fire notice, M2) can react. */\n target?: 'job' | 'session'\n }\n list: () => Array<{\n id: string\n name: string\n cronExpression: string\n enabled: boolean\n recurring: boolean\n nextFireAt: number\n lastFiredAt?: number\n lastFireFailed?: boolean\n lastFireError?: string\n lastFireCancelled?: boolean\n }>\n delete: (taskId: string) => boolean\n run: (taskId: string) => void\n cancel: (taskId: string) => { ok: boolean; reason?: string }\n}\n\nexport interface ScheduleCapabilityDeps {\n /**\n * RFC-438 D3b:create 时校验模型 id。返回错误信息或 undefined(合法)。\n * 缺省 = 不校验(运行时 fail-closed 兜底——fire 时 resolveJobModel 抛错)。\n */\n validateModelId?: (id: string) => string | undefined\n}\n\nexport function createScheduleCapability(\n getScheduler: () => SchedulerService | undefined,\n deps: ScheduleCapabilityDeps = {},\n): ScheduleCapability {\n return {\n create: (input) => {\n const scheduler = getScheduler()\n if (!scheduler) {\n throw new Error('Schedule scheduler is not available in this session.')\n }\n // RFC-438 D3b:create 时提前校验模型 id——模型乱填 id 应在创建时明确拒绝\n // (否则任务建了、到点 fire 才炸,recurring 每晚静默失败)。\n if (input.modelId) {\n const err = deps.validateModelId?.(input.modelId)\n if (err) throw new Error(err)\n }\n const task = scheduler.add({\n name: input.name ?? 'schedule',\n prompt: input.prompt,\n cronExpression: input.cron,\n recurring: input.recurring,\n origin: 'model', // pinned by wiring — schedule_create is always model-originated\n maxDurationMs: input.maxDurationMs,\n // RFC-357 D3: model may request target:'session'. Non-owner rejection and\n // boundSessionId binding happen inside scheduler.add() (RFC-357 M1) — this\n // capability layer does not duplicate that logic.\n target: input.target,\n // RFC-423:事件触发透传。scheduler.add() 内建闸门2(event × job 拒绝)。\n eventTrigger: input.eventTrigger,\n // RFC-438:per-job 模型指定透传(D3b 已校验 modelId 合法)。\n modelId: input.modelId,\n thinkingLevel: input.thinkingLevel,\n })\n return { taskId: task.id, nextFireAt: task.nextFireAt, name: task.name, target: task.target }\n },\n list: () =>\n (getScheduler()?.list() ?? []).map((t) => ({\n id: t.id,\n name: t.name,\n cronExpression: t.cronExpression,\n enabled: t.enabled,\n recurring: t.recurring,\n nextFireAt: t.nextFireAt,\n lastFiredAt: t.lastFiredAt,\n lastFireFailed: t.lastFireFailed,\n lastFireError: t.lastFireError,\n lastFireCancelled: t.lastFireCancelled,\n })),\n delete: (taskId) => getScheduler()?.remove(taskId) ?? false,\n run: (taskId) => {\n getScheduler()?.runNow(taskId)\n },\n cancel: (taskId) => {\n return getScheduler()?.cancel(taskId) ?? { ok: false, reason: 'Schedule scheduler is not available.' }\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["SNAPSHOT_VERSION"],"sources":["../src/cron-parser.ts","../src/registry.ts","../src/fire-target.ts","../src/fire-executor.ts","../src/fire-logger.ts","../src/schedule-config.ts","../src/tick-loop.ts","../src/schedule-persistence.ts","../src/scheduler.ts","../src/schedule-store.ts","../src/remote-schedule-store.ts","../src/schedule-lease-migrations.ts","../src/fire-ownership.ts","../src/remote-fire-ownership.ts","../src/fire-targets/job-fire-target.ts","../src/fire-targets/session-fire-target.ts","../src/fire-targets/subscriber-fire-target.ts","../src/triggers/in-process-event-trigger.ts","../src/schedule-capability.ts"],"sourcesContent":["// schedule/src/cron-parser.ts — 5-field cron expression parser.\n// Uses local Date API (DST-safe). Search capped at 366 days forward.\n\ntype FieldSet = Set<number>\n\n/**\n * Parse a 5-field cron expression (minute hour dom month dow) and return the next\n * fire time >= from (inclusive). All times in local timezone.\n *\n * Throws on invalid expressions.\n *\n * Field syntax: wildcard (*), single value (5), step (e.g. \\*\\/15 or 5\\/15),\n * range (1-5), list (1,15,30), range with step (1-10\\/2).\n *\n * Unsupported: L, W, ?, #, name aliases (MON, JAN).\n *\n * ## DoM∧DoW semantics\n *\n * If both day-of-month and day-of-week are constrained (not a bare *),\n * a date matches if **EITHER** field matches (standard vixie-cron OR behaviour).\n *\n * ## Day-of-week mapping\n *\n * 0 = Sunday, 1–6 = Monday–Saturday, 7 = also Sunday.\n */\nexport function parseCron(expr: string, from?: Date): Date {\n const { validMinute, validHour, validDom, validMonth, validDow, domConstrained, dowConstrained } =\n parseExpression(expr)\n\n const startTime = from ? new Date(from.getTime()) : new Date()\n startTime.setSeconds(0, 0)\n\n const maxTime = new Date(startTime.getTime() + 366 * 24 * 60 * 60 * 1000)\n const current = new Date(startTime.getTime())\n\n while (current.getTime() <= maxTime.getTime()) {\n // --- minute ---\n const minute = current.getMinutes()\n const nextMin = nextInSet(validMinute, minute)\n if (nextMin === null) {\n current.setMinutes(firstInSet(validMinute))\n current.setHours(current.getHours() + 1)\n current.setSeconds(0, 0)\n continue\n }\n if (nextMin !== minute) {\n current.setMinutes(nextMin)\n current.setSeconds(0, 0)\n continue\n }\n\n // --- hour ---\n const hour = current.getHours()\n const nextHr = nextInSet(validHour, hour)\n if (nextHr === null) {\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setDate(current.getDate() + 1)\n current.setSeconds(0, 0)\n continue\n }\n if (nextHr !== hour) {\n current.setHours(nextHr)\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // --- day (DoM ∨ DoW) ---\n const dom = current.getDate()\n const dow = current.getDay()\n const domMatch = validDom.has(dom)\n const dowMatch = validDow.has(dow)\n\n let dayMatch: boolean\n if (domConstrained && dowConstrained) {\n dayMatch = domMatch || dowMatch\n } else if (domConstrained) {\n dayMatch = domMatch\n } else if (dowConstrained) {\n dayMatch = dowMatch\n } else {\n dayMatch = true\n }\n\n if (!dayMatch) {\n current.setDate(current.getDate() + 1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // --- month ---\n const month = current.getMonth() + 1 // 1-based\n const nextMo = nextInSet(validMonth, month)\n if (nextMo === null) {\n // Wrap to next year\n current.setMonth(firstInSet(validMonth) - 1)\n current.setFullYear(current.getFullYear() + 1)\n current.setDate(1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n if (nextMo !== month) {\n current.setMonth(nextMo - 1)\n current.setDate(1)\n current.setHours(firstInSet(validHour))\n current.setMinutes(firstInSet(validMinute))\n current.setSeconds(0, 0)\n continue\n }\n\n // All fields matched\n return current\n }\n\n throw new Error(`No matching time found for cron \"${expr}\" within 366 days of ${startTime.toISOString()}`)\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\ninterface ParsedExpression {\n validMinute: FieldSet\n validHour: FieldSet\n validDom: FieldSet\n validMonth: FieldSet\n validDow: FieldSet\n domConstrained: boolean\n dowConstrained: boolean\n}\n\nfunction parseExpression(expr: string): ParsedExpression {\n if (!expr || !expr.trim()) {\n throw new Error('Invalid cron expression: empty string')\n }\n\n const fields = expr.trim().split(/\\s+/)\n if (fields.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length} (${expr})`)\n }\n\n const [minField, hourField, domField, monthField, dowField] = fields as [string, string, string, string, string]\n\n const validMinute = parseField(minField, 0, 59)\n const validHour = parseField(hourField, 0, 23)\n const validDom = parseField(domField, 1, 31)\n const validMonth = parseField(monthField, 1, 12)\n const validDow = parseField(dowField, 0, 7)\n\n // 7 = Sunday, also maps to 0 for Date.getDay() compatibility\n if (validDow.has(7)) {\n validDow.add(0)\n }\n\n // A field is \"constrained\" if the user explicitly specified values (not bare *)\n // 架构 review 2026-08-12 S12:`*/1` 语义等价 `*`(覆盖全域、步长 1),此前按裸字符串\n // 比较被判为 constrained,触发 DoM∧DoW 的 OR 语义——dow 单侧约束被误升为双约束。\n // 全域等价判定只认两种写法(`*` 与 `*/1`);`1-31`/`0-6` 这类显式全域范围仍判\n // constrained(写全范围的作者意图是显式约束,不替用户臆测)。\n const isWildcardEquivalent = (field: string): boolean => field === '*' || field === '*/1'\n const domConstrained = !isWildcardEquivalent(domField)\n const dowConstrained = !isWildcardEquivalent(dowField)\n\n return { validMinute, validHour, validDom, validMonth, validDow, domConstrained, dowConstrained }\n}\n\n/**\n * Parse a single cron field into a set of valid integer values.\n *\n * Supported sub-syntax:\n * - * -> all values in [min, max]\n * - 5 -> single value\n * - *\\/15 or 5\\/15 -> step (start/step)\n * - 1-5 -> inclusive range\n * - 1,15,30 -> comma-separated list\n * - 1-10/2 -> range with step\n *\n * Throws on unsupported syntax (letters, L, W, ?, # etc.) or out-of-range values.\n */\nfunction parseField(field: string, min: number, max: number): FieldSet {\n validateFieldSyntax(field)\n\n const result = new Set<number>()\n const parts = field.split(',')\n\n for (const part of parts) {\n const { range, step } = splitStep(part)\n validateStep(step, part)\n\n if (range === '*') {\n for (let i = min; i <= max; i += step) {\n result.add(i)\n }\n } else if (range.includes('-')) {\n const [startStr, endStr] = range.split('-') as [string, string]\n const start = parseFieldInt(startStr, part)\n const end = parseFieldInt(endStr, part)\n if (start < min || end > max || start > end) {\n throw new Error(`Invalid cron field \"${field}\": range [${start},${end}] out of bounds [${min},${max}]`)\n }\n for (let i = start; i <= end; i += step) {\n result.add(i)\n }\n } else {\n const val = parseFieldInt(range, part)\n if (val < min || val > max) {\n throw new Error(\n `Invalid cron field \"${field}\": value ${val} out of range [${min},${max}]`,\n )\n }\n if (step > 1) {\n // \"N/step\" — start at N, step to max (vixie-cron semantics, e.g. 5/15 → 5,20,35,50)\n for (let i = val; i <= max; i += step) {\n result.add(i)\n }\n } else {\n result.add(val)\n }\n }\n }\n\n return result\n}\n\nfunction validateFieldSyntax(field: string): void {\n // Allow only digits, *, /, -, comma, and whitespace\n const cleaned = field.replace(/[\\d*/\\-,]/g, '')\n if (cleaned.length > 0) {\n throw new Error(`Invalid cron field \"${field}\": unsupported syntax`)\n }\n}\n\nfunction splitStep(part: string): { range: string; step: number } {\n const slashIdx = part.indexOf('/')\n if (slashIdx === -1) {\n return { range: part, step: 1 }\n }\n return {\n range: part.substring(0, slashIdx),\n step: parseInt(part.substring(slashIdx + 1), 10),\n }\n}\n\nfunction validateStep(step: number, part: string): void {\n if (isNaN(step) || step < 1) {\n throw new Error(`Invalid cron field: invalid step in \"${part}\"`)\n }\n}\n\nfunction parseFieldInt(s: string, contextPart: string): number {\n const n = parseInt(s, 10)\n if (isNaN(n)) {\n throw new Error(`Invalid cron field: expected number, got \"${s}\" in \"${contextPart}\"`)\n }\n return n\n}\n\nfunction nextInSet(set: FieldSet, current: number): number | null {\n let best: number | null = null\n for (const v of set) {\n if (v >= current && (best === null || v < best)) {\n best = v\n }\n }\n return best\n}\n\nfunction firstInSet(set: FieldSet): number {\n let best = Infinity\n for (const v of set) {\n if (v < best) best = v\n }\n return best\n}\n","import type { ScheduledTask } from './types'\n\n// schedule/src/registry.ts — In-memory schedule task registry (Map-backed CRUD).\nexport class ScheduleRegistry {\n private tasks: Map<string, ScheduledTask>\n\n constructor() {\n this.tasks = new Map()\n }\n\n add(task: ScheduledTask): void {\n if (this.tasks.has(task.id)) {\n throw new Error(`ScheduleRegistry: duplicate task id \"${task.id}\"`)\n }\n this.tasks.set(task.id, task)\n }\n\n remove(id: string): boolean {\n return this.tasks.delete(id)\n }\n\n update(\n id: string,\n patch: Partial<\n Pick<\n ScheduledTask,\n | 'enabled'\n | 'lastFiredAt'\n | 'nextFireAt'\n | 'maxDurationMs'\n | 'maxRetries'\n | 'lastFireFailed'\n | 'lastFireError'\n | 'lastFireCancelled'\n >\n >,\n ): boolean {\n const task = this.tasks.get(id)\n if (!task) return false\n\n if (patch.enabled !== undefined) task.enabled = patch.enabled\n if (patch.lastFiredAt !== undefined) task.lastFiredAt = patch.lastFiredAt\n if (patch.nextFireAt !== undefined) task.nextFireAt = patch.nextFireAt\n if (patch.maxDurationMs !== undefined) task.maxDurationMs = patch.maxDurationMs\n if (patch.maxRetries !== undefined) task.maxRetries = patch.maxRetries\n if (patch.lastFireFailed !== undefined) task.lastFireFailed = patch.lastFireFailed\n if (patch.lastFireError !== undefined) task.lastFireError = patch.lastFireError\n if (patch.lastFireCancelled !== undefined) task.lastFireCancelled = patch.lastFireCancelled\n\n return true\n }\n\n get(id: string): ScheduledTask | undefined {\n return this.tasks.get(id)\n }\n\n list(): ScheduledTask[] {\n return [...this.tasks.values()].sort((a, b) => a.nextFireAt - b.nextFireAt)\n }\n\n /**\n * Find tasks that are due for execution.\n * Returns all enabled tasks (recurring or one-shot) whose nextFireAt <= now.\n * The 'firing' guard is a scheduler concern (managed via a separate Set).\n *\n * RFC-423:eventTrigger 任务永不由时间\"到点\"——它们由进程内事件驱动(InProcessEventTrigger\n * arm 监听),不进 cron 轮询。此处排除,使 cron 轮询(tick)与事件监听(arm)\n * 两条路径互斥(D5)。\n */\n findDue(now: number): ScheduledTask[] {\n const due: ScheduledTask[] = []\n for (const task of this.tasks.values()) {\n if (task.eventTrigger) continue\n if (task.enabled && task.nextFireAt <= now) {\n due.push(task)\n }\n }\n due.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return due\n }\n\n count(): number {\n return this.tasks.size\n }\n\n has(id: string): boolean {\n return this.tasks.has(id)\n }\n}\n","/**\n * fire-target.ts —— RFC-406 D1/D2:FireTarget 贡献点接口 + 注册表。\n *\n * 投递目标(job / session / subscriber / 自定义)从 scheduler.ts fire() 的硬编码 if/else\n * 分支拆出为独立 handler。插件可注册新 handler(webhook、email 等),不修改 scheduler.ts。\n *\n * 设计参照 deepseek-harness capability-seams 模式:\n * - FireTarget 接口 = Service Definition(契约)\n * - JobFireTarget / SessionFireTarget / SubscriberFireTarget = 内置 Service Provider\n * - ScheduleCapability = Consumer(model 工具消费方)\n * - 插件注册的新 handler = 外部 Service Provider\n */\nimport type { CancelResult, ScheduledTask } from './types'\n\n// ---------------------------------------------------------------------------\n// FireTarget 接口\n// ---------------------------------------------------------------------------\n\nexport interface FireTarget {\n /** 唯一标识('job' | 'session' | 'plugin-service') */\n readonly type: string\n\n /** 判断此 handler 是否能处理该 task。注册顺序决定优先级——先注册的先匹配。 */\n canHandle(task: ScheduledTask): boolean\n\n /**\n * 执行投递。返回 FireResult——scheduler 不关心 handler 内部如何实现,\n * 只根据 outcome 决定后续行为(one-shot 清理 / recurring 记账)。\n */\n fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult>\n\n /**\n * fire() 退出保障钩子(M1 修复①):无论 fire 正常返回、抛错还是早退,executor 的\n * finally 块都会调用。handler 必须在此清理本次 fire 的运行态记账\n * (runningJobs / cancelRequested 等)——漏清会导致 cancel 意图残留、\n * recurring 任务被永久跳过、运行态 Map 无界泄漏。\n */\n cleanup?(taskId: string): void\n\n /**\n * 可选能力:支持取消正在运行的投递(job 路径实现)。\n * SchedulerService.cancel() 路由到当前 fire 该任务的 handler。\n */\n requestCancel?(taskId: string): CancelResult\n\n /** 可选能力:当前有并发运行态的 handler(job 路径)暴露运行中的 task id 集合。 */\n getRunningTaskIds?(): Set<string>\n\n /**\n * 可选能力:session 投递 handler 暴露\"创建任务时绑定的当前 sessionId\"端口,\n * scheduler.add() 经此填充 boundSessionId(RFC-406 D6)。\n */\n getSessionId?(): string\n}\n\n// ---------------------------------------------------------------------------\n// 上下文与结果\n// ---------------------------------------------------------------------------\n\nexport interface FireContext {\n /** 本次计划触发时刻(ticker 路径传入,runNow 用实际时刻)。用于 occurrenceId 派生。 */\n scheduledFor?: number\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\nexport interface FireResult {\n outcome: 'success' | 'failed'\n /** cancel 信号——JobFireTarget 在 cancel() 路径设置 */\n cancelled?: boolean\n error?: string\n /** job-specific 信号(JobFireTarget 附加,用于 runningJobsByTaskId 记账) */\n jobId?: string\n}\n\n// ---------------------------------------------------------------------------\n// FireTargetRegistry\n// ---------------------------------------------------------------------------\n\n/**\n * FireTarget handler 注册表。注册顺序决定匹配优先级——先注册的先匹配。\n *\n * 内置 handler 注册顺序:subscriber > session > job\n * (保持当前 fire() 的分支优先级:subscriber 优先于 target:session,job 是默认路径)。\n */\nexport class FireTargetRegistry {\n private handlers: FireTarget[] = []\n\n register(handler: FireTarget): void {\n this.handlers.push(handler)\n }\n\n find(task: ScheduledTask): FireTarget | undefined {\n return this.handlers.find((h) => h.canHandle(task))\n }\n}\n","/**\n * fire-executor.ts —— RFC-406 D4:Fire 执行器。\n *\n * 从 scheduler.ts fire() 入口拆出。持有 FireTargetRegistry,路由到匹配的 handler。\n * 处理 fire 前后记账:firing Set(防重复 fire)、registry 更新(lastFiredAt/lastFireFailed)、\n * one-shot 清理(成功删除 / 失败保留 / cancel 保留)、持久化(saveOne)。\n *\n * 原代码位置:scheduler.ts fire() 方法的外层逻辑(firing Set / finally 块 / one-shot 处理)。\n */\nimport type { ScheduleRegistry } from './registry'\nimport type { SchedulePersistence } from './schedule-persistence'\nimport type { FireTarget, FireContext } from './fire-target'\nimport { FireTargetRegistry } from './fire-target'\nimport type { FireLogger } from './fire-logger'\nimport type { TerminationReason } from './scheduler'\nimport type { ScheduledTask } from './types'\n\nexport interface FireExecutorDeps {\n registry: ScheduleRegistry\n persistence: SchedulePersistence\n fireLogger: FireLogger\n fireTargets: FireTarget[]\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /**\n * RFC-433 D1:任务终结咽喉点(`SchedulerService.terminate`)。one-shot 成功后\n * 必须经此而非自行 `registry.remove`——后者不退订事件监听,会导致任务已删除\n * 却仍被事件触发(RFC-433 §1.2 M2)。\n */\n onTerminate: (taskId: string, reason: TerminationReason) => void\n /**\n * RFC-433 D2:fire ownership 准入。cron 路径由 tick 的 renew/isOwner 把关,\n * 但**事件路径不经过 tick**——监听独立于 ticker 存活,`TickLoop.stop()` 停不掉它。\n * 唯一的 fire 入口现场问一次,比在 ownership 状态机每个转换点维护 arm/disarm\n * 对称性更可靠(无状态判定,不会漏)。\n */\n isFireOwner: () => boolean\n}\n\nexport class FireExecutor {\n private readonly registry: FireTargetRegistry = new FireTargetRegistry()\n private readonly firing: Set<string> = new Set()\n private readonly deps: FireExecutorDeps\n\n constructor(deps: FireExecutorDeps) {\n this.deps = deps\n // 注册 handler——顺序决定优先级:subscriber > session > job\n for (const handler of deps.fireTargets) {\n this.registry.register(handler)\n }\n }\n\n /** 当前正在 fire 的 task id 集合(防重复 fire)。 */\n has(taskId: string): boolean {\n return this.firing.has(taskId)\n }\n\n /** 标记 task 正在 fire(同步,在 await 之前调用)。 */\n addFiring(taskId: string): void {\n this.firing.add(taskId)\n }\n\n /**\n * 执行一次 fire:路由到匹配的 FireTarget handler,处理结果记账。\n *\n * 调用方(tick / runNow)必须在调用前将 taskId 加入 firing Set(addFiring)。\n * finally 块负责移除,并调用 handler.cleanup() 清理 handler 侧运行态\n * (M1 修复①——拆分前该清理在旧 fire() 的 finally 里,拆分时被遗漏)。\n */\n async fire(task: ScheduledTask, scheduledFor?: number): Promise<void> {\n // ── 准入闸门(RFC-433 D2)──\n // fire() 是所有触发路径的唯一汇合点(tick / runNow / 事件 arm 回调),\n // 两道准入放这里才能覆盖全部路径:\n // · enabled —— 此前只在 registry.findDue 判,事件路径整条链零检查(M1)\n // · isFireOwner —— 此前只在 tick 判,事件监听独立于 ticker 存活(M7)\n // 上游的 findDue/tick 检查保留:它们负责\"要不要提出候选\"(筛选与触发 recovery),\n // 这里负责\"候选能不能真的执行\"(准入)。职责不同,不是重复。\n if (!task.enabled || !this.deps.isFireOwner()) {\n this.firing.delete(task.id)\n return\n }\n\n const ctx: FireContext = {\n scheduledFor,\n now: this.deps.now,\n logger: this.deps.logger,\n }\n\n let handler: FireTarget | undefined\n try {\n handler = this.registry.find(task)\n if (!handler) {\n this.deps.fireLogger.write(task.id, `no fire target handler for task (subscriber=${!!task.subscriber}, target=${task.target ?? 'default'})`)\n // RFC-435 D3:no-handler 的 **one-shot** 任务必须走终态——tick 在 fire 之前已\n // advance nextFireAt(tick-loop.ts),不终结会无限复燃:每天到点记一条日志、\n // 永不消失。标 lastFireFailed 保留(经咽喉点,用户可见可删),recurring 照旧\n // 每日日志(那是设计内:任务保留、不投递、不崩溃)。\n this.handleOneShotCompletion(task, { outcome: 'failed', error: 'no fire target handler (headless mode)' })\n return\n }\n\n const result = await handler.fire(task, ctx)\n\n // 成功记账(三态统一:session 成功不落盘,其余 saveOne)\n if (result.outcome === 'success' && !result.cancelled) {\n this.deps.registry.update(task.id, {\n lastFiredAt: this.deps.now(),\n lastFireFailed: false,\n lastFireError: undefined,\n })\n if (task.target !== 'session') {\n this.deps.persistence.saveOne(task)\n }\n }\n\n // one-shot 终态处理(原 scheduler.ts fire() finally 块语义,M1 修复②恢复统一保障):\n // success → 删除;cancelled → 标记 lastFireCancelled 保留;failed → 标记 lastFireFailed 保留。\n // session 分支不再早退——一次性 session 任务同样按此处理,否则成功后不删、\n // 失败不标,tick 已 advance 的 nextFireAt 会让它按 cron 点无限复燃。\n this.handleOneShotCompletion(task, result)\n } catch (err) {\n this.deps.logger?.error(`[schedules] fire failed for task ${task.id}:`, err)\n this.deps.fireLogger.write(task.id, `error: ${err instanceof Error ? err.message : String(err)}`)\n // handler 抛错时 one-shot 按失败终态处理(对齐 HEAD 版 catch→finally 行为)\n this.handleOneShotCompletion(task, { outcome: 'failed', error: err instanceof Error ? err.message : String(err) })\n } finally {\n handler?.cleanup?.(task.id)\n this.firing.delete(task.id)\n }\n }\n\n /**\n * one-shot 任务 fire 完成后的终态记账(fire() 内部调用,外部无调用方)。\n *\n * 成功 → 从 registry 和 store 删除。\n * cancel → 标记 lastFireCancelled,保留(不删除)。\n * 失败 → 标记 lastFireFailed + lastFireError,保留(review S6)。\n */\n private handleOneShotCompletion(task: ScheduledTask, result: { outcome: 'success' | 'failed'; cancelled?: boolean; error?: string }): void {\n if (task.recurring) return\n\n if (result.outcome === 'success') {\n // RFC-433 D1:经咽喉点终结——直接 registry.remove 不退订事件监听,\n // 会让已完成的 one-shot 事件任务在此后每次同类事件都重新投递(M2)。\n this.deps.onTerminate(task.id, 'one-shot-complete')\n } else if (result.cancelled) {\n this.deps.registry.update(task.id, {\n lastFireCancelled: true,\n lastFireError: undefined,\n })\n this.deps.persistence.saveOne(task)\n } else {\n this.deps.registry.update(task.id, {\n lastFireFailed: true,\n lastFireError: result.error,\n })\n this.deps.persistence.saveOne(task)\n }\n }\n}\n","/**\n * fire-logger.ts —— RFC-406 D4:Fire 日志写入/清理/读取。\n *\n * 从 scheduler.ts 拆出。每个 fire 结果写到 ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log,\n * 每任务保留最新 maxFireLogsPerTask 条。best-effort——错误静默吞(日志非关键)。\n */\nimport { writeFileSync, readFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nimport { OTTO_HOME, isShadowModeEnabled } from '@x-otto/env'\n\nimport type { ScheduleConfig } from './schedule-config'\n\nexport class FireLogger {\n constructor(private config: ScheduleConfig) {}\n\n /**\n * 写一行 fire log 到 ${OTTO_HOME}/schedule-logs/<taskId>-<epoch>.log,然后清理超出\n * maxFireLogsPerTask 的旧日志。best-effort——错误静默吞。\n *\n * RFC-345:影子态零磁盘写入——fire log 是纯本机持久化产物,影子进程跳过。\n */\n write(taskId: string, status: string): void {\n if (isShadowModeEnabled()) return\n const logDir = resolve(OTTO_HOME, 'schedule-logs')\n const logFile = resolve(logDir, `${taskId}-${Date.now()}.log`)\n try {\n mkdirSync(logDir, { recursive: true })\n writeFileSync(logFile, `${new Date().toISOString()} ${status}\\n`, 'utf-8')\n this.prune(logDir, taskId)\n } catch {\n // best-effort logging\n }\n }\n\n /** 保留最新 maxFireLogsPerTask 条日志文件。 */\n private prune(logDir: string, taskId: string): void {\n try {\n const prefix = `${taskId}-`\n const files = readdirSync(logDir)\n .filter((f) => f.startsWith(prefix) && f.endsWith('.log'))\n .sort() // filenames embed epoch ms — lexical sort == chronological\n const excess = files.length - this.config.maxFireLogsPerTask\n if (excess <= 0) return\n for (const f of files.slice(0, excess)) {\n try {\n unlinkSync(resolve(logDir, f))\n } catch {\n // best-effort — a single stale/locked file must not block the rest\n }\n }\n } catch {\n // best-effort — directory listing failure is non-critical\n }\n }\n\n /**\n * 读取最近的 fire log 条目。返回最新 maxEntries 条,每条 { timestamp, status }。\n */\n read(taskId: string, maxEntries: number = 20): Array<{ timestamp: string; status: string }> {\n const logDir = resolve(OTTO_HOME, 'schedule-logs')\n const prefix = `${taskId}-`\n try {\n const files = readdirSync(logDir)\n .filter((f) => f.startsWith(prefix) && f.endsWith('.log'))\n .sort()\n .reverse() // newest first\n const entries: Array<{ timestamp: string; status: string }> = []\n for (let i = 0; i < files.length && entries.length < maxEntries; i++) {\n try {\n const content = readFileSync(resolve(logDir, files[i]!), 'utf-8').trim()\n const spaceIdx = content.indexOf(' ')\n const timestamp = spaceIdx > 0 ? content.slice(0, spaceIdx) : content\n const status = spaceIdx > 0 ? content.slice(spaceIdx + 1) : ''\n entries.push({ timestamp, status })\n } catch {\n // skip unreadable file\n }\n }\n return entries\n } catch {\n return []\n }\n }\n}\n","/**\n * schedule-config.ts —— RFC-406 D4:常量配置化。\n *\n * 之前散落在 scheduler.ts 顶部的硬编码常量,提取为可注入的配置接口。\n * 不同部署场景(单用户 vs 团队共享、高频 vs 低频)可调整参数,不需改源码。\n *\n * **不纳入本接口的常量**(刻意游离,非遗漏):\n * - `fire-ownership.ts` 的 `STALE_MS`(5 分钟)——本地锁过期阈值,有不变量约束\n * (必须超过 tick renew 间隔,否则锁在 renew 周期间被误判过期),调错会致\n * 多进程重复 fire。它是 fire-ownership 的领域常量,不是可调运维参数。\n * - `remote-fire-ownership.ts` 的 `HEARTBEAT_MS`(30s)/ `STALE_MS`(180s)——\n * 远程租约心跳与过期,有 1:6 比例约束(过期 > 6 倍心跳,防网络抖动误夺锁)。\n * 调错同样致多终端重复 fire 或锁饥饿。两者已通过模块 export 暴露\n * (`REMOTE_FIRE_OWNERSHIP_HEARTBEAT_MS` / `REMOTE_FIRE_OWNERSHIP_STALE_MS`),\n * 不进 ScheduleConfig 以免暗示可随意调。\n */\n\nexport interface ScheduleConfig {\n /** 最大任务数 */\n maxTasks: number\n /** recurring 任务过期阈值(超过此时间未触发则清除) */\n recurringEvictionMs: number\n /** recurring 任务 jitter 窗口 */\n recurringJitterWindowMs: number\n /** one-shot 任务 jitter 窗口 */\n oneShotJitterWindowMs: number\n /** 默认单次执行超时 */\n defaultMaxDurationMs: number\n /** 默认重试次数 */\n defaultMaxRetries: number\n /** ticker 间隔 */\n tickIntervalMs: number\n /** needs_input 硬上限倍数 */\n needsInputMaxExtensionFactor: number\n /** 每任务最大 fire log 条数 */\n maxFireLogsPerTask: number\n /** owner 从 store 重新同步的间隔(tick 数) */\n reloadIntervalTicks: number\n /** recovery 模式重试间隔 */\n recoveryRetryIntervalMs: number\n}\n\nexport const DEFAULT_SCHEDULE_CONFIG: ScheduleConfig = {\n maxTasks: 50,\n recurringEvictionMs: 7 * 24 * 60 * 60 * 1000, // 7 days\n recurringJitterWindowMs: 30 * 60 * 1000, // 30 minutes\n oneShotJitterWindowMs: 90 * 1000, // 90 seconds\n defaultMaxDurationMs: 300_000, // 5 minutes\n defaultMaxRetries: 0,\n tickIntervalMs: 1000,\n needsInputMaxExtensionFactor: 3,\n maxFireLogsPerTask: 20,\n reloadIntervalTicks: 30,\n recoveryRetryIntervalMs: 30_000,\n}\n\n// ---------------------------------------------------------------------------\n// Jitter — RFC-406 终局自检:收敛 scheduler.ts / tick-loop.ts 两处重复的 djb2 hash。\n// ---------------------------------------------------------------------------\n\n/**\n * 确定性 per-task jitter(djb2 hash of taskId),跨重启可复现。\n * recurring 任务用 30 分钟窗口(分散周期负载);one-shot 用 90 秒窗口(RFC-087 §D3)。\n */\nexport function applyJitter(config: ScheduleConfig, taskId: string, baseTime: Date, recurring: boolean): number {\n let hash = 0\n for (let i = 0; i < taskId.length; i++) {\n hash = ((hash << 5) - hash) + taskId.charCodeAt(i)\n hash |= 0\n }\n const window = recurring ? config.recurringJitterWindowMs : config.oneShotJitterWindowMs\n const jitter = Math.abs(hash) % window\n return baseTime.getTime() + jitter\n}\n","/**\n * tick-loop.ts —— RFC-406 D4:Tick 循环。\n *\n * 从 scheduler.ts tick() 拆出。1 秒间隔心跳:renew ownership → 检查 owner →\n * reloadFromStore(每 N tick)→ 过期清理 → fire due tasks。\n *\n * 原代码位置:scheduler.ts tick() + reloadFromStore() + eviction 逻辑。\n */\nimport { parseCron } from './cron-parser'\nimport { applyJitter } from './schedule-config'\nimport type { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { SchedulePersistence } from './schedule-persistence'\nimport type { FireOwnership } from './fire-ownership'\nimport type { FireExecutor } from './fire-executor'\nimport type { ScheduleConfig } from './schedule-config'\nimport type { TerminationReason } from './scheduler'\nimport type { ScheduledTask } from './types'\n\nexport interface TickLoopDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n persistence: SchedulePersistence\n ownership: FireOwnership\n fireExecutor: FireExecutor\n config: ScheduleConfig\n now: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /**\n * ownership 丢失时的回调(SchedulerService 进入 recovery mode)。\n */\n onOwnershipLost: () => void\n /**\n * RFC-433 D1:任务终结咽喉点(滞留驱逐用)。不可自行 `registry.remove`——\n * 那样不退订事件监听。\n */\n onTerminate: (taskId: string, reason: TerminationReason) => void\n /**\n * RFC-433 D1:只从内存移除、保留 store 快照(差量同步用——store 里已经没有它了,\n * 是别的进程删的,再发一次 delete 是多余 IO)。同样必须退订。\n */\n onEvictFromMemory: (taskId: string) => void\n}\n\nexport class TickLoop {\n private readonly deps: TickLoopDeps\n private ticker: ReturnType<typeof setInterval> | null = null\n private tickInProgress = false\n private tickCount = 0\n\n constructor(deps: TickLoopDeps) {\n this.deps = deps\n }\n\n /** 启动 tick 循环。 */\n start(): void {\n if (this.ticker) return\n this.ticker = setInterval(() => {\n void this.tick()\n }, this.deps.config.tickIntervalMs)\n this.ticker.unref()\n }\n\n /** 停止 tick 循环。 */\n stop(): void {\n if (this.ticker) {\n clearInterval(this.ticker)\n this.ticker = null\n }\n }\n\n /**\n * 手动触发一次 tick(测试用)。\n *\n * **同时供测试与调度器复用**:`lifecycle-regression.test.ts` 用它精确控制\"8 天后\n * 跑一次 tick\"的场景(M3 系列回归),而 `SchedulerService` 也保留同名方法用于\n * 相同目的——两者都走同一个 `tick()` 内部方法,避免平行实现漂移。\n */\n async tickOnce(): Promise<void> {\n await this.tick()\n }\n\n // ── Internal ──\n\n private async tick(): Promise<void> {\n if (this.tickInProgress) return\n this.tickInProgress = true\n\n try {\n // A3: heartbeat the ownership lock\n await this.deps.ownership.renew()\n\n // S4: if renew() detected a takeover, stop firing + enter recovery\n if (!this.deps.ownership.isOwner()) {\n this.stop()\n this.deps.onOwnershipLost()\n return\n }\n\n // S5: periodically re-sync from store\n this.tickCount++\n if (this.tickCount % this.deps.config.reloadIntervalTicks === 0) {\n await this.reloadFromStore()\n }\n\n const now = this.deps.now()\n\n // Evict recurring tasks whose next fire is >7 days stale.\n //\n // RFC-433 D3:滞留判据是 `nextFireAt`,**只对 cron 任务有语义**。事件驱动任务的\n // nextFireAt 是 add() 时的 cron 占位符产物且永不更新(findDue 排除它们,只有\n // fire 路径才 advance),所以每个 recurring 事件任务的 nextFireAt 原地不动,\n // 第 7 天 100% 越过阈值被误删——任务从 UI 消失,用户既看不见也删不掉。\n //\n // TODO(RFC-434): 事件任务因此失去了这道\"最终安全阀\"——永远等不到事件的任务会\n // 永久占用 maxTasks 配额。正确的清理判据是 createdAt/lastFiredAt 而非 nextFireAt,\n // 属于新规则设计,随 trigger/delivery 可辨识联合一起做。\n for (const task of this.deps.registry.list()) {\n if (task.eventTrigger) continue\n if (task.recurring && now - task.nextFireAt > this.deps.config.recurringEvictionMs) {\n this.deps.onTerminate(task.id, 'stale-evicted')\n }\n }\n\n // Fire due tasks\n const due = this.deps.registry.findDue(now)\n for (const task of due) {\n if (this.deps.fireExecutor.has(task.id)) continue\n\n // S6: failed/canceled one-shot guard — no auto-refire\n if (!task.recurring && (task.lastFireFailed || task.lastFireCancelled)) continue\n\n // Advance nextFireAt before dispatching (MF-1); persist\n const scheduledFor = task.nextFireAt\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n const jittered = applyJitter(this.deps.config, task.id, nextBase, task.recurring)\n this.deps.registry.update(task.id, { nextFireAt: jittered })\n this.deps.persistence.saveOne(task)\n\n this.deps.fireExecutor.addFiring(task.id)\n void this.deps.fireExecutor.fire(task, scheduledFor)\n }\n } finally {\n this.tickInProgress = false\n }\n }\n\n private async reloadFromStore(): Promise<void> {\n let tasks: ScheduledTask[]\n try {\n tasks = await this.deps.store.loadAllTasks()\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to reload schedules from store:', err)\n return\n }\n\n const storeIds = new Set(tasks.map((t) => t.id))\n\n for (const task of tasks) {\n if (this.deps.fireExecutor.has(task.id)) continue\n if (!task.recurring && task.lastFiredAt != null && !this.deps.registry.get(task.id)) {\n void this.deps.persistence.deleteOne(task.id).catch((err) => {\n this.deps.logger?.error(`[schedules] Failed to delete zombie one-shot ${task.id}:`, err)\n })\n continue\n }\n const existing = this.deps.registry.get(task.id)\n if (!existing) {\n try { this.deps.registry.add(task) } catch { /* raced */ }\n } else {\n existing.enabled = task.enabled\n existing.name = task.name\n existing.prompt = task.prompt\n existing.cronExpression = task.cronExpression\n existing.maxDurationMs = task.maxDurationMs\n existing.maxRetries = task.maxRetries\n }\n }\n\n for (const task of this.deps.registry.list()) {\n if (task.target === 'session') continue\n if (!storeIds.has(task.id) && !this.deps.fireExecutor.has(task.id)) {\n // RFC-433 D1:只从内存移除(store 里已经没有它了),但仍须退订。\n this.deps.onEvictFromMemory(task.id)\n }\n }\n }\n}\n","/**\n * schedule-persistence.ts —— RFC-406 终局自检:持久化咽喉点收敛。\n *\n * 重构后 saveOne/deleteOne/flushAll 的 session-local 跳过 + 错误吞逻辑在 scheduler.ts /\n * fire-executor.ts / tick-loop.ts 三处重复。收敛为单一真源——所有写盘调用共享此对象,\n * session-local 任务永不落盘(RFC-357 D2)这条防线只有一个判断点。\n */\nimport type { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { ScheduledTask } from './types'\n\nexport interface SchedulePersistenceDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\nexport class SchedulePersistence {\n constructor(private deps: SchedulePersistenceDeps) {}\n\n /**\n * 持久化单个任务(fire-and-forget)。RFC-357 D2:session-local 任务永不落盘——\n * 这是唯一判断点,所有调用方共享此拦截。\n */\n saveOne(task: ScheduledTask): void {\n if (task.target === 'session') return\n void this.deps.store.saveTask(task).catch((err) => {\n this.deps.logger?.error(`[schedules] Failed to save schedule ${task.id}:`, err)\n })\n }\n\n /**\n * 删除单个任务快照(awaited——必须确认删除以防 store reload 时复活为僵尸)。\n * session-local 任务从不落盘,删除时直接视为成功(不发起多余 IO)。\n */\n async deleteOne(id: string): Promise<boolean> {\n const task = this.deps.registry.get(id)\n if (task?.target === 'session') return true\n try {\n return await this.deps.store.deleteTask(id)\n } catch (err) {\n this.deps.logger?.error(`[schedules] Failed to delete schedule ${id}:`, err)\n return false\n }\n }\n\n /**\n * 把所有内存任务写盘(stop 时调用;bounded,无 diff-delete)。\n * RFC-357 D2:跳过 session-local 任务——否则进程正常退出时会把它们写进 store,\n * 下次 start() 从 store 加载后在新进程的新 sessionRef 上\"复活\",击穿\"不落盘\"防线。\n */\n async flushAll(tasks: ScheduledTask[]): Promise<void> {\n try {\n for (const task of tasks) {\n if (task.target === 'session') continue\n await this.deps.store.saveTask(task)\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to flush schedules:', err)\n }\n }\n}\n","// schedule/src/scheduler.ts — RFC-406: Schedule lifecycle orchestrator.\n// Lifecycle: start→tick→fire→stop. Delegates to TickLoop + FireExecutor + FireLogger.\n// Fire logs → ${OTTO_HOME}/schedule-logs/. Clock: Date.now() by default, injectable for testing.\n\nimport { parseCron } from './cron-parser'\nimport { ScheduleRegistry } from './registry'\nimport type { ScheduleStore } from './schedule-store'\nimport type { FireOwnership } from './fire-ownership'\nimport { FireExecutor } from './fire-executor'\nimport { FireLogger } from './fire-logger'\nimport { TickLoop } from './tick-loop'\nimport { SchedulePersistence } from './schedule-persistence'\nimport { DEFAULT_SCHEDULE_CONFIG, applyJitter, type ScheduleConfig } from './schedule-config'\nimport type { FireTarget } from './fire-target'\nimport { type EventDrivenTrigger } from './trigger'\nimport type { CancelResult, ScheduledTask, ScheduledTaskInput } from './types'\n\n// ---------------------------------------------------------------------------\n// Task termination\n// ---------------------------------------------------------------------------\n\n/**\n * RFC-433 D1:任务终结的来源。仅用于诊断/日志语义——终结动作本身对三者一致\n * (退订 → 删快照 → 删 registry),区分来源是为了让调用点自我说明。\n */\nexport type TerminationReason =\n /** 用户经 `/schedule remove`、面板或 `schedule_delete` 工具删除。 */\n | 'user-removed'\n /** one-shot 任务成功执行完毕(FireExecutor 终态处理)。 */\n | 'one-shot-complete'\n /** recurring cron 任务 7 天未触发,被 TickLoop 判为滞留清除。 */\n | 'stale-evicted'\n\n// ---------------------------------------------------------------------------\n// Dependencies\n// ---------------------------------------------------------------------------\n\nexport interface SchedulerDeps {\n registry: ScheduleRegistry\n store: ScheduleStore\n ownership: FireOwnership\n /**\n * FireTarget handler 列表。内置 handler(job/session/subscriber)由消费方构造并传入,\n * SchedulerService 不自动注册——消费方完全控制 handler 集合和优先级。\n *\n * 如果传入空数组,fire() 会找不到 handler 并记日志跳过——不会崩溃。\n */\n fireTargets: FireTarget[]\n /**\n * RFC-423:事件驱动 Trigger 列表(如 InProcessEventTrigger)。缺省空数组——只有 cron\n * 触发(cron 任务由 TickLoop 轮询驱动,无需消费方传入)。消费方传入事件 trigger\n * 即启用事件触发能力。\n */\n triggers?: EventDrivenTrigger[]\n /** 可选配置覆盖(常量配置化,RFC-406 D4) */\n config?: Partial<ScheduleConfig>\n now?: () => number\n /**\n * `debug` 可选——生产装配(`interactive-schedule.ts`)注入的 logger 只有 `error`;\n * 有 `debug` 的实现能拿到任务终结的来源(user-removed / one-shot-complete /\n * stale-evicted),排查\"任务怎么没了\"时不必猜。\n */\n logger?: {\n error: (msg: string | Error, ...args: unknown[]) => void\n debug?: (msg: string, ...args: unknown[]) => void\n }\n}\n\n// ---------------------------------------------------------------------------\n// SchedulerService — lifecycle orchestrator\n// ---------------------------------------------------------------------------\n\nexport class SchedulerService {\n private readonly deps: SchedulerDeps\n private readonly registry: ScheduleRegistry\n private readonly persistence: SchedulePersistence\n private readonly fireExecutor: FireExecutor\n private readonly fireLogger: FireLogger\n private readonly tickLoop: TickLoop\n private readonly config: ScheduleConfig\n private recoveryTimer: ReturnType<typeof setInterval> | null = null\n /** RFC-435 D4:从未当过 owner 的非 owner 进程的 acquire 重试循环(TUI 释放锁后本进程可接管)。 */\n private ensureOwnershipTimer: ReturnType<typeof setInterval> | null = null\n private running = false\n /** RFC-423:事件驱动 trigger 列表(消费方传入)。 */\n private readonly triggers: EventDrivenTrigger[]\n /** RFC-423:已 arm 的事件任务 → 退订函数(remove/stop 时调用,防监听泄漏)。 */\n private readonly armedTriggers = new Map<string, () => void>()\n\n constructor(deps: SchedulerDeps) {\n this.deps = deps\n this.registry = deps.registry\n this.config = { ...DEFAULT_SCHEDULE_CONFIG, ...deps.config }\n this.fireLogger = new FireLogger(this.config)\n // RFC-406 终局自检:持久化咽喉点单一真源,三个消费者(scheduler/executor/tick)共享。\n this.persistence = new SchedulePersistence({\n registry: deps.registry,\n store: deps.store,\n logger: deps.logger,\n })\n this.fireExecutor = new FireExecutor({\n registry: deps.registry,\n persistence: this.persistence,\n fireLogger: this.fireLogger,\n fireTargets: deps.fireTargets,\n now: deps.now ?? Date.now,\n logger: deps.logger,\n // RFC-433 D1:one-shot 终结经咽喉点,而非自行 registry.remove(会漏退订)。\n onTerminate: (taskId, reason) => { this.terminate(taskId, reason) },\n // RFC-433 D2:fire 准入的 ownership 一侧。事件路径不经过 tick,\n // tick 里的 ownership 检查管不到它,必须在唯一的 fire 入口独立把关。\n isFireOwner: () => this.deps.ownership.isOwner(),\n })\n this.tickLoop = new TickLoop({\n registry: deps.registry,\n store: deps.store,\n persistence: this.persistence,\n ownership: deps.ownership,\n fireExecutor: this.fireExecutor,\n config: this.config,\n now: deps.now ?? Date.now,\n logger: deps.logger,\n onOwnershipLost: () => this.startRecovery(),\n // RFC-433 D1:eviction 与差量同步两条删除路径同样经咽喉点。\n onTerminate: (taskId, reason) => { this.terminate(taskId, reason) },\n onEvictFromMemory: (taskId) => { this.evictFromMemory(taskId) },\n })\n this.triggers = deps.triggers ?? []\n }\n\n // ── Public: ownership status ──\n\n isFireOwner(): boolean {\n return this.deps.ownership.isOwner()\n }\n\n fireOwnerFailureReason(): 'occupied' | 'network-error' | undefined {\n return this.deps.ownership.lastFailureReason?.()\n }\n\n // ── Lifecycle ──\n\n /** Load persisted tasks and start the ticker (if fire owner). */\n async start(): Promise<void> {\n if (this.running) return\n\n const isOwner = await this.deps.ownership.tryAcquire()\n if (!isOwner) {\n try {\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to load schedules (read-only mode):', err)\n }\n // RFC-435 D4:非 owner 也必须周期尝试 acquire——否则另一进程(TUI)释放锁后\n // 本进程(daemon)永远接管不了,凌晨任务静默不跑,直到进程重启。\n // (此前 start() 非 owner 分支加载一次就休眠;startRecovery 只在\"曾是 owner 后\n // 丢失\"时触发,对\"从未 owner\"的 daemon 无效。)\n this.startEnsureOwnership()\n return\n }\n\n try {\n const now = this.deps.now?.() ?? Date.now()\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n if (task.recurring && task.nextFireAt < now) {\n try {\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n task.nextFireAt = applyJitter(this.config, task.id, nextBase, true)\n } catch {\n /* keep stored nextFireAt if cron no longer parses */\n }\n }\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to load schedules:', err)\n }\n\n // RFC-423:为加载的事件驱动任务建立监听(cron 任务由 tickLoop 轮询,无需 arm)。\n for (const task of this.registry.list()) {\n this.armIfEventDriven(task)\n }\n\n this.running = true\n this.tickLoop.start()\n }\n\n // ── Task termination (RFC-433 D1) ──\n\n /**\n * 任务终结的**唯一入口**(RFC-433 D1 / R2)。三条删除路径——用户 `remove()`、\n * one-shot 完成、7 天滞留驱逐——全部经此,否则事件监听会泄漏:任务从 registry\n * 消失后,`armedTriggers` 里的退订函数仍在,而 arm 回调闭包捕获的是 **task 对象\n * 引用本身**(见 `armIfEventDriven`),registry 里有没有它根本不影响 fire。\n *\n * **三步顺序不可调换(R2b 不变量)**:`persistence.deleteOne()` 内部要\n * `registry.get(id)` 判 session-local(`schedule-persistence.ts:37`)——registry\n * 先删会让它读不到 task、判不出 session-local,对本就不落盘的任务白发一次 store IO。\n */\n private terminate(taskId: string, reason: TerminationReason): boolean {\n // 未知 id 不产生任何副作用(保持 remove() 的既有语义:不存在则 false,不发 store IO)。\n // disarm 仍先做一次——它对未 arm 的 id 是 no-op,但能兜住\"已泄漏\"的历史脏状态。\n this.disarmIfArmed(taskId) // ① 退订事件监听\n if (!this.registry.has(taskId)) return false\n void this.persistence.deleteOne(taskId) // ② 删快照(须先于 ③,见上)\n const removed = this.registry.remove(taskId) // ③ 从内存 registry 移除\n this.deps.logger?.debug?.(`[schedules] terminated ${taskId} (${reason})`)\n return removed\n }\n\n /**\n * 只从内存移除、**保留** store 快照——`reloadFromStore` 的差量同步用(store 里\n * 本来就已经没有它了,是别的进程删的;再发一次 delete 是多余 IO)。\n * 同样必须退订,理由同 `terminate()`。\n */\n private evictFromMemory(taskId: string): boolean {\n this.disarmIfArmed(taskId)\n return this.registry.remove(taskId)\n }\n\n /** Clear ticker, release ownership, and persist current state. */\n async stop(): Promise<void> {\n // RFC-435 D4:先清 acquire 重试循环——非 owner + timer 在跑时若被首行守卫短路,\n // timer 会泄漏(进程常驻则每 30s 空转一次 acquire)。\n this.stopEnsureOwnership()\n if (!this.running && !this.deps.ownership.isOwner()) return\n\n this.stopRecovery()\n this.tickLoop.stop()\n this.disarmAll()\n\n this.running = false\n await this.deps.ownership.release()\n await this.persistence.flushAll(this.registry.list())\n }\n\n // ── Recovery ──\n\n private startRecovery(): void {\n if (this.recoveryTimer) return\n this.deps.logger?.error('[schedules] Lost fire ownership — entering recovery mode')\n this.recoveryTimer = setInterval(() => {\n void this.recoveryAttempt()\n }, this.config.recoveryRetryIntervalMs)\n this.recoveryTimer.unref()\n }\n\n private async recoveryAttempt(): Promise<void> {\n try {\n const acquired = await this.deps.ownership.tryAcquire()\n if (acquired && this.deps.ownership.isOwner()) {\n this.stopRecovery()\n this.running = true\n // RFC-433 D3b:重获 ownership 后补 arm——与 start() 的 owner 分支对齐。\n // 不补的话事件任务在一次锁抖动后**永久假死**:cron 任务随 ticker 自动恢复,\n // 事件任务的监听却可能从未建立(非 owner 期间 start() 只加载不 arm)。\n // armIfEventDriven 内部幂等(先 disarm 再 arm),重复调用安全。\n for (const task of this.registry.list()) this.armIfEventDriven(task)\n this.tickLoop.start()\n this.deps.logger?.error('[schedules] Re-acquired fire ownership — recovery complete')\n }\n } catch {\n // tryAcquire failures during recovery are expected — suppress.\n }\n }\n\n private stopRecovery(): void {\n if (this.recoveryTimer) {\n clearInterval(this.recoveryTimer)\n this.recoveryTimer = null\n }\n }\n\n // ── RFC-435 D4: never-owner acquire retry ──\n\n /**\n * 非 owner 进程的低频 acquire 重试。TUI 先开持锁 → daemon 后起(acquire 失败,\n * 本循环待机)→ TUI 关闭释放锁 → 本循环在下一次 tick 接管。锁的原子性\n * (O_EXCL / 服务端 CAS)保证任一时刻只有一个 owner——重试不会造成双主。\n */\n private startEnsureOwnership(): void {\n if (this.ensureOwnershipTimer) return\n this.ensureOwnershipTimer = setInterval(() => {\n void this.ensureOwnershipAttempt()\n }, this.config.recoveryRetryIntervalMs)\n this.ensureOwnershipTimer.unref()\n }\n\n private async ensureOwnershipAttempt(): Promise<void> {\n try {\n const acquired = await this.deps.ownership.tryAcquire()\n if (acquired && this.deps.ownership.isOwner()) {\n this.stopEnsureOwnership()\n // 与 start() owner 分支对齐:重新 reload(待机期间可能有新任务写入 store)+\n // arm 事件任务 + 启动 ticker。\n try {\n const tasks = await this.deps.store.loadAllTasks()\n for (const task of tasks) {\n try { this.registry.add(task) } catch { /* skip duplicate */ }\n }\n } catch (err) {\n this.deps.logger?.error('[schedules] Failed to reload schedules after acquiring ownership:', err)\n }\n for (const task of this.registry.list()) this.armIfEventDriven(task)\n this.running = true\n this.tickLoop.start()\n this.deps.logger?.error('[schedules] Acquired fire ownership (was waiting for release)')\n }\n } catch {\n // tryAcquire failures during wait are expected — suppress.\n }\n }\n\n private stopEnsureOwnership(): void {\n if (this.ensureOwnershipTimer) {\n clearInterval(this.ensureOwnershipTimer)\n this.ensureOwnershipTimer = null\n }\n }\n\n // ── Public CRUD ──\n\n add(input: ScheduledTaskInput): ScheduledTask {\n if (this.registry.count() >= this.config.maxTasks) {\n throw new Error(`Schedule limit reached (max ${this.config.maxTasks}). Remove some tasks before adding more.`)\n }\n\n const wantsSession = input.target === 'session' && !input.subscriber\n\n if (wantsSession && !this.deps.ownership.isOwner()) {\n throw new Error(this.describeSessionOwnershipRefusal())\n }\n\n // RFC-423 D4 闸门2:事件驱动任务默认拒绝 job 投递(会产生新 job → 高死循环风险)。\n // 事件触发只放行 subscriber/session 目标(不产生新 job)。这也匹配\"注入对话继续\"诉求。\n if (input.eventTrigger && !input.subscriber && input.target !== 'session') {\n throw new Error(\n 'Event-triggered tasks must deliver to a session (target:\"session\") or a plugin subscriber, '\n + 'not an isolated job — job delivery from an event trigger risks a self-firing loop (RFC-423 D4).',\n )\n }\n\n const id = crypto.randomUUID().slice(0, 8)\n const now = this.deps.now?.() ?? Date.now()\n const nextBase = parseCron(input.cronExpression, new Date(now + 60_000))\n const nextFireAt = applyJitter(this.config, id, nextBase, input.recurring)\n\n const task: ScheduledTask = {\n id,\n name: input.name,\n prompt: input.prompt,\n cronExpression: input.cronExpression,\n enabled: true,\n recurring: input.recurring,\n origin: input.origin,\n createdAt: now,\n nextFireAt,\n maxDurationMs: input.maxDurationMs ?? this.config.defaultMaxDurationMs,\n maxRetries: input.maxRetries ?? this.config.defaultMaxRetries,\n ...(input.subscriber ? { subscriber: input.subscriber } : {}),\n ...(wantsSession ? { target: 'session' as const, boundSessionId: this.getSessionIdForTask() } : {}),\n ...(input.eventTrigger ? { eventTrigger: input.eventTrigger } : {}),\n ...(input.modelId ? { modelId: input.modelId } : {}),\n ...(input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {}),\n }\n\n this.registry.add(task)\n this.persistence.saveOne(task)\n this.armIfEventDriven(task)\n return task\n }\n\n remove(id: string): boolean {\n return this.terminate(id, 'user-removed')\n }\n\n update(\n id: string,\n patch: Partial<Pick<ScheduledTask, 'name' | 'prompt' | 'cronExpression' | 'enabled'>>,\n ): boolean {\n const task = this.registry.get(id)\n if (!task) return false\n\n if (patch.name !== undefined) task.name = patch.name\n if (patch.prompt !== undefined) task.prompt = patch.prompt\n if (patch.enabled !== undefined) task.enabled = patch.enabled\n if (patch.cronExpression !== undefined) {\n task.cronExpression = patch.cronExpression\n const now = this.deps.now?.() ?? Date.now()\n const nextBase = parseCron(task.cronExpression, new Date(now + 60_000))\n task.nextFireAt = applyJitter(this.config, task.id, nextBase, task.recurring)\n }\n\n this.persistence.saveOne(task)\n return true\n }\n\n list(): ScheduledTask[] {\n return this.registry.list()\n }\n\n getRunningTaskIds(): Set<string> {\n // 委托给实现了 getRunningTaskIds() 的 handler(job 路径)\n for (const handler of this.deps.fireTargets) {\n if (handler.getRunningTaskIds) {\n return handler.getRunningTaskIds()\n }\n }\n return new Set()\n }\n\n readFireLogs(taskId: string, maxEntries: number = 20): Array<{ timestamp: string; status: string }> {\n return this.fireLogger.read(taskId, maxEntries)\n }\n\n runNow(id: string): void {\n const task = this.registry.get(id)\n if (!task) {\n throw new Error(`Schedule task \"${id}\" not found`)\n }\n if (!task.enabled) {\n throw new Error(`Schedule task \"${id}\" is disabled`)\n }\n // RFC-435 D1:非 owner 进程(serve/daemon 与交互会话并存时的只读加载方)不能 fire——\n // 否则 fireExecutor 准入静默吞掉,用户以为执行了。显式抛错(面板/toast 可提示\"另一\n // 进程持锁\")。D4 的非 owner acquire 重试只负责\"锁释放后接管\",不代表 runNow 可执行。\n if (!this.deps.ownership.isOwner()) {\n throw new Error(`Schedule task \"${id}\" cannot be run now: another process holds the fire ownership`)\n }\n if (this.fireExecutor.has(task.id)) {\n throw new Error(`Schedule task \"${id}\" is already firing`)\n }\n this.fireExecutor.addFiring(task.id)\n void this.fireExecutor.fire(task)\n }\n\n cancel(taskId: string): CancelResult {\n const task = this.registry.get(taskId)\n if (!task) return { ok: false, reason: 'task_not_found' }\n\n // 委托给实现了 requestCancel() 的 handler(job 路径)\n for (const handler of this.deps.fireTargets) {\n if (handler.requestCancel) {\n return handler.requestCancel(taskId)\n }\n }\n return { ok: false, reason: 'not_running' }\n }\n\n // ── RFC-423: event-trigger arm/disarm ──\n\n /**\n * 若任务是事件驱动(eventTrigger 存在),用匹配的 EventDrivenTrigger arm 监听。\n * fire 回调复用既有 fireExecutor 路径——Trigger 只决定\"何时\",FireTarget 决定\"投哪\"。\n *\n * 死循环闸门2(D4):事件驱动任务默认拒绝 JobFireTarget 投递——只有 subscriber/session\n * 目标(不产生新 job)才放行。配 job 会产生新 job → 高死循环风险,在此拒绝而非静默。\n */\n private armIfEventDriven(task: ScheduledTask): void {\n if (!task.eventTrigger) return\n const trigger = this.triggers.find((t) => t.canHandle(task))\n if (!trigger) {\n this.deps.logger?.error(\n `[schedules] event-trigger task ${task.id} has no matching trigger (source=${task.eventTrigger.source}) — will never fire`,\n )\n return\n }\n // 已 arm 的先退订(幂等,防重复 arm 泄漏)。\n this.disarmIfArmed(task.id)\n const disarm = trigger.arm(task, (t) => {\n if (this.fireExecutor.has(t.id)) return\n this.fireExecutor.addFiring(t.id)\n void this.fireExecutor.fire(t)\n })\n this.armedTriggers.set(task.id, disarm)\n }\n\n private disarmIfArmed(taskId: string): void {\n const disarm = this.armedTriggers.get(taskId)\n if (disarm) {\n disarm()\n this.armedTriggers.delete(taskId)\n }\n }\n\n private disarmAll(): void {\n for (const disarm of this.armedTriggers.values()) disarm()\n this.armedTriggers.clear()\n }\n\n /**\n * 非 owner 进程拒绝创建 session-local 任务时的诊断文案(RFC-357 D2 R3 的执行面)。\n *\n * **拒绝本身不放宽**——它防的是\"任务出现在列表里却永远不触发\"的静默失效:非 owner\n * 进程不跑 ticker,任务又 durable:false 不落盘,owner 进程 loadAllTasks 也看不见它。\n * 本方法只解决\"用户知道被拒了,但不知道该怎么办\":补上**谁持有**(本地实现可从锁\n * 文件解出 pid/host)与**多久能好**(RFC-435 D4 的非 owner acquire 重试会在锁释放后\n * 自动接管,无需重启进程)。\n *\n * 三态退化:远程实现无本地 pid 概念 → 无主体文案;`network-error` 时不能说\"另一终端\n * 持有\"(可能压根没连上服务端,让用户去找一个不存在的第二终端是更糟的误导)——这条\n * 三态区分沿用 `handlers/schedule.ts:105-109` 既有 readOnlyHint 范式,不另造一套判据。\n */\n private describeSessionOwnershipRefusal(): string {\n const base = 'Cannot create a session-local scheduled task: this terminal does not hold fire ownership.'\n\n if (this.deps.ownership.lastFailureReason?.() === 'network-error') {\n return `${base} Could not reach the remote schedule service — check network/persistenced connectivity, then retry.`\n }\n\n const owner = this.deps.ownership.describeOwner?.()\n const holder = owner?.pid !== undefined\n ? ` It is currently held by another otto process (pid ${owner.pid}${owner.host !== undefined ? ` on ${owner.host}` : ''}).`\n : ' It is currently held by another otto process for this workspace.'\n\n return `${base}${holder}`\n + ' Create the task in that terminal instead, or close it — this process retries acquiring'\n + ` ownership every ${Math.round(this.config.recoveryRetryIntervalMs / 1000)}s and will take over automatically once the lease is released (no restart needed).`\n }\n\n // ── Internal: session id for task creation ──\n\n /**\n * session-local 任务创建时绑定的 sessionId。\n * RFC-406 D6:从 SessionFireTarget handler 的 getSessionId() 端口读取(旧 SchedulerDeps.getSessionId\n * 语义迁入此 handler)。add() 用它来设置 boundSessionId。\n *\n * 找不到 SessionFireTarget(消费方未装配 session 投递)时回退空字符串——此时 add() 的\n * wantsSession 分支不会命中(session 任务无 handler 可 fire),boundSessionId 也无意义。\n */\n private getSessionIdForTask(): string {\n for (const handler of this.deps.fireTargets) {\n if (handler.getSessionId) {\n return handler.getSessionId()\n }\n }\n return ''\n }\n}\n","// schedule/src/schedule-store.ts — Snapshot-per-id storage via Persistence<ScheduledTask>.\n// RFC-169 D1: partitioned by workspaceKey. Fail-open: read errors → [].\nimport { createPersistence, type Persistence, type Snapshot } from '@x-otto/persistence'\nimport { OTTO_HOME, isShadowModeEnabled } from '@x-otto/env'\nimport { resolve } from 'node:path'\n\nimport type { ScheduledTask } from './types'\n\nconst SNAPSHOT_VERSION = 1\n\n/** Per-workspace schedules directory: `${OTTO_HOME}/schedules/<workspaceKey>/`. */\nfunction schedulesDir(workspaceKey: string): string {\n return resolve(OTTO_HOME, 'schedules', workspaceKey)\n}\n\nexport interface ScheduleStore {\n saveTask(task: ScheduledTask): Promise<void>\n loadAllTasks(): Promise<ScheduledTask[]>\n deleteTask(id: string): Promise<boolean>\n}\n\nexport interface ScheduleStoreOptions {\n persistence?: Persistence<ScheduledTask>\n /** Logger (injected, replaces bare console.error). */\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n}\n\n/**\n * Create a ScheduleStore backed by FilePersistence in ${OTTO_HOME}/schedules/<workspaceKey>/.\n *\n * `workspaceKey` is required (RFC-169 D1) — every call site must explicitly state which\n * workspace's schedules it wants, so a caller can never silently fall back to a shared\n * global path. Remote mode (RemoteScheduleStore, RFC-169 D3) is a separate implementation\n * selected by the caller based on `--session-url`, not by this factory.\n */\nexport function createScheduleStore(workspaceKey: string, options: ScheduleStoreOptions = {}): ScheduleStore {\n // RFC-345:影子模式(OTTO_SHADOW)下默认持久化切到 memory 后端——`/schedule add`\n // 等交互路径只写内存、零磁盘落盘,符合影子态「写静默吞」(§D3)语义。调用方显式\n // 注入 options.persistence 时不受影子态控制。\n //\n // 已知折衷:影子态下 `/schedule list` 只含本进程内存任务,既有磁盘任务不可见。\n // 读穿透叠加 merge 的复杂度不值得为一个短暂影子会话引入,故不做。\n const store =\n options.persistence ??\n (isShadowModeEnabled()\n ? createPersistence<ScheduledTask>({ type: 'memory' })\n : createPersistence<ScheduledTask>({ type: 'file', baseDir: schedulesDir(workspaceKey) }))\n const logger = options.logger\n\n return {\n async saveTask(task: ScheduledTask): Promise<void> {\n const snapshot: Snapshot<ScheduledTask> = {\n version: SNAPSHOT_VERSION,\n id: task.id,\n data: task,\n metadata: {\n createdAt: task.createdAt,\n updatedAt: Date.now(),\n tags: ['schedule'],\n },\n }\n await store.save(snapshot)\n },\n\n async loadAllTasks(): Promise<ScheduledTask[]> {\n let ids: string[]\n try {\n ids = await store.list()\n } catch (err) {\n logger?.error('[schedules] Failed to list schedule snapshots:', err)\n return []\n }\n\n const tasks: ScheduledTask[] = []\n for (const id of ids) {\n try {\n const snapshot = await store.load(id)\n if (snapshot) {\n tasks.push(snapshot.data)\n }\n } catch (err) {\n logger?.error(`[schedules] Failed to load schedule \"${id}\":`, err)\n // skip corrupted record, continue loading rest\n }\n }\n\n tasks.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return tasks\n },\n\n async deleteTask(id: string): Promise<boolean> {\n try {\n return await store.delete(id)\n } catch (err) {\n logger?.error(`[schedules] Failed to delete schedule \"${id}\":`, err)\n return false\n }\n },\n }\n}\n","/**\n * remote-schedule-store.ts —— RFC-169 D3: 远程模式的 ScheduleStore 实现。\n *\n * `extends RemotePersistence<ScheduledTask>`(@x-otto/persistence 既有抽象基类,已有\n * RemoteSessionPersistence/PanelState 两个先例)——复用 wsKey 路径分区、鉴权 header 注入、\n * 超时/连接失败错误文案,不重新实现 HTTP 客户端。`ScheduleStore` 接口形状(saveTask/\n * loadAllTasks/deleteTask)与本地 `createScheduleStore` 一致,`setupScheduleService` 可\n * 按 remoteSessionConfigured 无痛切换两种实现(RFC-169 §4 重要事项规则 6)。\n */\nimport { RemotePersistence } from '@x-otto/persistence'\nimport type { RemotePersistenceOptions } from '@x-otto/persistence'\n\nimport type { ScheduleStore } from './schedule-store'\nimport type { ScheduledTask } from './types'\n\nconst SNAPSHOT_VERSION = 1\n\ninterface ScheduleSnapshot {\n version: number\n id: string\n data: ScheduledTask\n}\n\nclass RemoteScheduleStoreImpl extends RemotePersistence<ScheduleSnapshot> implements ScheduleStore {\n protected override get serviceLabel(): string {\n return 'schedule 远程服务'\n }\n protected override get endpointFlag(): string {\n return '--session-url'\n }\n\n protected extractId(snapshot: ScheduleSnapshot): string {\n return snapshot.id\n }\n\n async saveTask(task: ScheduledTask): Promise<void> {\n await this.save({ version: SNAPSHOT_VERSION, id: task.id, data: task })\n }\n\n /**\n * M3 里程碑完成审核记录的已知架构成本:list()+load() 是 1+N 次 HTTP 请求(先拿 id 列表,\n * 再逐个拉快照),非单次批量端点。可接受——schedule 任务集合天然小(MAX_TASKS=50,\n * scheduler.ts),且 loadAllTasks 只在 SchedulerService.start()/定期 reloadFromStore()\n * 调用,不在 tick 热路径。真要优化需要 persistenced 加批量读端点,超出本 RFC 范围。\n */\n async loadAllTasks(): Promise<ScheduledTask[]> {\n const ids = await this.list()\n const tasks: ScheduledTask[] = []\n for (const id of ids) {\n const snapshot = await this.load(id)\n if (snapshot) tasks.push(snapshot.data)\n }\n tasks.sort((a, b) => a.nextFireAt - b.nextFireAt)\n return tasks\n }\n\n async deleteTask(id: string): Promise<boolean> {\n return this.delete(id)\n }\n}\n\nexport interface RemoteScheduleStoreOptions {\n /**\n * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸\n * origin,该值本身是完整资源路径(对齐 `otto serve` 后端约定)。内部用\n * `new URL(sessionUrl).origin` 提取协议+主机+端口,再拼接 `/api/storage/schedules`——\n * 与 `remote-fire-ownership.ts` 的同一提取逻辑保持一致,不能直接拿 sessionUrl 当前缀用。\n */\n sessionUrl: string\n getAuth: () => Promise<{ token: string }>\n fetch?: typeof globalThis.fetch\n timeoutMs?: number\n wsKey?: string | (() => string | undefined)\n}\n\n/**\n * Create a ScheduleStore backed by the persistenced `/api/storage/schedules` namespace's\n * generic storage route (`storage.ts`, `namespace='schedules'`) — distinct from the\n * lease sub-route (`schedule-lease.ts`, `/api/schedules/...`) which handles fire-ownership\n * arbitration only.\n */\nexport function createRemoteScheduleStore(options: RemoteScheduleStoreOptions): ScheduleStore {\n const origin = new URL(options.sessionUrl).origin\n const remoteOptions: RemotePersistenceOptions<ScheduleSnapshot> = {\n baseUrl: `${origin}/api/storage/schedules`,\n getAuth: options.getAuth,\n fetch: options.fetch ?? globalThis.fetch,\n timeoutMs: options.timeoutMs ?? 10_000,\n wsKey: options.wsKey,\n }\n return new RemoteScheduleStoreImpl(remoteOptions)\n}\n","/**\n * schedule-lease-migrations.ts —— RFC-169 D3a: schedule_lease 表的独立 migration 段.\n *\n * `schedule_lease` is a schedule-domain table (server-arbitrated fire-ownership lease,\n * RFC-169 D3), not a generic blob-snapshot table — it must not join `BLOB_MIGRATIONS`\n * (@x-otto/persistence's generic storage kernel schema), per the \"generic storage kernel\n * does not hardcode domain schema\" principle already established by RFC-074 M5-02b\n * (session tables were carved out of the shared migration set for the same reason).\n *\n * Consumed by `persistenced-app.ts` via `acquireDb(leaseDbPath, wal, SCHEDULE_LEASE_MIGRATIONS)`\n * — deliberately a SEPARATE db file from the blob-snapshot `storage.db` (acquireDb's\n * fail-fast guard rejects injecting two different migration sets into the same db path,\n * `sqlite-db-cache.ts:134-140`).\n */\nimport type { Migration } from '@x-otto/persistence'\n\nexport const SCHEDULE_LEASE_MIGRATIONS: Migration[] = [\n {\n version: 1,\n up: (db) => {\n db.exec(`\n CREATE TABLE IF NOT EXISTS schedule_lease (\n ws_key TEXT PRIMARY KEY,\n token TEXT NOT NULL,\n renewed_at INTEGER NOT NULL\n );\n `)\n },\n },\n]\n","// schedule/src/fire-ownership.ts — Per-fire lease acquisition (async tryAcquire).\n// RFC-169 D1: partitioned by workspaceKey.\n// RFC-171 D2/D5: lease format token:pid:hostname + liveness probe.\nimport {\n closeSync,\n mkdirSync,\n openSync,\n writeSync,\n readFileSync,\n statSync,\n renameSync,\n rmSync,\n unlinkSync,\n utimesSync,\n} from 'node:fs'\nimport { resolve } from 'node:path'\nimport { randomUUID } from 'node:crypto'\n\nimport {\n OTTO_HOME,\n currentHostname,\n decodeLeaseToken,\n encodeLeaseToken,\n isProcessAlive,\n isShadowModeEnabled,\n} from '@x-otto/env'\n\n/** Per-workspace lock file: `${OTTO_HOME}/schedule-owner-<workspaceKey>.lock`. */\nfunction lockFilePath(workspaceKey: string): string {\n return resolve(OTTO_HOME, `schedule-owner-${workspaceKey}.lock`)\n}\n\nconst STALE_MS = 5 * 60_000 // 5-minute stale threshold (must exceed renew interval)\n\nexport interface FireOwnership {\n tryAcquire(): Promise<boolean>\n\n renew(): Promise<void>\n\n release(): Promise<void>\n\n lastFailureReason?(): 'occupied' | 'network-error' | undefined\n\n /**\n * 当前锁持有者的可读描述(诊断用,非语义判定)。返回 `undefined` 表示无法确定\n * (旧格式锁文件、锁已消失、或远程实现——服务端仲裁没有本地 pid 概念)。\n *\n * 为什么是可选端口:`FireOwnership` 是本地 O_EXCL 与远程 HTTP 双实现的公共接口\n * (RFC-169 D3/D3b),\"谁持有\"只在本地实现里有确切答案。消费方必须按可选处理,\n * 拿不到就退回不带主体的提示文案,不得据此做任何准入判定。\n */\n describeOwner?(): { pid?: number; host?: string } | undefined\n\n /** Whether this process currently holds the lock (verified against the token). */\n isOwner(): boolean\n}\n\n/**\n * Create a local token-based O_EXCL FireOwnership, scoped to `workspaceKey` (RFC-169 D1).\n * Only the lock holder runs the ticker and fires tasks.\n */\nexport function createLocalFireOwnership(workspaceKey: string, clock?: () => number): FireOwnership {\n const now = clock ?? Date.now\n const token = randomUUID()\n const LOCK_FILE = lockFilePath(workspaceKey)\n const myHostname = currentHostname()\n let owned = false\n\n /** Create the lockfile with our token (encoded as `token:pid:hostname`, RFC-171 D2). Returns false if it already exists. */\n const writeLock = (): boolean => {\n try {\n // OTTO_HOME may not exist yet on a fresh install — otto doesn't self-create it\n // ahead of first write. mkdirSync recursive is a cheap no-op once it does.\n mkdirSync(OTTO_HOME, { recursive: true })\n const fd = openSync(LOCK_FILE, 'wx') // O_EXCL\n try {\n writeSync(fd, encodeLeaseToken(token, process.pid, myHostname))\n } finally {\n closeSync(fd)\n }\n owned = true\n return true\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err\n return false\n }\n }\n\n /** Read + decode the lockfile currently written, or null if unreadable. */\n const readDecoded = (): { token: string; pid?: number; host?: string } | null => {\n try {\n const raw = readFileSync(LOCK_FILE, 'utf-8').trim()\n return decodeLeaseToken(raw)\n } catch {\n return null\n }\n }\n\n /** Read only the token field (fencing comparisons). */\n const readToken = (): string | null => readDecoded()?.token ?? null\n\n /** RFC-171 D2: confirmed-dead check — same hostname + pid not alive. Cross-host/malformed → false (safe fallback to stale-timeout). */\n const isConfirmedDead = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return !isProcessAlive(decoded.pid)\n }\n\n /** RFC-171 D2: confirmed-alive check — same hostname + pid alive. Used to reject takeover even past STALE_MS. */\n const isConfirmedAlive = (decoded: { pid?: number; host?: string }): boolean => {\n if (decoded.pid === undefined || decoded.host === undefined) return false\n if (decoded.host !== myHostname) return false\n return isProcessAlive(decoded.pid)\n }\n\n // RFC-169 D3/D3b: FireOwnership methods are Promise-returning so this local O_EXCL\n // implementation and the remote HTTP one (remote-fire-ownership.ts) satisfy the same\n // interface. All work below is genuinely synchronous fs I/O — wrapped in an\n // immediately-resolved Promise, not an actual async boundary.\n const tryAcquireSync = (): boolean => {\n if (owned) return true\n // RFC-345:影子态不参与 fire ownership 竞选(§D9)——不建 schedule-owner-*.lock、\n // 不报失败原因。影子会话加载任务列表只读展示,fire 由真实 owner(或影子会话内部的\n // 其它路径)承担。这样影子进程既零写盘,也不会因竞选失败而降级走 STALE_MS 等\n // 既有的健康检查路径。\n if (isShadowModeEnabled()) return false\n if (writeLock()) return true\n\n // Lock exists — try liveness probe first (RFC-171 D2), fall back to mtime stale-timeout.\n const decoded = readDecoded()\n let mtime: number\n try {\n mtime = statSync(LOCK_FILE).mtimeMs\n } catch {\n // Vanished between calls — retry create.\n return writeLock()\n }\n\n if (decoded !== null && isConfirmedAlive(decoded)) {\n // Confirmed same-host owner is alive — reject even past STALE_MS (hung-process\n // trade-off; see module header + session-write-lease.ts D2 for the rationale).\n return false\n }\n\n const confirmedDead = decoded !== null && isConfirmedDead(decoded)\n // B9 修正(评审建议优化):stale 判定用注入 clock(缺省 Date.now 行为零变化)——\n // 测试可注入假时钟获得确定性;消费方(scheduler deps.now 同款模式)可传同一时钟。\n if (!confirmedDead && now() - mtime <= STALE_MS) return false\n\n // Stale or confirmed dead: atomically claim it away, then recreate with our token.\n const claimPath = `${LOCK_FILE}.stale-${randomUUID()}`\n try {\n renameSync(LOCK_FILE, claimPath)\n rmSync(claimPath, { force: true })\n } catch {\n return false // raced another taker — let the winner keep it\n }\n return writeLock()\n }\n\n return {\n async tryAcquire(): Promise<boolean> {\n return tryAcquireSync()\n },\n\n async renew(): Promise<void> {\n if (!owned) return\n // Only touch the lock if it is still ours (guards against a takeover).\n if (readToken() !== token) {\n owned = false\n return\n }\n try {\n const t = new Date()\n utimesSync(LOCK_FILE, t, t)\n } catch {\n owned = false // lock vanished under us\n }\n },\n\n async release(): Promise<void> {\n if (!owned) return\n // Only unlink if the lock is still ours (never delete a successor's lock).\n if (readToken() === token) {\n try {\n unlinkSync(LOCK_FILE)\n } catch {\n // best-effort cleanup\n }\n }\n owned = false\n },\n\n isOwner(): boolean {\n return owned && readToken() === token\n },\n\n /**\n * 读当前锁文件、解出持有者 pid/host(纯诊断)。锁不存在/旧格式裸 token/字段非法\n * 时返回 undefined——`decodeLeaseToken` 已对 pid 做合法性校验并安全降级。\n */\n describeOwner(): { pid?: number; host?: string } | undefined {\n const decoded = readDecoded()\n if (decoded === null) return undefined\n if (decoded.pid === undefined && decoded.host === undefined) return undefined\n return { ...(decoded.pid !== undefined ? { pid: decoded.pid } : {}), ...(decoded.host !== undefined ? { host: decoded.host } : {}) }\n },\n }\n}\n","// schedule/src/remote-fire-ownership.ts — Remote fire ownership via HTTP storage host.\n// RFC-169 D3c: 30s heartbeat, 180s stale.\n// D3d: fallback to tryAcquire on renew failure.\n// F1: in-flight dedup via inFlightAcquire.\nimport { randomUUID } from 'node:crypto'\n\nimport type { FireOwnership } from './fire-ownership'\n\nconst HEARTBEAT_MS = 30_000\nconst STALE_MS = 180_000\n\nexport interface RemoteFireOwnershipOptions {\n /**\n * `--session-url` 的原始值(如 `http://host:3001/api/storage/sessions`)——**不是**裸\n * origin。该值本身是完整资源路径(对齐 `otto serve` 后端约定,见 RFC-146 §1.3 两条远程\n * 产品线对照表),本构造函数内部用 `new URL(sessionUrl).origin` 提取协议+主机+端口,\n * 再拼接 `/api/schedules/...` lease 路径——不能直接拿 `sessionUrl` 当 baseUrl 使用。\n */\n sessionUrl: string\n wsKey: string\n getAuth: () => Promise<{ token: string }>\n fetch?: typeof globalThis.fetch\n timeoutMs?: number\n now?: () => number\n}\n\ntype LeaseAction = 'acquire' | 'renew' | 'release'\n\n/**\n * F2 (RFC-169 终局 review 2026-07-15): `callLease` 此前把\"服务端明确拒绝\"(HTTP 200 但\n * `acquired:false`/`renewed:false`)和\"请求本身没打到/没打通\"(网络错误/超时/非 2xx HTTP\n * 状态)统一折叠成同一个 `null` 返回值——消费方因此无法区分\"另一个终端真的持有租约\"和\n * \"我这边压根没连上服务端、连判定结果都没拿到\",导致 TUI 只读提示在网络故障时显示一句\n * 具体但错误的诊断(\"另一终端持有触发权\")。现在用一个可辨识返回类型显式区分三态:\n * `{ ok:true, data }`(服务端已应答,无论 acquired/renewed 是 true 还是 false,这本身\n * 就是一次成功的判定往返)、`{ ok:false, reason:'network-error' }`(请求本身失败)。\n * `response.ok===false`(如 403/500)也归为 network-error——那不是\"lease 被占用\"这个\n * 业务语义上的拒绝,是请求层面出了问题(鉴权/服务端错误),同样不该说成\"被占用\"。\n *\n * 已知精度局限(本次 review 复核发现,暂不展开为独立修复):`network-error` 这个名字\n * 把\"真连不上网络\"和\"鉴权失败(403 token 不对)\"两种不同性质的请求层失败合并成一类\n * ——两者的正确排查方向不同(前者查网络连通性,后者查 token 配置),但当前 TUI 提示\n * 文案统一说\"连接远程服务失败\",对鉴权失败场景不够精确。之所以不现在拆出独立的\n * `'auth-error'` 三态:① token 由 `getAuth()` 单一配置源注入,生产场景下 401/403 出现\n * 概率极低(配置错误通常在首次连接就会暴露,不会隐蔽到运行时才触发);② 拆分收益不足以\n * 覆盖新增一个枚举值分支的代码/文档维护成本,属于本次 review 判断\"可保留观察\"的一项。\n */\ntype LeaseCallResult =\n | { ok: true; data: Record<string, unknown> }\n | { ok: false; reason: 'network-error' }\n\n/** Create a server-arbitrated FireOwnership backed by persistenced's lease endpoints. */\nexport function createRemoteFireOwnership(options: RemoteFireOwnershipOptions): FireOwnership {\n const token = randomUUID()\n const fetchImpl = options.fetch ?? globalThis.fetch\n const timeoutMs = options.timeoutMs ?? 10_000\n const baseUrl = new URL(options.sessionUrl).origin\n const now = options.now ?? Date.now\n let owned = false\n /** Last time an actual HTTP renew/acquire heartbeat was sent (throttling — see header). */\n let lastHeartbeatAt = 0\n /** F1: dedupe concurrent tryAcquire() calls onto one in-flight request/state-write. */\n let inFlightAcquire: Promise<boolean> | undefined\n /** F2: reason for the most recent non-owner outcome, for TUI diagnostics. */\n let lastFailureReason: 'occupied' | 'network-error' | undefined\n\n const callLease = async (action: LeaseAction): Promise<LeaseCallResult> => {\n const auth = await options.getAuth()\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const response = await fetchImpl(\n `${baseUrl}/api/schedules/${encodeURIComponent(options.wsKey)}/lease/${action}`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${auth.token}`,\n },\n body: JSON.stringify({ token }),\n signal: controller.signal,\n },\n )\n if (!response.ok) return { ok: false, reason: 'network-error' }\n return { ok: true, data: (await response.json()) as Record<string, unknown> }\n } catch {\n // Network error / timeout / abort — treated as \"did not succeed\", never thrown.\n // The scheduler's tick loop tolerates transient failures and retries next heartbeat.\n return { ok: false, reason: 'network-error' }\n } finally {\n clearTimeout(timeout)\n }\n }\n\n const tryAcquire = (): Promise<boolean> => {\n // F1: if a tryAcquire() is already in flight, share its result instead of firing a\n // second concurrent HTTP request whose (possibly stale-ordered) response could\n // clobber `owned` after the first call already set it. Both callers get the same\n // outcome, which is the only sane semantics for \"try to acquire\" called concurrently.\n if (inFlightAcquire) return inFlightAcquire\n\n const promise = (async (): Promise<boolean> => {\n lastHeartbeatAt = now()\n const result = await callLease('acquire')\n if (!result.ok) {\n owned = false\n lastFailureReason = 'network-error'\n return false\n }\n const acquired = result.data['acquired'] === true\n owned = acquired\n lastFailureReason = acquired ? undefined : 'occupied'\n return acquired\n })()\n\n inFlightAcquire = promise\n promise.finally(() => {\n if (inFlightAcquire === promise) inFlightAcquire = undefined\n })\n return promise\n }\n\n return {\n tryAcquire,\n\n async renew(): Promise<void> {\n // Throttle: SchedulerService.tick() calls renew() every second, but a real HTTP\n // heartbeat only needs to fire every HEARTBEAT_MS (30s, D3c) — everything in between\n // is a free no-op. The very first call after construction always fires (lastHeartbeatAt\n // starts at 0), matching the local implementation's \"renew is safe to call immediately\".\n if (now() - lastHeartbeatAt < HEARTBEAT_MS) return\n\n if (!owned) {\n // D3d: not currently owner (lost it, or never acquired) — every heartbeat is an\n // acquire attempt so a healed network partition or an expired competitor's lease\n // can be reclaimed without waiting for a full process restart.\n await tryAcquire()\n return\n }\n lastHeartbeatAt = now()\n const result = await callLease('renew')\n if (!result.ok) {\n // F2: renew failed at the transport level (not a server-side \"superseded\"\n // rejection) — still downgrade to non-owner (D3d semantics unchanged), but record\n // the accurate reason so the TUI doesn't claim \"another terminal holds it\".\n owned = false\n lastFailureReason = 'network-error'\n return\n }\n const renewed = result.data['renewed'] === true\n if (!renewed) {\n // D3d: lost the lease (superseded, or this renew call itself failed/timed out).\n // Don't distinguish \"genuinely superseded\" from \"network blip\" here — both cases\n // downgrade to non-owner and let the next heartbeat's tryAcquire() sort it out.\n owned = false\n lastFailureReason = 'occupied'\n } else {\n lastFailureReason = undefined\n }\n },\n\n async release(): Promise<void> {\n if (!owned) return\n await callLease('release')\n owned = false\n },\n\n isOwner(): boolean {\n return owned\n },\n\n lastFailureReason(): 'occupied' | 'network-error' | undefined {\n return owned ? undefined : lastFailureReason\n },\n }\n}\n\nexport { HEARTBEAT_MS as REMOTE_FIRE_OWNERSHIP_HEARTBEAT_MS, STALE_MS as REMOTE_FIRE_OWNERSHIP_STALE_MS }\n","/**\n * job-fire-target.ts —— RFC-406 D3:JobFireTarget handler。\n *\n * 从 scheduler.ts fire() 的 job 分支拆出。到点派发 AgentJob(隔离 worktree 执行),\n * 监控完成状态,失败重试(maxRetries 次),支持 cancel。\n *\n * 原代码位置:scheduler.ts fire() 的 else 分支 + waitForCompletion() + cancel() 的\n * runningJobsByTaskId/cancelRequested 记账。\n *\n * **cancel 交互**:SchedulerService.cancel() 调用此 handler 的 requestCancel(),\n * 设置 cancelRequested 标志。fire() 循环在每次 attempt 前检查此标志。\n */\nimport type { AgentJobRegistry } from '@x-otto/runtime'\nimport type { ThinkingLevel } from '@x-otto/interchange'\n\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { CancelResult, ScheduledTask } from '../types'\n\nexport interface JobFireTargetDeps {\n jobRegistry: AgentJobRegistry\n /**\n * Dispatch an agent job. Mirrors AgentJobService.start() shape:\n * synchronously returns a record with at least { id }.\n */\n startJob: (input: {\n title: string\n prompt: string\n sessionId: string\n origin: 'user' | 'main'\n /** RFC-438:per-job 指定模型与思考档位(task.modelId/thinkingLevel 透传)。 */\n model?: { id: string; thinkingLevel?: ThinkingLevel }\n }) => { id: string }\n /**\n * RFC-435 D1:绑定会话 id 端口。serve/daemon 无人值守装配可缺省——\n * job-only 模式无会话,fire 时 sessionId 用空串(无会话绑定,notify 不投递)。\n */\n getSessionId?: () => string\n /** RFC-230:重试之间的固定延迟(ms)。默认 0。 */\n retryBackoffMs?: number\n /** 测试可注入假 sleep 避免真实等待。 */\n sleep?: (ms: number) => Promise<void>\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n /** needs_input 硬上限倍数(对齐 ScheduleConfig.needsInputMaxExtensionFactor,默认 3)。 */\n needsInputMaxExtensionFactor?: number\n}\n\ninterface RunningJobRecord {\n jobId: string\n fireToken: number\n attempt: number\n}\n\nexport class JobFireTarget implements FireTarget {\n readonly type = 'job'\n\n private readonly runningJobsByTaskId: Map<string, RunningJobRecord> = new Map()\n private readonly cancelRequested: Set<string> = new Set()\n private _nextFireToken = 0\n\n constructor(private deps: JobFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n // job 是默认路径——当 task 既无 subscriber 也非 target:session 时命中\n return !task.subscriber && task.target !== 'session'\n }\n\n async fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult> {\n let outcome: 'success' | 'failed' = 'failed'\n let outcomeCancelled = false\n let lastError: string | undefined\n const fireToken = ++this._nextFireToken\n\n const origin: 'user' | 'main' = task.origin === 'model' ? 'main' : 'user'\n // RFC-435 D1:无人值守(无 getSessionId 端口)时用空串——job 无会话绑定,\n // 下游 notify 无匹配消费者即安全旁路(同 fail-open 纪律)。\n const sessionId = this.deps.getSessionId?.() ?? ''\n\n for (let attempt = 0; attempt <= task.maxRetries; attempt++) {\n if (this.cancelRequested.has(task.id)) {\n this.deps.fireLogger.write(task.id, `canceled by user (attempt ${attempt + 1})`)\n outcomeCancelled = true\n break\n }\n\n // RFC-438 评审阻断②:startJob 同步抛错(如 per-job 模型在 fire 时才失效)必须\n // 折叠为一次失败 attempt——否则跳出循环,maxRetries 被跳过、recurring 不标\n // lastFireFailed(= 每晚静默失败仅一行 fire log)。\n let job: { id: string }\n try {\n job = this.deps.startJob({\n title: task.name,\n prompt: task.prompt,\n sessionId,\n origin,\n ...(task.modelId ? { model: { id: task.modelId, thinkingLevel: task.thinkingLevel } } : {}),\n })\n } catch (err) {\n lastError = err instanceof Error ? err.message : String(err)\n this.deps.fireLogger.write(task.id, `startJob failed (attempt ${attempt + 1}): ${lastError}`)\n if (attempt < task.maxRetries) {\n const backoffMs = this.deps.retryBackoffMs ?? 0\n const sleep = this.deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n await sleep(backoffMs)\n }\n continue\n }\n\n this.runningJobsByTaskId.set(task.id, { jobId: job.id, fireToken, attempt })\n\n const result = await this.waitForCompletion(job.id, task.maxDurationMs, ctx)\n\n if (result === 'success') {\n // review I3: when the job produced a diff (status 'ready'), surface the pending review.\n const finishedJob = this.deps.jobRegistry.get(job.id)\n const pendingApply = finishedJob?.status === 'ready' && !!finishedJob.diff?.trim()\n this.deps.fireLogger.write(\n task.id,\n pendingApply\n ? `completed (attempt ${attempt + 1}) — diff ready, /job apply ${job.id}`\n : `completed (attempt ${attempt + 1})`,\n )\n outcome = 'success'\n return { outcome, jobId: job.id }\n }\n\n if (result === 'canceled') {\n this.deps.fireLogger.write(task.id, `canceled by user (attempt ${attempt + 1})`)\n outcomeCancelled = true\n break\n }\n\n lastError = result === 'timeout' ? 'timed out' : 'job failed'\n\n if (attempt < task.maxRetries) {\n const backoffMs = this.deps.retryBackoffMs ?? 0\n const sleep = this.deps.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n await sleep(backoffMs)\n }\n }\n\n if (!outcomeCancelled) {\n this.deps.fireLogger.write(task.id, `failed after ${task.maxRetries + 1} attempt(s)`)\n }\n\n return {\n outcome,\n cancelled: outcomeCancelled,\n error: lastError,\n }\n }\n\n // ── Cancel support ──\n\n /**\n * 设置 cancel 意图——fire() 循环在下次 attempt 前检查并停止。\n * 同时调用 jobRegistry.cancel() 终止当前正在运行的 job。\n * 返回 CancelResult(与 SchedulerService.cancel() 的返回类型对齐)。\n */\n requestCancel(taskId: string): CancelResult {\n let jobId: string | undefined\n const record = this.runningJobsByTaskId.get(taskId)\n if (record) {\n jobId = record.jobId\n } else {\n // Fallback: scan jobRegistry for a still-running job (best-effort after process restart)\n const runningJobs = this.deps.jobRegistry.list().filter(\n (j) => j.status === 'running' || j.status === 'needs_input',\n )\n if (runningJobs.length === 1) {\n jobId = runningJobs[0]!.id\n }\n }\n if (!jobId) return { ok: false, reason: 'not_running' }\n\n const job = this.deps.jobRegistry.get(jobId)\n if (!job) return { ok: false, reason: 'job_not_found' }\n if (job.status === 'ready') return { ok: false, reason: 'job_is_ready' }\n if (job.status === 'applied') return { ok: false, reason: 'job_already_applied' }\n if (job.status === 'failed') return { ok: false, reason: 'job_already_failed' }\n if (job.status === 'canceled') return { ok: false, reason: 'job_already_canceled' }\n\n this.cancelRequested.add(taskId)\n this.deps.jobRegistry.cancel(jobId)\n return { ok: true }\n }\n\n /** 清理 fire 完成后的记账状态。由 FireExecutor 在 fire().finally 中调用。 */\n cleanup(taskId: string): void {\n this.runningJobsByTaskId.delete(taskId)\n this.cancelRequested.delete(taskId)\n }\n\n /** 当前有正在运行的 job 的 task id 集合。 */\n getRunningTaskIds(): Set<string> {\n return new Set(this.runningJobsByTaskId.keys())\n }\n\n // ── Internal: event-based completion monitor ──\n\n private waitForCompletion(\n jobId: string,\n maxDurationMs: number,\n ctx: FireContext,\n ): Promise<'success' | 'failed' | 'timeout' | 'canceled'> {\n const needsInputMaxExtensionFactor = this.deps.needsInputMaxExtensionFactor ?? 3 // review S8: hard cap on needs_input total wait\n\n return new Promise((resolve) => {\n let settled = false\n let timeout: ReturnType<typeof setTimeout> | undefined\n let unsubExit: (() => void) | undefined\n let unsubUpdate: (() => void) | undefined\n\n const startedAt = ctx.now()\n const hardDeadline = startedAt + maxDurationMs * needsInputMaxExtensionFactor\n\n const cleanup = () => {\n if (timeout) clearTimeout(timeout)\n unsubExit?.()\n unsubUpdate?.()\n }\n\n const finish = (result: 'success' | 'failed' | 'timeout' | 'canceled') => {\n if (settled) return\n settled = true\n cleanup()\n resolve(result)\n }\n\n const armTimeout = (ms: number) => {\n if (timeout) clearTimeout(timeout)\n timeout = setTimeout(() => {\n this.deps.jobRegistry.cancel(jobId)\n finish('failed')\n }, ms)\n }\n\n unsubUpdate = this.deps.jobRegistry.onUpdate((job) => {\n if (job.id !== jobId) return\n\n if (job.status === 'ready') {\n finish('success')\n } else if (job.status === 'needs_input') {\n const now = ctx.now()\n const remaining = Math.min(maxDurationMs, hardDeadline - now)\n if (remaining <= 0) {\n this.deps.jobRegistry.cancel(jobId)\n finish('failed')\n return\n }\n armTimeout(remaining)\n }\n })\n\n unsubExit = this.deps.jobRegistry.onExit((job) => {\n if (job.id !== jobId) return\n\n if (job.status === 'failed') {\n finish('failed')\n } else if (job.status === 'canceled') {\n finish('canceled')\n } else if (job.status === 'applied') {\n finish('success')\n }\n })\n\n armTimeout(maxDurationMs)\n })\n }\n}\n","/**\n * session-fire-target.ts —— RFC-406 D3:SessionFireTarget handler。\n *\n * 从 scheduler.ts fireSession() 拆出。把 target==='session' 任务的 prompt 注入\n * boundSessionId 对应的当前会话。不建 worktree、不派 AgentJob、不进重试循环。\n *\n * 原代码位置:scheduler.ts fireSession() 方法。\n */\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { ScheduledTask } from '../types'\n\nexport interface SessionFireTargetDeps {\n /**\n * 把 prompt 注入 boundSessionId 对应的当前会话。\n * 返回 true=已成功注入;false=失效(会话身份不匹配/已销毁/archived/paused 等)。\n */\n followUp: (task: {\n prompt: string\n boundSessionId: string\n name: string\n origin: 'user' | 'model'\n }) => boolean\n /**\n * RFC-406 D6:session-local 任务创建时绑定的当前 sessionId 端口。\n * scheduler.add() 经 SchedulerService.getSessionIdForTask() 读取此值填充 boundSessionId。\n * (旧 SchedulerDeps.getSessionId 的语义迁入此 handler。)\n */\n getSessionId: () => string\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n}\n\nexport class SessionFireTarget implements FireTarget {\n readonly type = 'session'\n\n constructor(private deps: SessionFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n return task.target === 'session'\n }\n\n /** RFC-406 D6:暴露当前 sessionId 供 scheduler.add() 绑定 boundSessionId。 */\n getSessionId(): string {\n return this.deps.getSessionId()\n }\n\n async fire(task: ScheduledTask, _ctx: FireContext): Promise<FireResult> {\n if (!task.boundSessionId) {\n this.deps.fireLogger.write(task.id, 'session tick skipped: task has no boundSessionId (invalid state)')\n return { outcome: 'failed', error: 'no boundSessionId' }\n }\n\n // RFC-357 G5:每次 session-local 投递都带可见来源标记。\n // D3 新增约束1:模型创建的 session-local 任务额外在首次 fire 加一段更详细的提示。\n const isFirstModelFire = task.origin === 'model' && task.lastFiredAt == null\n const marker = `⏰ scheduled: ${task.name}`\n const prompt = isFirstModelFire\n ? `${marker} [created by the model — first fire, use /schedule cancel ${task.id} to stop it]\\n\\n${task.prompt}`\n : `${marker}\\n\\n${task.prompt}`\n\n let delivered = false\n try {\n delivered = this.deps.followUp({\n prompt,\n boundSessionId: task.boundSessionId,\n name: task.name,\n origin: task.origin,\n })\n } catch (err) {\n this.deps.fireLogger.write(\n task.id,\n `session tick error: ${err instanceof Error ? err.message : String(err)}`,\n )\n return { outcome: 'failed', error: err instanceof Error ? err.message : String(err) }\n }\n\n this.deps.fireLogger.write(\n task.id,\n delivered\n ? `session tick delivered (session ${task.boundSessionId})`\n : `session tick not delivered (session ${task.boundSessionId} unavailable — evicted/disposed/archived/paused/mismatched)`,\n )\n return { outcome: delivered ? 'success' : 'failed' }\n }\n}\n","/**\n * subscriber-fire-target.ts —— RFC-406 D3:SubscriberFireTarget handler。\n *\n * 从 scheduler.ts fireSubscriber() 拆出。到点只投递一条通知给已订阅的插件常驻服务,\n * 不派 AgentJob——服务若要产生工作须自己走 capability-gated startJob。\n *\n * 原代码位置:scheduler.ts fireSubscriber() 方法。\n */\nimport type { FireTarget, FireContext, FireResult } from '../fire-target'\nimport type { FireLogger } from '../fire-logger'\nimport type { ScheduleSubscriber, ScheduledTask } from '../types'\n\nexport interface SubscriberFireTargetDeps {\n /**\n * 向订阅者投递到点通知的窄端口。返回是否真的送达——false 表示服务未运行/已停机。\n * 端口刻意只接受纯数据标识:handler 不知道插件服务的存在形式,也不持有其句柄。\n */\n notify: (\n subscriber: ScheduleSubscriber,\n tick: { scheduleId: string; occurrenceId: string; occurredAt: number },\n ) => boolean\n /** FireLogger 实例(写 fire 日志) */\n fireLogger: FireLogger\n}\n\nexport class SubscriberFireTarget implements FireTarget {\n readonly type = 'plugin-service'\n\n constructor(private deps: SubscriberFireTargetDeps) {}\n\n canHandle(task: ScheduledTask): boolean {\n return !!task.subscriber\n }\n\n async fire(task: ScheduledTask, ctx: FireContext): Promise<FireResult> {\n const subscriber = task.subscriber\n if (!subscriber) return { outcome: 'failed', error: 'no subscriber on task' }\n\n const occurredAt = ctx.now()\n // occurrenceId 由 {taskId, 本次计划触发时刻} 派生——同一次计划触发的重复投递\n // 得到同一个 id(幂等键)。ticker 路径由调用方传入 scheduledFor;runNow 用实际触发时刻。\n const occurrenceId = `${task.id}@${ctx.scheduledFor ?? occurredAt}`\n\n let delivered = false\n try {\n delivered = this.deps.notify(subscriber, { scheduleId: task.id, occurrenceId, occurredAt })\n } catch (err) {\n this.deps.fireLogger.write(\n task.id,\n `subscriber tick error: ${err instanceof Error ? err.message : String(err)}`,\n )\n return { outcome: 'failed', error: err instanceof Error ? err.message : String(err) }\n }\n\n this.deps.fireLogger.write(\n task.id,\n delivered\n ? `subscriber tick delivered (${subscriber.pluginId}:${subscriber.serviceId}, occurrence ${occurrenceId})`\n : `subscriber tick not delivered (${subscriber.pluginId}:${subscriber.serviceId} not running)`,\n )\n return { outcome: delivered ? 'success' : 'failed' }\n }\n}\n","/**\n * in-process-event-trigger.ts —— 进程内事件触发器(RFC-423 D3/D4,经 RFC-433 D4 收敛)。\n *\n * 监听 AgentJobRegistry 终态事件(onExit:applied/failed/canceled),事件满足\n * EventTriggerSpec 时回调 fire(task)。MVP 唯一的进程内事件源。\n *\n * ## 两重死循环闸门(RFC-357 §7 点名的核心风险)\n *\n * 自触发死循环场景:event(job.applied) × JobFireTarget → 触发的 job apply 后又满足\n * job.applied → 再触发 → 无限循环。两重闸门:\n *\n * 1. **拒绝 job 投递**(闸门A,`SchedulerService.add()`):event × JobFireTarget 组合被\n * **无条件拒绝**——事件任务只能投 session 或 subscriber,两者都不产生新 job,\n * 自触发链在源头就不成立。这也正好匹配\"注入对话继续任务\"的诉求。\n * 2. **频率 debounce**(闸门B,本文件):单任务在窗口 debounceWindowMs 内最多\n * maxFiresPerWindow 次,超限记日志跳过。防高频事件(如连续多个 job 快速 apply)\n * 打爆当前对话。\n *\n * **RFC-433 D4 删除的第三重闸门**:原设计还有\"自产 job 排除\"(`markSelfDerived` +\n * `selfDerivedJobIds`),用于在 event × job 组合下不对自己产出的 job 触发。它从未接线\n * (生产代码零调用方)——因为闸门A 已让该组合根本不可能存在,闸门内的场景不可达。\n * **若将来放开闸门A,必须同时恢复自产 job 排除**(→ RFC-433 §4 R4)。\n */\nimport type { AgentJobRegistry, AgentSessionJob } from '@x-otto/runtime'\n\nimport type { EventDrivenTrigger } from '../trigger'\nimport type { ScheduledTask, EventTriggerSpec } from '../types'\n\nexport interface InProcessEventTriggerDeps {\n jobRegistry: AgentJobRegistry\n now?: () => number\n logger?: { error: (msg: string | Error, ...args: unknown[]) => void }\n /** 闸门B:debounce 窗口(ms)。缺省 60s。 */\n debounceWindowMs?: number\n /** 闸门B:窗口内最大触发次数。缺省 5。 */\n maxFiresPerWindow?: number\n}\n\nexport class InProcessEventTrigger implements EventDrivenTrigger {\n readonly type = 'in-process-event'\n\n /** 闸门B:每任务的触发时刻队列(滑动窗口)。 */\n private readonly fireTimestamps = new Map<string, number[]>()\n\n private readonly now: () => number\n private readonly debounceWindowMs: number\n private readonly maxFiresPerWindow: number\n\n constructor(private deps: InProcessEventTriggerDeps) {\n this.now = deps.now ?? Date.now\n this.debounceWindowMs = deps.debounceWindowMs ?? 60_000\n this.maxFiresPerWindow = deps.maxFiresPerWindow ?? 5\n }\n\n canHandle(task: ScheduledTask): boolean {\n // 只接管进程内 job 事件源的任务(MVP 唯一支持的 source)。\n return task.eventTrigger?.source === 'job'\n }\n\n arm(task: ScheduledTask, fire: (task: ScheduledTask) => void): () => void {\n const spec = task.eventTrigger\n if (!spec || spec.source !== 'job') return () => {}\n\n // 监听 AgentJobRegistry 终态(applied/failed/canceled)。\n return this.deps.jobRegistry.onExit((job) => {\n if (!this.matches(job, spec)) return\n if (!this.passesDebounce(task.id)) {\n this.deps.logger?.error(\n `[schedules] event-trigger ${task.id} debounced (>${this.maxFiresPerWindow} fires / ${this.debounceWindowMs}ms)`,\n )\n return\n }\n fire(task)\n })\n }\n\n // ── 匹配 + 闸门 ──\n\n private matches(job: AgentSessionJob, spec: EventTriggerSpec): boolean {\n // 事件名匹配 job 终态\n if (spec.event !== job.status) return false\n // 可选 sessionId 过滤\n if (spec.filter?.sessionId && spec.filter.sessionId !== job.sessionId) return false\n return true\n }\n\n /** 闸门B:滑动窗口频率检查。返回 true=放行,false=超限拦截。 */\n private passesDebounce(taskId: string): boolean {\n const now = this.now()\n const cutoff = now - this.debounceWindowMs\n const stamps = (this.fireTimestamps.get(taskId) ?? []).filter((t) => t > cutoff)\n if (stamps.length >= this.maxFiresPerWindow) {\n this.fireTimestamps.set(taskId, stamps) // 保留裁剪后的窗口\n return false\n }\n stamps.push(now)\n this.fireTimestamps.set(taskId, stamps)\n return true\n }\n}\n","/**\n * schedule-capability.ts —— Binds a SchedulerService to the shape the schedule_* model\n * tools consume (ToolCallbacksDeps.schedule).\n *\n * `origin` is pinned to 'model' HERE (the model-vs-user distinction lives in\n * wiring, not tool execute — ToolCallContext has no origin field). The scheduler\n * is resolved lazily so this capability object can exist at tool-registration\n * time (the `when: Boolean(c.schedule)` gate) even before the interactive scheduler\n * is constructed; it is populated via App.setScheduleService during interactive\n * setup (RFC-087 A1).\n */\nimport type { SchedulerService } from './scheduler'\n\nexport interface ScheduleCapability {\n create: (input: {\n cron: string\n prompt: string\n recurring: boolean\n name?: string\n /** review I2: optional override for the default 5-min single-fire timeout. */\n maxDurationMs?: number\n /**\n * RFC-357 D3 (M2): where the prompt fires. Default 'job' (existing behavior, zero\n * change). 'session' injects into the CURRENT conversation via followUp instead of\n * dispatching an isolated worktree AgentJob — see `create.ts`'s tool description for\n * the model-facing guidance on when to pick which.\n */\n target?: 'job' | 'session'\n /**\n * RFC-423:进程内事件触发。存在时任务由事件驱动(如 job diff applied)而非 cron。\n * 必须配 target:'session'(或 subscriber)——事件触发不允许 job 投递(死循环闸门)。\n */\n eventTrigger?: { source: string; event: string; filter?: { sessionId?: string } }\n /** RFC-438:到点执行时使用的模型 id(如 \"deepseek/deepseek-v4-flash\")。缺省 = 当前会话模型。 */\n modelId?: string\n /** RFC-438:思考档位(配 modelId 使用)。 */\n thinkingLevel?: 'low' | 'medium' | 'high' | 'xhigh' | 'max'\n }) => {\n taskId: string\n nextFireAt: number\n name: string\n /** RFC-357 D3: echoes the resolved target so callers (e.g. first-fire notice, M2) can react. */\n target?: 'job' | 'session'\n }\n list: () => Array<{\n id: string\n name: string\n cronExpression: string\n enabled: boolean\n recurring: boolean\n nextFireAt: number\n lastFiredAt?: number\n lastFireFailed?: boolean\n lastFireError?: string\n lastFireCancelled?: boolean\n }>\n delete: (taskId: string) => boolean\n run: (taskId: string) => void\n cancel: (taskId: string) => { ok: boolean; reason?: string }\n}\n\nexport interface ScheduleCapabilityDeps {\n /**\n * RFC-438 D3b:create 时校验模型 id。返回错误信息或 undefined(合法)。\n * 缺省 = 不校验(运行时 fail-closed 兜底——fire 时 resolveJobModel 抛错)。\n */\n validateModelId?: (id: string) => string | undefined\n}\n\nexport function createScheduleCapability(\n getScheduler: () => SchedulerService | undefined,\n deps: ScheduleCapabilityDeps = {},\n): ScheduleCapability {\n return {\n create: (input) => {\n const scheduler = getScheduler()\n if (!scheduler) {\n throw new Error('Schedule scheduler is not available in this session.')\n }\n // RFC-438 D3b:create 时提前校验模型 id——模型乱填 id 应在创建时明确拒绝\n // (否则任务建了、到点 fire 才炸,recurring 每晚静默失败)。\n if (input.modelId) {\n const err = deps.validateModelId?.(input.modelId)\n if (err) throw new Error(err)\n }\n const task = scheduler.add({\n name: input.name ?? 'schedule',\n prompt: input.prompt,\n cronExpression: input.cron,\n recurring: input.recurring,\n origin: 'model', // pinned by wiring — schedule_create is always model-originated\n maxDurationMs: input.maxDurationMs,\n // RFC-357 D3: model may request target:'session'. Non-owner rejection and\n // boundSessionId binding happen inside scheduler.add() (RFC-357 M1) — this\n // capability layer does not duplicate that logic.\n target: input.target,\n // RFC-423:事件触发透传。scheduler.add() 内建闸门2(event × job 拒绝)。\n eventTrigger: input.eventTrigger,\n // RFC-438:per-job 模型指定透传(D3b 已校验 modelId 合法)。\n modelId: input.modelId,\n thinkingLevel: input.thinkingLevel,\n })\n return { taskId: task.id, nextFireAt: task.nextFireAt, name: task.name, target: task.target }\n },\n list: () =>\n (getScheduler()?.list() ?? []).map((t) => ({\n id: t.id,\n name: t.name,\n cronExpression: t.cronExpression,\n enabled: t.enabled,\n recurring: t.recurring,\n nextFireAt: t.nextFireAt,\n lastFiredAt: t.lastFiredAt,\n lastFireFailed: t.lastFireFailed,\n lastFireError: t.lastFireError,\n lastFireCancelled: t.lastFireCancelled,\n })),\n delete: (taskId) => getScheduler()?.remove(taskId) ?? false,\n run: (taskId) => {\n getScheduler()?.runNow(taskId)\n },\n cancel: (taskId) => {\n return getScheduler()?.cancel(taskId) ?? { ok: false, reason: 'Schedule scheduler is not available.' }\n },\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-otto/schedule",
3
- "version": "0.0.1-alpha.4",
3
+ "version": "0.1.0-alpha.9",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -14,9 +14,10 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@x-otto/persistence": "0.0.1-alpha.5",
18
- "@x-otto/runtime": "0.0.1-alpha.11",
19
- "@x-otto/env": "0.1.0-alpha.7"
17
+ "@x-otto/env": "0.1.0-alpha.9",
18
+ "@x-otto/persistence": "0.1.0-alpha.9",
19
+ "@x-otto/interchange": "0.1.0-alpha.9",
20
+ "@x-otto/runtime": "0.1.0-alpha.9"
20
21
  },
21
22
  "devDependencies": {
22
23
  "typescript": "6.0.2",