@zhushanwen/extension-protocol 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -80,6 +80,26 @@ interface GuiComponentProps {
80
80
  */
81
81
  sections?: GuiComponent[][];
82
82
  };
83
+ /**
84
+ * 操作按钮栏——GuiComponent 协议首个交互原语(设计 AP-3)。
85
+ * 点击经宿主壳 provide 的执行器走既有命令链(commandRegistry.execute → WS
86
+ * plugin.executeCommand),协议不新增回调通道(设计 §3.3 D2)。
87
+ * 未含此 type 的旧宿主自动降级 ansi-text 可读文本,PROTOCOL_VERSION 不 bump
88
+ * (设计 §3.3 D8,降级契约 P5)。
89
+ */
90
+ 'action-bar': {
91
+ items: {
92
+ id: string;
93
+ label: string;
94
+ /** 视觉档位:只保留当前唯一实际消费的 danger(删除按钮);更多档位有真实消费方时再加 */
95
+ kind?: 'danger';
96
+ disabled?: boolean;
97
+ /** 缺省 = 纯展示项(不可点、弱化样式) */
98
+ commandId?: string;
99
+ /** 只许标量:args 原样传给插件 handler,禁嵌套对象防无 schema 结构化旁路(设计 AP-3 校验) */
100
+ args?: Record<string, string | number | boolean>;
101
+ }[];
102
+ };
83
103
  /** 自定义组件——逃生口(仅限内置 extension 编译期注册) */
