botmux 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/adapters/cli/pi.d.ts.map +1 -1
  2. package/dist/adapters/cli/pi.js +4 -0
  3. package/dist/adapters/cli/pi.js.map +1 -1
  4. package/dist/adapters/cli/types.d.ts +8 -1
  5. package/dist/adapters/cli/types.d.ts.map +1 -1
  6. package/dist/cli/deferred-topic-send.d.ts +37 -0
  7. package/dist/cli/deferred-topic-send.d.ts.map +1 -0
  8. package/dist/cli/deferred-topic-send.js +69 -0
  9. package/dist/cli/deferred-topic-send.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +160 -34
  12. package/dist/cli.js.map +1 -1
  13. package/dist/core/command-handler.d.ts.map +1 -1
  14. package/dist/core/command-handler.js +17 -12
  15. package/dist/core/command-handler.js.map +1 -1
  16. package/dist/core/dashboard-ipc-server.d.ts +4 -1
  17. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  18. package/dist/core/dashboard-ipc-server.js +78 -8
  19. package/dist/core/dashboard-ipc-server.js.map +1 -1
  20. package/dist/core/deferred-schedule-settlement.d.ts +20 -0
  21. package/dist/core/deferred-schedule-settlement.d.ts.map +1 -0
  22. package/dist/core/deferred-schedule-settlement.js +18 -0
  23. package/dist/core/deferred-schedule-settlement.js.map +1 -0
  24. package/dist/core/deferred-topic-binding.d.ts +19 -0
  25. package/dist/core/deferred-topic-binding.d.ts.map +1 -0
  26. package/dist/core/deferred-topic-binding.js +55 -0
  27. package/dist/core/deferred-topic-binding.js.map +1 -0
  28. package/dist/core/scheduler.d.ts +26 -20
  29. package/dist/core/scheduler.d.ts.map +1 -1
  30. package/dist/core/scheduler.js +114 -56
  31. package/dist/core/scheduler.js.map +1 -1
  32. package/dist/core/session-discovery.d.ts.map +1 -1
  33. package/dist/core/session-discovery.js +47 -15
  34. package/dist/core/session-discovery.js.map +1 -1
  35. package/dist/core/session-manager.d.ts +17 -1
  36. package/dist/core/session-manager.d.ts.map +1 -1
  37. package/dist/core/session-manager.js +178 -84
  38. package/dist/core/session-manager.js.map +1 -1
  39. package/dist/core/types.d.ts +4 -0
  40. package/dist/core/types.d.ts.map +1 -1
  41. package/dist/core/types.js +10 -0
  42. package/dist/core/types.js.map +1 -1
  43. package/dist/core/worker-pool.d.ts +7 -0
  44. package/dist/core/worker-pool.d.ts.map +1 -1
  45. package/dist/core/worker-pool.js +51 -1
  46. package/dist/core/worker-pool.js.map +1 -1
  47. package/dist/daemon.d.ts.map +1 -1
  48. package/dist/daemon.js +90 -5
  49. package/dist/daemon.js.map +1 -1
  50. package/dist/dashboard/schedule-card-model.d.ts +12 -3
  51. package/dist/dashboard/schedule-card-model.d.ts.map +1 -1
  52. package/dist/dashboard/schedule-card-model.js +32 -8
  53. package/dist/dashboard/schedule-card-model.js.map +1 -1
  54. package/dist/dashboard/web/dashboard-components.d.ts +7 -0
  55. package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
  56. package/dist/dashboard/web/dashboard-components.js +11 -2
  57. package/dist/dashboard/web/dashboard-components.js.map +1 -1
  58. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  59. package/dist/dashboard/web/i18n.js +30 -6
  60. package/dist/dashboard/web/i18n.js.map +1 -1
  61. package/dist/dashboard/web/schedules-page.d.ts +2 -0
  62. package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
  63. package/dist/dashboard/web/schedules-page.js +74 -27
  64. package/dist/dashboard/web/schedules-page.js.map +1 -1
  65. package/dist/dashboard-web/app.js +1 -1
  66. package/dist/dashboard-web/chunks/{bot-defaults-page-O37SAQD5.js → bot-defaults-page-B5AY4B2J.js} +1 -1
  67. package/dist/dashboard-web/chunks/{chunk-6QHMLMIM.js → chunk-CUV65BIQ.js} +3 -3
  68. package/dist/dashboard-web/chunks/{chunk-O2DPQL7C.js → chunk-JNRSPKLS.js} +1 -1
  69. package/dist/dashboard-web/chunks/{chunk-FYALHCPG.js → chunk-P6GO5KT4.js} +1 -1
  70. package/dist/dashboard-web/chunks/chunk-QFMH6ITB.js +1 -0
  71. package/dist/dashboard-web/chunks/{chunk-B42PCL5P.js → chunk-UBOLHQCK.js} +1 -1
  72. package/dist/dashboard-web/chunks/{connectors-page-DZBQ42MH.js → connectors-page-KTC2QWNW.js} +1 -1
  73. package/dist/dashboard-web/chunks/{groups-page-3IEGRZKQ.js → groups-page-B3EKGOLY.js} +1 -1
  74. package/dist/dashboard-web/chunks/{insights-page-X3ADRON3.js → insights-page-X6RXNCWX.js} +1 -1
  75. package/dist/dashboard-web/chunks/{monitor-room-IV36RGQY.js → monitor-room-LLRA5LQW.js} +1 -1
  76. package/dist/dashboard-web/chunks/{monitoring-page-ADFKVON4.js → monitoring-page-7MT5FEOS.js} +1 -1
  77. package/dist/dashboard-web/chunks/{office-page-CALAOWNS.js → office-page-KD46D5TD.js} +1 -1
  78. package/dist/dashboard-web/chunks/{overview-page-SLMGLK3M.js → overview-page-JUWNH32O.js} +1 -1
  79. package/dist/dashboard-web/chunks/{roles-page-L7JJ4FFV.js → roles-page-P6F7NLOI.js} +1 -1
  80. package/dist/dashboard-web/chunks/schedules-page-XIHWZST4.js +1 -0
  81. package/dist/dashboard-web/chunks/{sessions-page-OKJ5YWUZ.js → sessions-page-2VECQ27T.js} +1 -1
  82. package/dist/dashboard-web/chunks/{settings-page-P6MI2ZUE.js → settings-page-HMIUE2JA.js} +1 -1
  83. package/dist/dashboard-web/chunks/{skills-page-FT7BVZ6X.js → skills-page-A46KVWBL.js} +1 -1
  84. package/dist/dashboard-web/chunks/{team-federation-page-DEYCPUFB.js → team-federation-page-SDUC56DF.js} +1 -1
  85. package/dist/dashboard-web/chunks/{v3-page-4IOWREZG.js → v3-page-J5JOTUSQ.js} +1 -1
  86. package/dist/dashboard-web/chunks/{whiteboards-page-2VMBETZH.js → whiteboards-page-U7LSOAWI.js} +1 -1
  87. package/dist/dashboard-web/style.css +34 -4
  88. package/dist/dashboard.js +16 -1
  89. package/dist/dashboard.js.map +1 -1
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +16 -7
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/zh.d.ts.map +1 -1
  94. package/dist/i18n/zh.js +16 -7
  95. package/dist/i18n/zh.js.map +1 -1
  96. package/dist/im/lark/card-handler.d.ts.map +1 -1
  97. package/dist/im/lark/card-handler.js +3 -0
  98. package/dist/im/lark/card-handler.js.map +1 -1
  99. package/dist/im/lark/event-dispatcher.d.ts +1 -0
  100. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  101. package/dist/im/lark/event-dispatcher.js +1 -1
  102. package/dist/im/lark/event-dispatcher.js.map +1 -1
  103. package/dist/im/lark/schedules-card.d.ts +2 -2
  104. package/dist/im/lark/schedules-card.d.ts.map +1 -1
  105. package/dist/im/lark/schedules-card.js +50 -34
  106. package/dist/im/lark/schedules-card.js.map +1 -1
  107. package/dist/index-daemon.js +13 -1
  108. package/dist/index-daemon.js.map +1 -1
  109. package/dist/services/schedule-store.d.ts +6 -2
  110. package/dist/services/schedule-store.d.ts.map +1 -1
  111. package/dist/services/schedule-store.js +19 -4
  112. package/dist/services/schedule-store.js.map +1 -1
  113. package/dist/skills/definitions.d.ts.map +1 -1
  114. package/dist/skills/definitions.js +4 -5
  115. package/dist/skills/definitions.js.map +1 -1
  116. package/dist/types.d.ts +36 -8
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/utils/child-env.d.ts +40 -1
  119. package/dist/utils/child-env.d.ts.map +1 -1
  120. package/dist/utils/child-env.js +45 -0
  121. package/dist/utils/child-env.js.map +1 -1
  122. package/dist/utils/pending-input-queue.d.ts +10 -0
  123. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  124. package/dist/utils/pending-input-queue.js +15 -0
  125. package/dist/utils/pending-input-queue.js.map +1 -1
  126. package/dist/worker.js +102 -9
  127. package/dist/worker.js.map +1 -1
  128. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +3 -1
  129. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
  130. package/dist/workflows/hostExecutors/botmux-schedule.js +14 -3
  131. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
  132. package/dist/workflows/v3/architect.js +1 -1
  133. package/dist/workflows/v3/architect.js.map +1 -1
  134. package/dist/workflows/v3/contract.d.ts +8 -0
  135. package/dist/workflows/v3/contract.d.ts.map +1 -1
  136. package/dist/workflows/v3/contract.js.map +1 -1
  137. package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
  138. package/dist/workflows/v3/daemon-run.js +2 -0
  139. package/dist/workflows/v3/daemon-run.js.map +1 -1
  140. package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
  141. package/dist/workflows/v3/ephemeral-pool.js +10 -2
  142. package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
  143. package/dist/workflows/v3/run-envelope.d.ts +24 -24
  144. package/dist/workflows/v3/runtime.d.ts +5 -0
  145. package/dist/workflows/v3/runtime.d.ts.map +1 -1
  146. package/dist/workflows/v3/runtime.js +1 -0
  147. package/dist/workflows/v3/runtime.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +0 -1
  150. package/dist/dashboard-web/chunks/schedules-page-Z3672POL.js +0 -1