84
104
  'custom': {
85
105
  component: string;
@@ -504,6 +524,176 @@ declare function getAskUserOther(answers: AskUserAnswers, question: AskUserQuest
504
524
  */
505
525
  declare function isAskUserQuestion(value: unknown): value is AskUserQuestion;
506
526
 
527
+ type ScheduleSpec = {
528
+ mode: 'cron';
529
+ cronExpression: string;
530
+ } | {
531
+ mode: 'interval';
532
+ intervalMs: number;
533
+ };
534
+ type TaskKind = 'once' | 'recurring';
535
+ type TaskStatus = 'success' | 'failed';
536
+ interface ScheduledTask {
537
+ id: string;
538
+ name: string;
539
+ prompt: string;
540
+ kind: TaskKind;
541
+ schedule: ScheduleSpec;
542
+ model?: string;
543
+ enabled: boolean;
544
+ createdAt: number;
545
+ nextRunAt: number;
546
+ expiresAt?: number;
547
+ runCount: number;
548
+ lastRunAt?: number;
549
+ lastStatus?: TaskStatus;
550
+ lastError?: string;
551
+ history: ExecutionRecord[];
552
+ ownerSessionFile?: string;
553
+ pending?: boolean;
554
+ }
555
+ interface ExecutionRecord {
556
+ at: number;
557
+ status: TaskStatus;
558
+ }
559
+ /**
560
+ * history 裁剪上限(ext-simplify-08 L5 单点):runtime 侧 dispatch 累积
561
+ * (onDispatchSuccess)与 replay 侧 advance 折叠共用同一上限——
562
+ * 双处各持字面量会漂移(曾 runtime.ts / replay.ts 各写一份 20),收敛到本文件单点导出。
563
+ */
564
+ declare const HISTORY_LIMIT = 20;
565
+ /**
566
+ * 追加执行记录并按 HISTORY_LIMIT 裁剪(ext-simplify-17 B4 单点):runtime 侧 dispatch
567
+ * 累积与 replay 侧 advance 折叠共用的 push+trim——三处内联时与上限字面量同源存在
568
+ * 「漏改一处」漂移风险,随上限一并收敛。
569
+ */
570
+ declare function appendExecutionRecord(task: ScheduledTask, at: number, status: TaskStatus): void;
571
+ /**
572
+ * pi.appendEntry 写入的 custom entry customType 标识(ext-simplify-17 B1 单点):
573
+ * backend.appendEntry 写入 / replay 折叠过滤 / importer 旧 store 导入三处共用——
574
+ * 各持字面量时「改一处漏两处」会使写入与重放识别脱节(写入新标识、重放不认,任务静默丢失)。
575
+ */
576
+ declare const TASK_ENTRY_TYPE = "pi-scheduler:task";
577
+ /**
578
+ * upsert op 携带的全量任务快照(不含 ownerSessionFile / pending)。
579
+ * ownerSessionFile 放在 op 顶层(见 SchedulerEntryOp.upsert),
580
+ * pending 是运行时标记不持久化。其余字段与 ScheduledTask 对齐。
581
+ */
582
+ interface TaskSnapshot {
583
+ id: string;
584
+ name: string;
585
+ prompt: string;
586
+ kind: TaskKind;
587
+ schedule: ScheduleSpec;
588
+ model?: string;
589
+ enabled: boolean;
590
+ createdAt: number;
591
+ nextRunAt: number;
592
+ expiresAt?: number;
593
+ runCount: number;
594
+ lastRunAt?: number;
595
+ lastStatus?: TaskStatus;
596
+ lastError?: string;
597
+ history: ExecutionRecord[];
598
+ }
599
+ /**
600
+ * pi.appendEntry(TASK_ENTRY_TYPE, SchedulerEntryOp) 写入 session JSONL 的 op 联合类型。
601
+ *
602
+ * advance.status 固定为 'success'(CL8):对齐现有 TaskStatus='success'|'failed',
603
+ * 而非继承的 DM-SCHEDULER-OP 词表 'ok'。唯一 emit 值——按 CL7,advance 仅在 dispatch
604
+ * 成功(nextRunAt 实际推进)时 append,失败 dispatch 不 append(transient 失败重试语义)。
605
+ * fold 的 `task.lastStatus = entry.status` 直接赋值合法('success' 属 TaskStatus)。
606
+ */
607
+ type SchedulerEntryOp = {
608
+ op: 'upsert';
609
+ taskId: string;
610
+ ownerSessionFile: string;
611
+ task: TaskSnapshot;
612
+ } | {
613
+ op: 'advance';
614
+ taskId: string;
615
+ nextRunAt: number;
616
+ at: number;
617
+ status: 'success';
618
+ } | {
619
+ op: 'toggle';
620
+ taskId: string;
621
+ enabled: boolean;
622
+ nextRunAt?: number;
623
+ } | {
624
+ op: 'delete';
625
+ taskId: string;
626
+ };
627
+ /**
628
+ * TaskSnapshot → ScheduledTask(replay 侧重建):pending 不持久化故不恢复,
629
+ * ownerSessionFile 由调用方(replay applyUpsert)从 op 顶层补回。history 逐项深拷贝——
630
+ * 避免恢复出的运行时 task 与快照共享数组/元素引用(task 后续被 mutate 时不污染快照)。
631
+ */
632
+ declare function snapshotToTask(snapshot: TaskSnapshot): ScheduledTask;
633
+
634
+ /**
635
+ * 折叠期降级日志注入点:包内不持有任何 logger 依赖(零 pi 依赖收敛),由调用方注入。
636
+ * 省略时降级路径静默(折叠行为不受影响——返回值与注入无关)。
637
+ */
638
+ interface ReplayFoldOptions {
639
+ warn?: (message: string, context?: unknown) => void;
640
+ }
641
+ /**
642
+ * CustomEntry 的最小可识别形状(duck-typed,不依赖 @earendil-works/pi-coding-agent 的
643
+ * 具体 SessionEntry 类型)。与 pending-notifications/state.ts、goal/ports.ts 同款:
644
+ * 纯函数可独立单测,调用方传入真实 SessionEntry[] 时按结构兼容。
645
+ */
646
+ interface SchedulerEntryLike {
647
+ type: string;
648
+ customType?: string;
649
+ data?: unknown;
650
+ }
651
+ /**
652
+ * 从 CustomEntry 序列折叠恢复 per-task 末态(event sourcing)。
653
+ *
654
+ * 两类调用方:
655
+ * - pi 扩展 session_start 重放:PiSchedulerBackend.loadTasks() 委托本函数,把当前 session 的
656
+ * TASK_ENTRY_TYPE custom entries 折叠成运行时任务 Map。
657
+ * - taiji plugin 条目镜像:对 readEntries 拉取的累计 entries 调用本函数折叠出任务表
658
+ * (与扩展同一实现,插件与扩展折叠结果构造性一致)。
659
+ *
660
+ * 两步:
661
+ * ① 全量折叠(foldEntries)——遍历 customType===TASK_ENTRY_TYPE 的 entries,按 op 类型折叠:
662
+ * - upsert → 用快照重建任务(ownerSessionFile 来自 op 顶层)
663
+ * - advance → nextRunAt=新值 / lastRunAt=at / runCount++ / history.push(裁20) / lastStatus=status(gap2)
664
+ * - toggle → enabled=新值
665
+ * - delete → 从 Map 移除(后续同 taskId 的 advance/toggle 对不存在 taskId 为 no-op,安全)
666
+ * advance/toggle/delete 对不存在的 taskId 为 no-op,因此 fork 继承的完整序列必须先全量折叠,
667
+ * 不能在处理 upsert 时按 owner 跳过(否则后续无 owner 字段的 op 会作用于「不存在的 taskId」丢失)。
668
+ * ② 折叠完成后按 task.ownerSessionFile !== currentSessionFile 整体过滤(gap1,filterNonOwnerTasks):
669
+ * 移除 fork 继承的非 owner 任务——fork 出的 session B 继承了 owner=A 的 upsert+advance 序列,
670
+ * 但 B 不应拥有 A 的任务,重放后整体过滤掉。
671
+ *
672
+ * append-only 时序保证 nextRunAt 不回退(D1):advance entry 按写入顺序折叠,自然取到最后推进值。
673
+ *
674
+ * getEntries 解析/迭代异常 → options.warn + 返回空 Map(gap4):降级为无任务而非崩溃调用方。
675
+ */
676
+ declare function replayFoldEntries(entries: Iterable<SchedulerEntryLike>, currentSessionFile: string | undefined, options?: ReplayFoldOptions): Map<string, ScheduledTask>;
677
+
678
+ declare const MS_PER_DAY = 86400000;
679
+ declare const MS_PER_HOUR = 3600000;
680
+ declare const MS_PER_MINUTE = 60000;
681
+ declare const MS_PER_SECOND = 1000;
682
+ /** 格式化毫秒数为可读 duration 字符串。优先使用最大单位:300000 → "5m",不是 "300s"。 */
683
+ declare function formatDuration(ms: number): string;
684
+ /** Format ScheduleSpec to readable string. kind 区分 once/recurring(once 显示 'once in X' 而非误导性的 'every X')。 */
685
+ declare function formatSchedule(spec: ScheduleSpec, kind?: TaskKind): string;
686
+ /**
687
+ * 格式化时间戳为相对时间字符串。
688
+ * 未来: "in 5m"
689
+ * 过去: "5m ago"
690
+ * 当前(+-5s): "now"
691
+ *
692
+ * now 可选参数:基准时间戳,默认 Date.now()。测试可传固定值快进/锁定,
693
+ * 生产调用方无需传(参数可选,行为不变)。
694
+ */
695
+ declare function formatRelativeTime(timestamp: number, now?: number): string;
696
+
507
697
  /**
508
698
  * scheduler 创建确认弹框的协议类型(Draft / FormResult)。
509
699
  *
@@ -1170,4 +1360,4 @@ declare function collectActivePendingIds(entries: unknown[], opts?: CollectPendi
1170
1360
  type MappedPendingStatus = 'completed' | 'failed' | 'cancelled' | 'expired' | 'time_limited' | 'aborted';
1171
1361
  declare function mapReasonToStatus(reason: string): MappedPendingStatus;
1172
1362
 
1173
- export { ASK_USER_MARKER, type AskUserAnswers, type AskUserOption, type AskUserQuestion, BRIDGE_MARKER, BRIDGE_METHODS, type BridgeErrorResponse, type BridgeInterceptResponse, type BridgeMethod, type BridgeRequest, type BridgeSyncPayload, type BridgeToolExecuteResponse, type ChannelErrorResult, type ChoiceQuestion, type CollectPendingIdsOptions, type DualWidgetContent, type FormAnswers, type FormOption, type FormQuestion, GUI_WIDGET_MARKER, type GuiComponent, type GuiComponentProps, type GuiComponentType, type GuiContext, type GuiRenderResult, INFLIGHT_REPORT_ACK, type MappedPendingStatus, type MarkerRpcOptions, type MarkerRpcResult, PENDING_REGISTER_ENTRY_TYPE, PENDING_UNREGISTER_ENTRY_TYPE, PLAN_REVIEW_MARKER, PROTOCOL_VERSION, type PendingEntriesScan, type PlanDocMeta, type PlanReviewComment, type PlanReviewDecision, type PlanReviewRequest, type PlanReviewResponse, SCHEDULE_CREATE_MARKER, SESSION_MANAGER_ACTIONS, SESSION_MANAGER_MARKER, SUBAGENTS_ENGINES_FILENAME, SUBAGENT_INFLIGHT_MARKER, type ScheduleDraft, type ScheduleFormResult, type ScheduleKind, type ScheduleQuestion, type SessionManagerAbortParams, type SessionManagerAbortResult, type SessionManagerAction, type SessionManagerCreateParams, type SessionManagerCreateResult, type SessionManagerErrorResult, type SessionManagerHistoryParams, type SessionManagerHistoryResult, type SessionManagerListParams, type SessionManagerListResult, type SessionManagerParams, type SessionManagerSendParams, type SessionManagerSendResult, type SessionManagerSessionSummary, type SessionManagerStatusParams, type SessionManagerStatusResult, type StatItem, type SubagentEngineConfigView, type SubagentEnginesFile, type SubagentInFlightReport, type TextQuestion, type TreeItem, type TreeItemIcon, UI_FORM_MARKER, type UiFormInteractOptions, type UiFormInteractResult, type WidgetIconPathsRejection, type WidgetIconPathsValidation, type WidgetMeta, applyPendingDiff, callMarkerRpc, collectActivePendingIds, dateToOnceCron, extractGui, firstContentText, formatChannelErrorText, getAskUserAnswer, getAskUserOther, guiComponent, guiResult, guiSetWidget, isAskUserQuestion, isBridgeErrorResponse, isBridgeInterceptResponse, isBridgeSyncPayload, isBridgeToolExecuteResponse, isChannelErrorResult, isFormAnswers, isFormQuestion, isGuiCapable, isGuiComponent, isGuiRenderResult, isInFlightReportAck, isScheduleDraft, isScheduleFormResult, isSessionManagerAbortParams, isSessionManagerCreateParams, isSessionManagerHistoryParams, isSessionManagerListParams, isSessionManagerSendParams, isSessionManagerStatusParams, isSubagentInFlightReport, isSyncedTool, mapReasonToStatus, onceCronToDate, scanPendingEntries, setWidgetDual, uiFormInteract, validateWidgetIconPaths };
1363
+ export { ASK_USER_MARKER, type AskUserAnswers, type AskUserOption, type AskUserQuestion, BRIDGE_MARKER, BRIDGE_METHODS, type BridgeErrorResponse, type BridgeInterceptResponse, type BridgeMethod, type BridgeRequest, type BridgeSyncPayload, type BridgeToolExecuteResponse, type ChannelErrorResult, type ChoiceQuestion, type CollectPendingIdsOptions, type DualWidgetContent, type ExecutionRecord, type FormAnswers, type FormOption, type FormQuestion, GUI_WIDGET_MARKER, type GuiComponent, type GuiComponentProps, type GuiComponentType, type GuiContext, type GuiRenderResult, HISTORY_LIMIT, INFLIGHT_REPORT_ACK, MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE, MS_PER_SECOND, type MappedPendingStatus, type MarkerRpcOptions, type MarkerRpcResult, PENDING_REGISTER_ENTRY_TYPE, PENDING_UNREGISTER_ENTRY_TYPE, PLAN_REVIEW_MARKER, PROTOCOL_VERSION, type PendingEntriesScan, type PlanDocMeta, type PlanReviewComment, type PlanReviewDecision, type PlanReviewRequest, type PlanReviewResponse, type ReplayFoldOptions, SCHEDULE_CREATE_MARKER, SESSION_MANAGER_ACTIONS, SESSION_MANAGER_MARKER, SUBAGENTS_ENGINES_FILENAME, SUBAGENT_INFLIGHT_MARKER, type ScheduleDraft, type ScheduleFormResult, type ScheduleKind, type ScheduleQuestion, type ScheduleSpec, type ScheduledTask, type SchedulerEntryLike, type SchedulerEntryOp, type SessionManagerAbortParams, type SessionManagerAbortResult, type SessionManagerAction, type SessionManagerCreateParams, type SessionManagerCreateResult, type SessionManagerErrorResult, type SessionManagerHistoryParams, type SessionManagerHistoryResult, type SessionManagerListParams, type SessionManagerListResult, type SessionManagerParams, type SessionManagerSendParams, type SessionManagerSendResult, type SessionManagerSessionSummary, type SessionManagerStatusParams, type SessionManagerStatusResult, type StatItem, type SubagentEngineConfigView, type SubagentEnginesFile, type SubagentInFlightReport, TASK_ENTRY_TYPE, type TaskKind, type TaskSnapshot, type TaskStatus, type TextQuestion, type TreeItem, type TreeItemIcon, UI_FORM_MARKER, type UiFormInteractOptions, type UiFormInteractResult, type WidgetIconPathsRejection, type WidgetIconPathsValidation, type WidgetMeta, appendExecutionRecord, applyPendingDiff, callMarkerRpc, collectActivePendingIds, dateToOnceCron, extractGui, firstContentText, formatChannelErrorText, formatDuration, formatRelativeTime, formatSchedule, getAskUserAnswer, getAskUserOther, guiComponent, guiResult, guiSetWidget, isAskUserQuestion, isBridgeErrorResponse, isBridgeInterceptResponse, isBridgeSyncPayload, isBridgeToolExecuteResponse, isChannelErrorResult, isFormAnswers, isFormQuestion, isGuiCapable, isGuiComponent, isGuiRenderResult, isInFlightReportAck, isScheduleDraft, isScheduleFormResult, isSessionManagerAbortParams, isSessionManagerCreateParams, isSessionManagerHistoryParams, isSessionManagerListParams, isSessionManagerSendParams, isSessionManagerStatusParams, isSubagentInFlightReport, isSyncedTool, mapReasonToStatus, onceCronToDate, replayFoldEntries, scanPendingEntries, setWidgetDual, snapshotToTask, uiFormInteract, validateWidgetIconPaths };
package/dist/index.js CHANGED
@@ -185,6 +185,176 @@ function isAskUserQuestion(value) {
185
185
  return typeof q.question === "string" && (q.header === void 0 || typeof q.header === "string") && (q.options === void 0 || Array.isArray(q.options)) && (q.multiSelect === void 0 || typeof q.multiSelect === "boolean") && (q.allowOther === void 0 || typeof q.allowOther === "boolean");
186
186
  }
187
187
 
188
+ // src/extensions/scheduler/types.ts
189
+ var HISTORY_LIMIT = 20;
190
+ function appendExecutionRecord(task, at, status) {
191
+ task.history.push({ at, status });
192
+ if (task.history.length > HISTORY_LIMIT) task.history.shift();
193
+ }
194
+ var TASK_ENTRY_TYPE = "pi-scheduler:task";
195
+ function snapshotToTask(snapshot) {
196
+ return {
197
+ id: snapshot.id,
198
+ name: snapshot.name,
199
+ prompt: snapshot.prompt,
200
+ kind: snapshot.kind,
201
+ schedule: snapshot.schedule,
202
+ model: snapshot.model,
203
+ enabled: snapshot.enabled,
204
+ createdAt: snapshot.createdAt,
205
+ nextRunAt: snapshot.nextRunAt,
206
+ expiresAt: snapshot.expiresAt,
207
+ runCount: snapshot.runCount,
208
+ lastRunAt: snapshot.lastRunAt,
209
+ lastStatus: snapshot.lastStatus,
210
+ lastError: snapshot.lastError,
211
+ history: snapshot.history.map((h) => ({ ...h }))
212
+ };
213
+ }
214
+
215
+ // src/extensions/scheduler/replay.ts
216
+ function toErrorMessage(e) {
217
+ return e instanceof Error ? e.message : String(e);
218
+ }
219
+ function replayFoldEntries(entries, currentSessionFile, options) {
220
+ try {
221
+ const tasks = /* @__PURE__ */ new Map();
222
+ foldEntries(entries, tasks, options);
223
+ filterNonOwnerTasks(tasks, currentSessionFile);
224
+ return tasks;
225
+ } catch (err) {
226
+ options?.warn?.("replayFoldEntries failed", { error: toErrorMessage(err) });
227
+ return /* @__PURE__ */ new Map();
228
+ }
229
+ }
230
+ function foldEntries(entries, tasks, options) {
231
+ for (const entry of entries) {
232
+ if (entry.type !== "custom" || entry.customType !== TASK_ENTRY_TYPE) continue;
233
+ if (!isSchedulerEntryOp(entry.data)) continue;
234
+ const op = entry.data;
235
+ try {
236
+ applyOp(tasks, op);
237
+ } catch (err) {
238
+ options?.warn?.("skipping corrupted scheduler entry", { error: toErrorMessage(err) });
239
+ continue;
240
+ }
241
+ }
242
+ }
243
+ function applyOp(tasks, op) {
244
+ switch (op.op) {
245
+ case "upsert":
246
+ applyUpsert(tasks, op);
247
+ break;
248
+ case "advance":
249
+ applyAdvance(tasks, op);
250
+ break;
251
+ case "toggle":
252
+ applyToggle(tasks, op);
253
+ break;
254
+ case "delete":
255
+ applyDelete(tasks, op);
256
+ break;
257
+ }
258
+ }
259
+ function applyUpsert(tasks, op) {
260
+ tasks.set(op.taskId, { ...snapshotToTask(op.task), ownerSessionFile: op.ownerSessionFile });
261
+ }
262
+ function applyAdvance(tasks, op) {
263
+ const task = tasks.get(op.taskId);
264
+ if (!task) return;
265
+ task.nextRunAt = op.nextRunAt;
266
+ task.lastRunAt = op.at;
267
+ task.runCount += 1;
268
+ appendExecutionRecord(task, op.at, op.status);
269
+ task.lastStatus = op.status;
270
+ }
271
+ function applyToggle(tasks, op) {
272
+ const task = tasks.get(op.taskId);
273
+ if (!task) return;
274
+ task.enabled = op.enabled;
275
+ if (op.nextRunAt !== void 0) task.nextRunAt = op.nextRunAt;
276
+ }
277
+ function applyDelete(tasks, op) {
278
+ tasks.delete(op.taskId);
279
+ }
280
+ function filterNonOwnerTasks(tasks, currentSessionFile) {
281
+ for (const [id, task] of tasks) {
282
+ if (task.ownerSessionFile !== currentSessionFile) {
283
+ tasks.delete(id);
284
+ }
285
+ }
286
+ }
287
+ function isSchedulerEntryOp(data) {
288
+ if (!data || typeof data !== "object") return false;
289
+ const record = data;
290
+ const op = record.op;
291
+ if (op === "upsert") {
292
+ return typeof record.taskId === "string" && typeof record.task === "object" && record.task !== null;
293
+ }
294
+ if (op === "advance") {
295
+ return typeof record.taskId === "string" && typeof record.nextRunAt === "number" && typeof record.at === "number" && record.status === "success";
296
+ }
297
+ if (op === "toggle") {
298
+ return typeof record.taskId === "string" && typeof record.enabled === "boolean" && (record.nextRunAt === void 0 || typeof record.nextRunAt === "number");
299
+ }
300
+ if (op === "delete") {
301
+ return typeof record.taskId === "string";
302
+ }
303
+ return false;
304
+ }
305
+
306
+ // src/extensions/scheduler/format.ts
307
+ var MS_PER_DAY = 864e5;
308
+ var MS_PER_HOUR = 36e5;
309
+ var MS_PER_MINUTE = 6e4;
310
+ var MS_PER_SECOND = 1e3;
311
+ var NOW_THRESHOLD_MS = 5e3;
312
+ function formatDuration(ms) {
313
+ if (ms <= 0) return "0s";
314
+ const units = [
315
+ ["d", MS_PER_DAY],
316
+ ["h", MS_PER_HOUR],
317
+ ["m", MS_PER_MINUTE],
318
+ ["s", MS_PER_SECOND]
319
+ ];
320
+ for (const [suffix, divisor] of units) {
321
+ if (ms >= divisor && ms % divisor === 0) {
322
+ return `${ms / divisor}${suffix}`;
323
+ }
324
+ }
325
+ return `${Math.round(ms / MS_PER_SECOND)}s`;
326
+ }
327
+ function formatSchedule(spec, kind) {
328
+ if (spec.mode === "interval") {
329
+ return kind === "once" ? `once in ${formatDuration(spec.intervalMs)}` : `every ${formatDuration(spec.intervalMs)}`;
330
+ }
331
+ return spec.cronExpression;
332
+ }
333
+ function formatRelativeTime(timestamp, now) {
334
+ const currentTime = now ?? Date.now();
335
+ const diff = timestamp - currentTime;
336
+ if (Math.abs(diff) < NOW_THRESHOLD_MS) return "now";
337
+ const absDiff = Math.abs(diff);
338
+ const units = [
339
+ ["d", MS_PER_DAY],
340
+ ["h", MS_PER_HOUR],
341
+ ["m", MS_PER_MINUTE],
342
+ ["s", MS_PER_SECOND]
343
+ ];
344
+ let formatted = "";
345
+ for (const [suffix, divisor] of units) {
346
+ if (absDiff >= divisor) {
347
+ const value = Math.floor(absDiff / divisor);
348
+ formatted = `${value}${suffix}`;
349
+ break;
350
+ }
351
+ }
352
+ if (!formatted) {
353
+ formatted = `${Math.round(absDiff / MS_PER_SECOND)}s`;
354
+ }
355
+ return diff > 0 ? `in ${formatted}` : `${formatted} ago`;
356
+ }
357
+
188
358
  // src/extensions/scheduler-create/marker.ts