@@ -0,0 +1,20 @@
1
+ import type { DaemonSession } from './types.js';
2
+ export type DeferredScheduleSettlementResult = {
3
+ action: 'ignored';
4
+ } | {
5
+ action: 'materialized';
6
+ rootMessageId: string;
7
+ } | {
8
+ action: 'closed';
9
+ };
10
+ /** Execute the exact-turn lifecycle decision after a hidden schedule run
11
+ * reaches a terminal/idle edge. Timer debounce lives in daemon.ts; keeping the
12
+ * decision here makes the close-vs-retain contract independently testable. */
13
+ export declare function settleDeferredScheduleRun(ds: DaemonSession, context: {
14
+ turnId: string;
15
+ source: 'terminal' | 'idle';
16
+ }, deps: {
17
+ reconcile: (ds: DaemonSession) => string | undefined;
18
+ closeSession: (sessionId: string) => Promise<unknown>;
19
+ }): Promise<DeferredScheduleSettlementResult>;
20
+ //# sourceMappingURL=deferred-schedule-settlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-schedule-settlement.d.ts","sourceRoot":"","sources":["../../src/core/deferred-schedule-settlement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,gCAAgC,GACxC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEzB;;8EAE8E;AAC9E,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAA;CAAE,EACxD,IAAI,EAAE;IACJ,SAAS,EAAE,CAAC,EAAE,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD,GACA,OAAO,CAAC,gCAAgC,CAAC,CAY3C"}
@@ -0,0 +1,18 @@
1
+ /** Execute the exact-turn lifecycle decision after a hidden schedule run
2
+ * reaches a terminal/idle edge. Timer debounce lives in daemon.ts; keeping the
3
+ * decision here makes the close-vs-retain contract independently testable. */
4
+ export async function settleDeferredScheduleRun(ds, context, deps) {
5
+ const run = ds.session.deferredScheduleRun;
6
+ if (!run || run.turnId !== context.turnId || ds.session.status === 'closed') {
7
+ return { action: 'ignored' };
8
+ }
9
+ if (context.source === 'idle' && ds.lastScreenStatus !== 'idle') {
10
+ return { action: 'ignored' };
11
+ }
12
+ const rootMessageId = deps.reconcile(ds);
13
+ if (rootMessageId)
14
+ return { action: 'materialized', rootMessageId };
15
+ await deps.closeSession(ds.session.sessionId);
16
+ return { action: 'closed' };
17
+ }
18
+ //# sourceMappingURL=deferred-schedule-settlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-schedule-settlement.js","sourceRoot":"","sources":["../../src/core/deferred-schedule-settlement.ts"],"names":[],"mappings":"AAOA;;8EAE8E;AAC9E,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,EAAiB,EACjB,OAAwD,EACxD,IAGC;IAED,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC;IAC3C,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC5E,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,IAAI,EAAE,CAAC,gBAAgB,KAAK,MAAM,EAAE,CAAC;QAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACzC,IAAI,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC;IACpE,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC9C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface DeferredTopicBinding {
2
+ sessionId: string;
3
+ turnId: string;
4
+ chatId: string;
5
+ larkAppId: string;
6
+ routingAnchor: string;
7
+ rootMessageId: string;
8
+ createdAt: string;
9
+ }
10
+ export declare function deferredTopicBindingPath(dataDir: string, sessionId: string): string;
11
+ export declare function readDeferredTopicBinding(dataDir: string, sessionId: string): DeferredTopicBinding | undefined;
12
+ export declare function writeDeferredTopicBinding(dataDir: string, binding: DeferredTopicBinding): void;
13
+ /** Serialize the first-message claim across concurrent `botmux send`
14
+ * processes. The callback may hold the lock across the Lark request: that is
15
+ * intentional, because releasing it before the provider returns would allow
16
+ * two callers to create two roots for the same deferred run. */
17
+ export declare function withDeferredTopicBindingLock<T>(dataDir: string, sessionId: string, fn: () => Promise<T>): Promise<T>;
18
+ export declare function removeDeferredTopicBinding(dataDir: string, sessionId: string): void;
19
+ //# sourceMappingURL=deferred-topic-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-topic-binding.d.ts","sourceRoot":"","sources":["../../src/core/deferred-topic-binding.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,oBAAoB,GAAG,SAAS,CAmBlC;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,oBAAoB,GAC5B,IAAI,CAMN;AAED;;;gEAGgE;AAChE,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAIZ;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAGnF"}
@@ -0,0 +1,55 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { withFileLock } from '../utils/file-lock.js';
5
+ function bindingDir(dataDir) {
6
+ return join(dataDir, 'deferred-topics');
7
+ }
8
+ export function deferredTopicBindingPath(dataDir, sessionId) {
9
+ return join(bindingDir(dataDir), `${sessionId}.json`);
10
+ }
11
+ export function readDeferredTopicBinding(dataDir, sessionId) {
12
+ const path = deferredTopicBindingPath(dataDir, sessionId);
13
+ if (!existsSync(path))
14
+ return undefined;
15
+ try {
16
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
17
+ if (parsed.sessionId !== sessionId
18
+ || typeof parsed.turnId !== 'string'
19
+ || typeof parsed.chatId !== 'string'
20
+ || typeof parsed.larkAppId !== 'string'
21
+ || typeof parsed.routingAnchor !== 'string'
22
+ || typeof parsed.rootMessageId !== 'string'
23
+ || !parsed.rootMessageId.startsWith('om_')
24
+ || typeof parsed.createdAt !== 'string')
25
+ return undefined;
26
+ return parsed;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+ export function writeDeferredTopicBinding(dataDir, binding) {
33
+ const path = deferredTopicBindingPath(dataDir, binding.sessionId);
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
36
+ writeFileSync(tmp, JSON.stringify(binding, null, 2), 'utf8');
37
+ renameSync(tmp, path);
38
+ }
39
+ /** Serialize the first-message claim across concurrent `botmux send`
40
+ * processes. The callback may hold the lock across the Lark request: that is
41
+ * intentional, because releasing it before the provider returns would allow
42
+ * two callers to create two roots for the same deferred run. */
43
+ export function withDeferredTopicBindingLock(dataDir, sessionId, fn) {
44
+ const path = deferredTopicBindingPath(dataDir, sessionId);
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ return withFileLock(path, fn, { maxWaitMs: 30_000 });
47
+ }
48
+ export function removeDeferredTopicBinding(dataDir, sessionId) {
49
+ const path = deferredTopicBindingPath(dataDir, sessionId);
50
+ try {
51
+ rmSync(path, { force: true });
52
+ }
53
+ catch { /* best-effort lifecycle cleanup */ }
54
+ }
55
+ //# sourceMappingURL=deferred-topic-binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deferred-topic-binding.js","sourceRoot":"","sources":["../../src/core/deferred-topic-binding.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ScheduledTask, ParsedSchedule } from '../types.js';
1
+ import type { ScheduledTask, ParsedSchedule, ScheduleExecutionPosition } from '../types.js';
2
2
  export declare function setExecuteCallback(cb: (task: ScheduledTask) => Promise<void>): void;
3
3
  /**
4
4
  * Bind the scheduler to a specific bot (larkAppId). In multi-bot setups every
@@ -33,13 +33,8 @@ interface ParseNLResult {
33
33
  * handler where user types e.g. "/schedule 每日17:50 帮我看看AI新闻".
34
34
  */
35
35
  export declare function parseNaturalSchedule(input: string): ParseNLResult | null;
36
- /**
37
- * Detect a leading "new topic" delivery keyword in a /schedule prompt and strip
38
- * it. Lets users write `/schedule 每日9:00 新话题 帮我看AI新闻` so every fire
39
- * opens a brand-new topic in the chat. Returns the resolved delivery mode plus
40
- * the prompt with the keyword removed. When no keyword is present (or nothing
41
- * follows it) the prompt is returned unchanged with deliver='origin'.
42
- */
36
+ /** Backward-compatible parser; `deliver:new-topic` means a routing modifier
37
+ * was present. New callers should also read extractScheduleModifiers.position. */
43
38
  export declare function extractDeliveryMode(prompt: string): {
44
39
  deliver: 'origin' | 'new-topic';
45
40
  prompt: string;
@@ -57,12 +52,14 @@ export declare function extractSilentMode(prompt: string): {
57
52
  prompt: string;
58
53
  };
59
54
  /**
60
- * Extract both /schedule prompt modifiers (delivery + silent) regardless of
61
- * their order ("静默 新话题 …" / "新话题 静默 …"). The combination is NOT
62
- * validated here callers reject silent+new-topic with a user-facing error.
55
+ * Extract both /schedule prompt modifiers regardless of their order.
56
+ * `deliver:new-topic` remains a compatibility token indicating that a position
57
+ * modifier was present. `executionPosition` carries the unambiguous modern
58
+ * value: group top level or a fresh topic on every run.
63
59
  */
64
60
  export declare function extractScheduleModifiers(prompt: string): {
65
61
  deliver: 'origin' | 'new-topic';
62
+ executionPosition?: Extract<ScheduleExecutionPosition, 'top-level' | 'new-topic'>;
66
63
  silent: boolean;
67
64
  prompt: string;
68
65
  };
@@ -90,6 +87,8 @@ export declare function addTask(params: {
90
87
  chatId: string;
91
88
  rootMessageId?: string;
92
89
  scope?: 'thread' | 'chat';
90
+ executionPosition?: ScheduleExecutionPosition;
91
+ topicTitle?: string;
93
92
  chatType?: 'group' | 'p2p' | 'topic_group';
94
93
  larkAppId?: string;
95
94
  creatorChatId?: string;
@@ -103,6 +102,8 @@ export declare function addTask(params: {
103
102
  deliver?: 'origin' | 'local' | 'new-topic';
104
103
  silent?: boolean;
105
104
  }): ScheduledTask;
105
+ export declare function normalizeTopicTitle(value: string | undefined): string | undefined;
106
+ export declare function resolveTaskExecutionPosition(task: Pick<ScheduledTask, 'executionPosition' | 'scope' | 'rootMessageId' | 'deliver'>): ScheduleExecutionPosition;
106
107
  export declare function removeTask(id: string): boolean;
107
108
  export declare function enableTask(id: string): boolean;
108
109
  export declare function disableTask(id: string): boolean;
@@ -127,22 +128,24 @@ export declare function setEnabled(id: string, enabled: boolean): {
127
128
  error?: string;
128
129
  };
129
130
  /**
130
- * Toggle a task's delivery mode between 'origin' and 'new-topic' and persist.
131
- * Only these two modes participate: 'new-topic' flips to 'origin', anything else
132
- * treated as origin flips to 'new-topic'. The 'local' (log-only, no delivery)
133
- * mode is REFUSED toggling it would silently turn a "don't post" task into an
134
- * in-chat new-topic poster; 'local' is a CLI-only choice. Emits a
135
- * `schedule.updated` event so the dashboard reflects the change immediately.
131
+ * Cycle a task's execution position: retained topic group top level → fresh
132
+ * topic per run retained topic (or group top level when no root is retained).
133
+ * Silent tasks skip the fresh-topic state because that state needs a visible
134
+ * seed message. The `deliver` response remains for cached clients.
136
135
  */
137
136
  export declare function toggleDelivery(id: string): {
138
137
  ok: boolean;
139
138
  error?: string;
140
139
  deliver?: 'origin' | 'new-topic';
140
+ executionPosition?: ScheduleExecutionPosition;
141
141
  };
142
142
  /**
143
- * Update editable fields of a scheduled task (name, prompt, schedule, deliver,
144
- * silent). Re-parses the schedule expression and recomputes nextRunAt when the
145
- * schedule string changes. Enforces the silent↔new-topic mutual exclusion.
143
+ * Update editable fields of a scheduled task (name, prompt, schedule, silent,
144
+ * execution position and retained topic root).
145
+ * Re-parses the schedule expression and recomputes nextRunAt when the schedule
146
+ * string changes. A legacy `deliver` input is accepted and normalized to
147
+ * `origin` for normal writes. Legacy `deliver:new-topic` still maps to the
148
+ * explicit fresh-topic position for cached clients.
146
149
  * Emits a `schedule.updated` event so the dashboard reflects changes live.
147
150
  */
148
151
  export declare function updateTask(id: string, updates: {
@@ -151,6 +154,9 @@ export declare function updateTask(id: string, updates: {
151
154
  schedule?: string;
152
155
  deliver?: 'origin' | 'new-topic';
153
156
  silent?: boolean;
157
+ executionPosition?: ScheduleExecutionPosition;
158
+ rootMessageId?: string;
159
+ topicTitle?: string;
154
160
  }): {
155
161
  ok: boolean;
156
162
  error?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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"}
@@ -243,26 +243,28 @@ export function parseNaturalSchedule(input) {
243
243
  const name = prompt.length > 20 ? prompt.substring(0, 20) + '...' : prompt;
244
244
  return { parsed: zh.parsed, prompt, name };
245
245
  }
246
- /**
247
- * Detect a leading "new topic" delivery keyword in a /schedule prompt and strip
248
- * it. Lets users write `/schedule 每日9:00 新话题 帮我看AI新闻` so every fire
249
- * opens a brand-new topic in the chat. Returns the resolved delivery mode plus
250
- * the prompt with the keyword removed. When no keyword is present (or nothing
251
- * follows it) the prompt is returned unchanged with deliver='origin'.
252
- */
253
- export function extractDeliveryMode(prompt) {
254
- // Match a leading new-topic phrase: optional 每次/每回/每天/每日, then any run of
255
- // 开/起/另/一/个/新/的/space fillers that MUST contain 新, immediately followed
256
- // by 话题. `新` is mandatory so we don't match a normal prompt like
257
- // "总结这个话题…" or "新闻话题…". Covers 新话题 / 新开话题 / 开新话题 /
258
- // 每次开新话题 / 每次开一个新话题 / 新开一个话题 等变体。
246
+ function extractExecutionPositionModifier(prompt) {
247
+ const topLevelZh = prompt.match(/^\s*(?:群消息顶层|群顶层|顶层)(?:执行|运行)?[\s,,、::。-]+(.+)$/s);
248
+ if (topLevelZh && topLevelZh[1].trim())
249
+ return { executionPosition: 'top-level', prompt: topLevelZh[1].trim() };
250
+ const topLevelEn = prompt.match(/^\s*(?:group\s+)?top[\s-]?level[\s,::-]+(.+)$/is);
251
+ if (topLevelEn && topLevelEn[1].trim())
252
+ return { executionPosition: 'top-level', prompt: topLevelEn[1].trim() };
259
253
  const zh = prompt.match(/^\s*(?:每次|每回|每天|每日)?\s*[开起另一个新的\s]*新[开起另一个新的\s]*话题[\s,,、::。-]*(.+)$/s);
260
254
  if (zh && zh[1].trim())
261
- return { deliver: 'new-topic', prompt: zh[1].trim() };
255
+ return { executionPosition: 'new-topic', prompt: zh[1].trim() };
262
256
  const en = prompt.match(/^\s*(?:every\s+run\s+in\s+a\s+)?new[\s-]?topic[\s,::-]+(.+)$/is);
263
257
  if (en && en[1].trim())
264
- return { deliver: 'new-topic', prompt: en[1].trim() };
265
- return { deliver: 'origin', prompt };
258
+ return { executionPosition: 'new-topic', prompt: en[1].trim() };
259
+ return { prompt };
260
+ }
261
+ /** Backward-compatible parser; `deliver:new-topic` means a routing modifier
262
+ * was present. New callers should also read extractScheduleModifiers.position. */
263
+ export function extractDeliveryMode(prompt) {
264
+ const parsed = extractExecutionPositionModifier(prompt);
265
+ return parsed.executionPosition
266
+ ? { deliver: 'new-topic', prompt: parsed.prompt }
267
+ : { deliver: 'origin', prompt };
266
268
  }
267
269
  /**
268
270
  * Detect a leading "silent" keyword in a /schedule prompt and strip it. Lets
@@ -282,19 +284,22 @@ export function extractSilentMode(prompt) {
282
284
  return { silent: false, prompt };
283
285
  }
284
286
  /**
285
- * Extract both /schedule prompt modifiers (delivery + silent) regardless of
286
- * their order ("静默 新话题 …" / "新话题 静默 …"). The combination is NOT
287
- * validated here callers reject silent+new-topic with a user-facing error.
287
+ * Extract both /schedule prompt modifiers regardless of their order.
288
+ * `deliver:new-topic` remains a compatibility token indicating that a position
289
+ * modifier was present. `executionPosition` carries the unambiguous modern
290
+ * value: group top level or a fresh topic on every run.
288
291
  */
289
292
  export function extractScheduleModifiers(prompt) {
290
293
  let deliver = 'origin';
294
+ let executionPosition;
291
295
  let silent = false;
292
296
  let rest = prompt;
293
297
  // Two keywords max — loop twice so either order is handled.
294
298
  for (let i = 0; i < 2; i++) {
295
- const d = extractDeliveryMode(rest);
296
- if (d.deliver === 'new-topic') {
299
+ const d = extractExecutionPositionModifier(rest);
300
+ if (d.executionPosition) {
297
301
  deliver = 'new-topic';
302
+ executionPosition = d.executionPosition;
298
303
  rest = d.prompt;
299
304
  continue;
300
305
  }
@@ -306,7 +311,7 @@ export function extractScheduleModifiers(prompt) {
306
311
  }
307
312
  break;
308
313
  }
309
- return { deliver, silent, prompt: rest };
314
+ return { deliver, ...(executionPosition ? { executionPosition } : {}), silent, prompt: rest };
310
315
  }
311
316
  // ─── next-run computation ───────────────────────────────────────────────────
312
317
  /** Compute the next run time for a parsed schedule. Returns ISO string, or null if exhausted. */
@@ -513,14 +518,19 @@ export function stopScheduler() {
513
518
  logger.info('[scheduler] Stopped');
514
519
  }
515
520
  export function addTask(params) {
516
- // Single choke point for the invalid combination — every creation entry
517
- // (CLI/slash/dashboard/workflow) funnels through here. Entry points give
518
- // friendlier errors first; this guard catches anything they miss.
519
- if (params.silent && params.deliver === 'new-topic') {
520
- throw new Error('silent schedules cannot use deliver:new-topic — a new topic requires a first message');
521
- }
522
521
  const parsed = params.parsed ?? parseSchedule(params.schedule);
523
522
  const nextRunAt = computeNextRun(parsed) ?? undefined;
523
+ const executionPosition = params.executionPosition
524
+ ?? (params.deliver === 'new-topic'
525
+ ? 'new-topic'
526
+ : params.scope === 'chat'
527
+ ? 'top-level'
528
+ : params.rootMessageId ? 'topic' : 'top-level');
529
+ if (executionPosition === 'topic' && !params.rootMessageId) {
530
+ throw new Error('topic_root_required');
531
+ }
532
+ const topicTitle = normalizeTopicTitle(params.topicTitle);
533
+ const scope = executionPosition === 'topic' ? 'thread' : 'chat';
524
534
  const task = scheduleStore.createTask({
525
535
  name: params.name,
526
536
  schedule: params.schedule,
@@ -529,7 +539,9 @@ export function addTask(params) {
529
539
  workingDir: params.workingDir,
530
540
  chatId: params.chatId,
531
541
  rootMessageId: params.rootMessageId,
532
- scope: params.scope,
542
+ scope,
543
+ executionPosition,
544
+ topicTitle,
533
545
  chatType: params.chatType,
534
546
  larkAppId: params.larkAppId,
535
547
  creatorChatId: params.creatorChatId,
@@ -537,12 +549,33 @@ export function addTask(params) {
537
549
  creatorLarkAppId: params.creatorLarkAppId,
538
550
  nextRunAt,
539
551
  repeat: params.repeat,
540
- deliver: params.deliver ?? 'origin',
552
+ // Delivery shape is now expressed by scope/rootMessageId. Persist only the
553
+ // local-vs-chat distinction; schedule-store also normalizes legacy values.
554
+ deliver: params.deliver === 'local' ? 'local' : 'origin',
541
555
  silent: params.silent,
542
556
  });
543
557
  logger.info(`[scheduler] Added task "${task.name}" (${task.id}) — ${parsed.display}, next: ${nextRunAt ?? 'N/A'}`);
544
558
  return task;
545
559
  }
560
+ export function normalizeTopicTitle(value) {
561
+ const trimmed = value?.trim();
562
+ if (!trimmed)
563
+ return undefined;
564
+ const chars = Array.from(trimmed);
565
+ if (chars.length > 200)
566
+ throw new Error('topic_title_too_long');
567
+ return trimmed;
568
+ }
569
+ export function resolveTaskExecutionPosition(task) {
570
+ if (task.executionPosition === 'top-level' || task.executionPosition === 'topic' || task.executionPosition === 'new-topic') {
571
+ return task.executionPosition === 'topic' && !task.rootMessageId ? 'top-level' : task.executionPosition;
572
+ }
573
+ if (task.deliver === 'new-topic')
574
+ return 'new-topic';
575
+ if (task.scope === 'chat')
576
+ return 'top-level';
577
+ return task.rootMessageId ? 'topic' : 'top-level';
578
+ }
546
579
  export function removeTask(id) {
547
580
  return scheduleStore.removeTask(id);
548
581
  }
@@ -648,12 +681,10 @@ export function setEnabled(id, enabled) {
648
681
  return { ok: true };
649
682
  }
650
683
  /**
651
- * Toggle a task's delivery mode between 'origin' and 'new-topic' and persist.
652
- * Only these two modes participate: 'new-topic' flips to 'origin', anything else
653
- * treated as origin flips to 'new-topic'. The 'local' (log-only, no delivery)
654
- * mode is REFUSED toggling it would silently turn a "don't post" task into an
655
- * in-chat new-topic poster; 'local' is a CLI-only choice. Emits a
656
- * `schedule.updated` event so the dashboard reflects the change immediately.
684
+ * Cycle a task's execution position: retained topic group top level → fresh
685
+ * topic per run retained topic (or group top level when no root is retained).
686
+ * Silent tasks skip the fresh-topic state because that state needs a visible
687
+ * seed message. The `deliver` response remains for cached clients.
657
688
  */
658
689
  export function toggleDelivery(id) {
659
690
  const task = scheduleStore.getTask(id);
@@ -661,44 +692,71 @@ export function toggleDelivery(id) {
661
692
  return { ok: false, error: 'not_found' };
662
693
  if (task.deliver === 'local')
663
694
  return { ok: false, error: 'local_not_toggleable' };
664
- // Silent tasks must stay in-place: new-topic needs a first message to open
665
- // the topic, which contradicts "post nothing on fire".
666
- if (task.silent && task.deliver !== 'new-topic') {
667
- return { ok: false, error: 'silent_task_origin_only' };
668
- }
669
- const next = task.deliver === 'new-topic' ? 'origin' : 'new-topic';
670
- scheduleStore.updateTask(id, { deliver: next });
695
+ const current = resolveTaskExecutionPosition(task);
696
+ let executionPosition;
697
+ if (current === 'topic')
698
+ executionPosition = 'top-level';
699
+ else if (current === 'top-level')
700
+ executionPosition = 'new-topic';
701
+ else
702
+ executionPosition = task.rootMessageId ? 'topic' : 'top-level';
703
+ if (executionPosition === current)
704
+ return { ok: false, error: 'topic_root_required' };
705
+ const scope = executionPosition === 'topic' ? 'thread' : 'chat';
706
+ scheduleStore.updateTask(id, { scope, executionPosition });
707
+ const deliver = executionPosition === 'new-topic' ? 'new-topic' : 'origin';
671
708
  dashboardEventBus.publish({
672
709
  type: 'schedule.updated',
673
- body: { id, patch: { deliver: next } },
710
+ body: { id, patch: { scope, executionPosition } },
674
711
  });
675
- return { ok: true, deliver: next };
712
+ return { ok: true, deliver, executionPosition };
676
713
  }
677
714
  /**
678
- * Update editable fields of a scheduled task (name, prompt, schedule, deliver,
679
- * silent). Re-parses the schedule expression and recomputes nextRunAt when the
680
- * schedule string changes. Enforces the silent↔new-topic mutual exclusion.
715
+ * Update editable fields of a scheduled task (name, prompt, schedule, silent,
716
+ * execution position and retained topic root).
717
+ * Re-parses the schedule expression and recomputes nextRunAt when the schedule
718
+ * string changes. A legacy `deliver` input is accepted and normalized to
719
+ * `origin` for normal writes. Legacy `deliver:new-topic` still maps to the
720
+ * explicit fresh-topic position for cached clients.
681
721
  * Emits a `schedule.updated` event so the dashboard reflects changes live.
682
722
  */
683
723
  export function updateTask(id, updates) {
684
724
  const task = scheduleStore.getTask(id);
685
725
  if (!task)
686
726
  return { ok: false, error: 'not_found' };
687
- // Enforce silent ↔ new-topic mutual exclusion (same rule as addTask).
688
- const nextDeliver = updates.deliver ?? task.deliver ?? 'origin';
689
- const nextSilent = updates.silent ?? task.silent === true;
690
- if (nextSilent && nextDeliver === 'new-topic') {
691
- return { ok: false, error: 'silent_new_topic_exclusive' };
692
- }
693
727
  const patch = {};
694
728
  if (updates.name !== undefined)
695
729
  patch.name = updates.name;
696
730
  if (updates.prompt !== undefined)
697
731
  patch.prompt = updates.prompt;
698
- if (updates.deliver !== undefined)
699
- patch.deliver = updates.deliver;
700
732
  if (updates.silent !== undefined)
701
733
  patch.silent = updates.silent === true ? true : undefined;
734
+ const legacyPosition = updates.deliver === 'new-topic'
735
+ ? 'new-topic'
736
+ : undefined;
737
+ const executionPosition = updates.executionPosition ?? legacyPosition;
738
+ const nextRootMessageId = updates.rootMessageId ?? task.rootMessageId;
739
+ if (executionPosition === 'topic' && !nextRootMessageId) {
740
+ return { ok: false, error: 'topic_root_required' };
741
+ }
742
+ if (updates.topicTitle !== undefined) {
743
+ try {
744
+ patch.topicTitle = normalizeTopicTitle(updates.topicTitle);
745
+ }
746
+ catch (err) {
747
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
748
+ }
749
+ }
750
+ if (updates.rootMessageId !== undefined)
751
+ patch.rootMessageId = updates.rootMessageId;
752
+ if (executionPosition !== undefined) {
753
+ patch.scope = executionPosition === 'topic' ? 'thread' : 'chat';
754
+ patch.executionPosition = executionPosition;
755
+ patch.deliver = 'origin';
756
+ }
757
+ else if (updates.deliver !== undefined) {
758
+ patch.deliver = 'origin';
759
+ }
702
760
  // Re-parse + recompute next run when the schedule expression changes.
703
761
  if (updates.schedule !== undefined && updates.schedule !== task.schedule) {
704
762
  let parsed;