189
359
  var SCHEDULE_CREATE_MARKER = "\0TAIJI_SCHEDULE_CREATE";
190
360
 
@@ -493,8 +663,13 @@ export {
493
663
  BRIDGE_MARKER,
494
664
  BRIDGE_METHODS,
495
665
  GUI_WIDGET_MARKER,
666
+ HISTORY_LIMIT,
496
667
  INFLIGHT_REPORT_ACK,
497
668
  MAX_TERMINAL_REGISTRY_ENTRIES,
669
+ MS_PER_DAY,
670
+ MS_PER_HOUR,
671
+ MS_PER_MINUTE,
672
+ MS_PER_SECOND,
498
673
  PENDING_REGISTER_ENTRY_TYPE,
499
674
  PENDING_UNREGISTER_ENTRY_TYPE,
500
675
  PLAN_REVIEW_MARKER,
@@ -504,7 +679,9 @@ export {
504
679
  SESSION_MANAGER_MARKER,
505
680
  SUBAGENTS_ENGINES_FILENAME,
506
681
  SUBAGENT_INFLIGHT_MARKER,
682
+ TASK_ENTRY_TYPE,
507
683
  UI_FORM_MARKER,
684
+ appendExecutionRecord,
508
685
  applyPendingDiff,
509
686
  callMarkerRpc,
510
687
  collectActivePendingIds,
@@ -512,6 +689,9 @@ export {
512
689
  extractGui,
513
690
  firstContentText,
514
691
  formatChannelErrorText,
692
+ formatDuration,
693
+ formatRelativeTime,
694
+ formatSchedule,
515
695
  getAskUserAnswer,
516
696
  getAskUserOther,
517
697
  guiComponent,
@@ -544,8 +724,10 @@ export {
544
724
  isTerminalBackgroundTaskState,
545
725
  mapReasonToStatus,
546
726
  onceCronToDate,
727
+ replayFoldEntries,
547
728
  scanPendingEntries,
548
729
  setWidgetDual,
730
+ snapshotToTask,
549
731
  uiFormInteract,
550
732
  validateWidgetIconPaths
551
733
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/extension-protocol",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Cross-layer pi extension contract package: dual-mode (TUI/GUI) rendering helpers, session/background-task/pending-entries protocols, and shared behavior primitives",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",