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.
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +4 -0
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +8 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/cli/deferred-topic-send.d.ts +37 -0
- package/dist/cli/deferred-topic-send.d.ts.map +1 -0
- package/dist/cli/deferred-topic-send.js +69 -0
- package/dist/cli/deferred-topic-send.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +160 -34
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +17 -12
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +4 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +78 -8
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/deferred-schedule-settlement.d.ts +20 -0
- package/dist/core/deferred-schedule-settlement.d.ts.map +1 -0
- package/dist/core/deferred-schedule-settlement.js +18 -0
- package/dist/core/deferred-schedule-settlement.js.map +1 -0
- package/dist/core/deferred-topic-binding.d.ts +19 -0
- package/dist/core/deferred-topic-binding.d.ts.map +1 -0
- package/dist/core/deferred-topic-binding.js +55 -0
- package/dist/core/deferred-topic-binding.js.map +1 -0
- package/dist/core/scheduler.d.ts +26 -20
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +114 -56
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +47 -15
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +17 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +178 -84
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +7 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +51 -1
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +90 -5
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/schedule-card-model.d.ts +12 -3
- package/dist/dashboard/schedule-card-model.d.ts.map +1 -1
- package/dist/dashboard/schedule-card-model.js +32 -8
- package/dist/dashboard/schedule-card-model.js.map +1 -1
- package/dist/dashboard/web/dashboard-components.d.ts +7 -0
- package/dist/dashboard/web/dashboard-components.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-components.js +11 -2
- package/dist/dashboard/web/dashboard-components.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +30 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts +2 -0
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +74 -27
- package/dist/dashboard/web/schedules-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-O37SAQD5.js → bot-defaults-page-B5AY4B2J.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-6QHMLMIM.js → chunk-CUV65BIQ.js} +3 -3
- package/dist/dashboard-web/chunks/{chunk-O2DPQL7C.js → chunk-JNRSPKLS.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FYALHCPG.js → chunk-P6GO5KT4.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-QFMH6ITB.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-B42PCL5P.js → chunk-UBOLHQCK.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-DZBQ42MH.js → connectors-page-KTC2QWNW.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-3IEGRZKQ.js → groups-page-B3EKGOLY.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-X3ADRON3.js → insights-page-X6RXNCWX.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-IV36RGQY.js → monitor-room-LLRA5LQW.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-ADFKVON4.js → monitoring-page-7MT5FEOS.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-CALAOWNS.js → office-page-KD46D5TD.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-SLMGLK3M.js → overview-page-JUWNH32O.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-L7JJ4FFV.js → roles-page-P6F7NLOI.js} +1 -1
- package/dist/dashboard-web/chunks/schedules-page-XIHWZST4.js +1 -0
- package/dist/dashboard-web/chunks/{sessions-page-OKJ5YWUZ.js → sessions-page-2VECQ27T.js} +1 -1
- package/dist/dashboard-web/chunks/{settings-page-P6MI2ZUE.js → settings-page-HMIUE2JA.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-FT7BVZ6X.js → skills-page-A46KVWBL.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-DEYCPUFB.js → team-federation-page-SDUC56DF.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-4IOWREZG.js → v3-page-J5JOTUSQ.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-2VMBETZH.js → whiteboards-page-U7LSOAWI.js} +1 -1
- package/dist/dashboard-web/style.css +34 -4
- package/dist/dashboard.js +16 -1
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -7
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +16 -7
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +3 -0
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +1 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +1 -1
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/schedules-card.d.ts +2 -2
- package/dist/im/lark/schedules-card.d.ts.map +1 -1
- package/dist/im/lark/schedules-card.js +50 -34
- package/dist/im/lark/schedules-card.js.map +1 -1
- package/dist/index-daemon.js +13 -1
- package/dist/index-daemon.js.map +1 -1
- package/dist/services/schedule-store.d.ts +6 -2
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +19 -4
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +4 -5
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +36 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +40 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +45 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/pending-input-queue.d.ts +10 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +15 -0
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/worker.js +102 -9
- package/dist/worker.js.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts +3 -1
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.js +14 -3
- package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
- package/dist/workflows/v3/architect.js +1 -1
- package/dist/workflows/v3/architect.js.map +1 -1
- package/dist/workflows/v3/contract.d.ts +8 -0
- package/dist/workflows/v3/contract.d.ts.map +1 -1
- package/dist/workflows/v3/contract.js.map +1 -1
- package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
- package/dist/workflows/v3/daemon-run.js +2 -0
- package/dist/workflows/v3/daemon-run.js.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.d.ts.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.js +10 -2
- package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +24 -24
- package/dist/workflows/v3/runtime.d.ts +5 -0
- package/dist/workflows/v3/runtime.d.ts.map +1 -1
- package/dist/workflows/v3/runtime.js +1 -0
- package/dist/workflows/v3/runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/chunk-I35DXGM4.js +0 -1
- package/dist/dashboard-web/chunks/schedules-page-Z3672POL.js +0 -1
|
@@ -24,7 +24,7 @@ import { composeRowFromActive } from './dashboard-rows.js';
|
|
|
24
24
|
import { composeSpawnCodexAppContext, composeSpawnUserContent, deriveSessionTitleFromContent, } from './session-create.js';
|
|
25
25
|
import { validateZellijAdoptTarget } from './zellij-adopt-discovery.js';
|
|
26
26
|
import { addCodexAppContext } from '../utils/codex-app-context.js';
|
|
27
|
-
import { activeSessionKey, sessionKey, sessionAnchorId } from './types.js';
|
|
27
|
+
import { activeSessionKey, sessionKey, sessionAnchorId, storedSessionAnchorId } from './types.js';
|
|
28
28
|
import { announceSessionRow, markSessionActivity, announcePendingRepoSession } from './session-activity.js';
|
|
29
29
|
import { scanMultipleProjects } from '../services/project-scanner.js';
|
|
30
30
|
import { buildRepoSelectCard } from '../im/lark/card-builder.js';
|
|
@@ -37,6 +37,9 @@ import { ensureDefaultWhiteboard, getWhiteboard, whiteboardEnabled } from '../se
|
|
|
37
37
|
import { botAutoWorktreeEnabled } from '../services/default-worktree.js';
|
|
38
38
|
import { armSilentScheduledTurn, disarmSilentScheduledTurn } from './silent-schedule-turns.js';
|
|
39
39
|
import { getAttachmentsDir } from './attachment-path.js';
|
|
40
|
+
import { resolveRegularGroupMode } from '../services/chat-reply-mode-store.js';
|
|
41
|
+
import { beginReplyTargetTurn } from './reply-target.js';
|
|
42
|
+
import { readDeferredTopicBinding, removeDeferredTopicBinding } from './deferred-topic-binding.js';
|
|
40
43
|
export { getAttachmentsDir } from './attachment-path.js';
|
|
41
44
|
function sessionCreatedAtMs(session) {
|
|
42
45
|
return session.createdAt ? (Date.parse(session.createdAt) || Date.now()) : Date.now();
|
|
@@ -1052,8 +1055,9 @@ export async function restoreActiveSessions(activeSessions) {
|
|
|
1052
1055
|
// Restored sessions persisted before the scope field was added default to
|
|
1053
1056
|
// 'thread' — that matches the legacy thread-only behaviour.
|
|
1054
1057
|
const scope = session.scope === 'chat' ? 'chat' : 'thread';
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1058
|
+
// Persisted metadata is the authoritative adopt marker. The title is
|
|
1059
|
+
// user-editable via /rename and must not change restore semantics.
|
|
1060
|
+
if (session.adoptedFrom) {
|
|
1057
1061
|
const adopted = session.adoptedFrom;
|
|
1058
1062
|
const validation = adopted.zellijPaneId
|
|
1059
1063
|
? (typeof adopted.originalCliPid === 'number' && validateZellijAdoptTarget(adopted.zellijSession ?? '', adopted.zellijPaneId, adopted.originalCliPid, adopted.cliId) ? 'alive' : 'missing')
|
|
@@ -1118,7 +1122,8 @@ export async function restoreActiveSessions(activeSessions) {
|
|
|
1118
1122
|
logger.info(`[${session.sessionId.substring(0, 8)}] Restored adopt session (target: ${adoptTargetLabel(adopted)}, scope: ${scope})`);
|
|
1119
1123
|
continue;
|
|
1120
1124
|
}
|
|
1121
|
-
//
|
|
1125
|
+
// Title-only adopt sessions have no target metadata and can only come from
|
|
1126
|
+
// legacy records. They cannot be validated or safely restored.
|
|
1122
1127
|
if (session.title?.startsWith('Adopt:')) {
|
|
1123
1128
|
logger.debug(`Closing adopt session ${session.sessionId} (no persisted metadata)`);
|
|
1124
1129
|
sessionStore.closeSession(session.sessionId);
|
|
@@ -1195,6 +1200,33 @@ export async function restoreActiveSessions(activeSessions) {
|
|
|
1195
1200
|
// patch a streaming card. Cleared on the first real CLI input.
|
|
1196
1201
|
suppressRecoveryCard: true,
|
|
1197
1202
|
};
|
|
1203
|
+
if (session.deferredScheduleRun) {
|
|
1204
|
+
const binding = readDeferredTopicBinding(config.session.dataDir, session.sessionId);
|
|
1205
|
+
if (!binding) {
|
|
1206
|
+
// A daemon restart makes an unmaterialized hidden run ambiguous: there
|
|
1207
|
+
// is no visible conversation to resume and no safe user-facing place
|
|
1208
|
+
// to report recovery. Fence its backing process and close the audit row
|
|
1209
|
+
// instead of resurrecting an invisible worker.
|
|
1210
|
+
const backendType = getSessionPersistentBackendType(ds);
|
|
1211
|
+
if (backendType) {
|
|
1212
|
+
killPersistentSession(backendType, persistentSessionName(backendType, session.sessionId));
|
|
1213
|
+
}
|
|
1214
|
+
sessionStore.closeSession(session.sessionId);
|
|
1215
|
+
removeDeferredTopicBinding(config.session.dataDir, session.sessionId);
|
|
1216
|
+
logger.info(`[${session.sessionId.substring(0, 8)}] Closed unmaterialized deferred schedule run during restore`);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const nowIso = new Date().toISOString();
|
|
1220
|
+
const aliases = { ...(session.replyThreadAliases ?? {}) };
|
|
1221
|
+
aliases[binding.rootMessageId] = {
|
|
1222
|
+
createdAt: aliases[binding.rootMessageId]?.createdAt ?? binding.createdAt,
|
|
1223
|
+
lastUsedAt: nowIso,
|
|
1224
|
+
};
|
|
1225
|
+
session.replyThreadAliases = aliases;
|
|
1226
|
+
session.rootMessageId = binding.rootMessageId;
|
|
1227
|
+
ds.replyThreadAliases = aliases;
|
|
1228
|
+
sessionStore.updateSession(session);
|
|
1229
|
+
}
|
|
1198
1230
|
if (await closeActiveSessionIfCliMismatch(ds))
|
|
1199
1231
|
continue;
|
|
1200
1232
|
const anchor = sessionAnchorId(ds);
|
|
@@ -1229,6 +1261,12 @@ export async function restoreActiveSessions(activeSessions) {
|
|
|
1229
1261
|
// 会把「找不到 backing」误判成僵尸而关掉它。
|
|
1230
1262
|
if (ds.session.queued)
|
|
1231
1263
|
continue;
|
|
1264
|
+
// External /adopt sessions were already validated and re-forked above
|
|
1265
|
+
// against their real tmux/zellij target. They never own Botmux's
|
|
1266
|
+
// deterministic bmx-<sessionId> backing session, so probing that name here
|
|
1267
|
+
// would immediately zombie-close the session we just restored.
|
|
1268
|
+
if (ds.adoptedFrom)
|
|
1269
|
+
continue;
|
|
1232
1270
|
const backendType = getSessionPersistentBackendType(ds);
|
|
1233
1271
|
if (!backendType)
|
|
1234
1272
|
continue;
|
|
@@ -1362,6 +1400,8 @@ export async function ensureTerminalWorkerPort(ds) {
|
|
|
1362
1400
|
* - 'vc_receiver_managed' — dedicated meeting receivers are reconstructed
|
|
1363
1401
|
* through the meeting membership/hub lifecycle; a
|
|
1364
1402
|
* manual resume could resurrect a stale member epoch
|
|
1403
|
+
* - 'deferred_unmaterialized' — a silent fresh-topic run finished without
|
|
1404
|
+
* publishing, so it has no conversation to resume
|
|
1365
1405
|
*/
|
|
1366
1406
|
export async function resumeSession(sessionId, activeSessions) {
|
|
1367
1407
|
const session = sessionStore.getSession(sessionId);
|
|
@@ -1380,6 +1420,14 @@ export async function resumeSession(sessionId, activeSessions) {
|
|
|
1380
1420
|
if (session.vcMeetingReceiver) {
|
|
1381
1421
|
return { ok: false, error: 'vc_receiver_managed' };
|
|
1382
1422
|
}
|
|
1423
|
+
// Auto-closed invisible schedule runs are audit records, not conversations.
|
|
1424
|
+
// Without a materialized binding, resuming one would wake a virtual chat-
|
|
1425
|
+
// scope anchor that no inbound Lark message can reach; a later botmux send
|
|
1426
|
+
// would also fall through to the ordinary group top level. Keep it closed.
|
|
1427
|
+
if (session.deferredScheduleRun
|
|
1428
|
+
&& !readDeferredTopicBinding(config.session.dataDir, session.sessionId)) {
|
|
1429
|
+
return { ok: false, error: 'deferred_unmaterialized' };
|
|
1430
|
+
}
|
|
1383
1431
|
// Adopt sessions don't survive /close — the user's tmux pane and original
|
|
1384
1432
|
// CLI pid have already moved on, and bringing the bridge back without a live
|
|
1385
1433
|
// pane is meaningless.
|
|
@@ -1388,7 +1436,7 @@ export async function resumeSession(sessionId, activeSessions) {
|
|
|
1388
1436
|
}
|
|
1389
1437
|
const scope = session.scope === 'chat' ? 'chat' : 'thread';
|
|
1390
1438
|
const larkAppId = session.larkAppId ?? getAllBots()[0]?.config.larkAppId ?? '';
|
|
1391
|
-
const anchor =
|
|
1439
|
+
const anchor = storedSessionAnchorId(session);
|
|
1392
1440
|
const key = sessionKey(anchor, larkAppId);
|
|
1393
1441
|
// In-memory occupant check. A daemon-command scratch (e.g. an unconfirmed
|
|
1394
1442
|
// `/relay` picker, a bare `/help`) parks a worker:null placeholder at this
|
|
@@ -1439,7 +1487,7 @@ export async function resumeSession(sessionId, activeSessions) {
|
|
|
1439
1487
|
&& s.status === 'active'
|
|
1440
1488
|
&& (s.larkAppId ?? '') === larkAppId
|
|
1441
1489
|
&& (s.scope === 'chat' ? 'chat' : 'thread') === scope
|
|
1442
|
-
&& (
|
|
1490
|
+
&& storedSessionAnchorId(s) === anchor);
|
|
1443
1491
|
const realConflict = conflicts.find(s => !!s.cliId || !!s.lastCliInput);
|
|
1444
1492
|
if (realConflict) {
|
|
1445
1493
|
return { ok: false, error: 'anchor_occupied', activeSessionId: realConflict.sessionId };
|
|
@@ -1516,6 +1564,40 @@ export function buildSilentScheduleHint(taskName, locale) {
|
|
|
1516
1564
|
'</botmux_silent_schedule>',
|
|
1517
1565
|
].join('\n');
|
|
1518
1566
|
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Resolve the durable execution position of a scheduled task.
|
|
1569
|
+
*
|
|
1570
|
+
* Schedule execution position is task-level state: top-level starts from the
|
|
1571
|
+
* group top level, topic continues under a retained root, and new-topic posts a
|
|
1572
|
+
* fresh seed on every run. Legacy `deliver:new-topic` rows resolve to that third
|
|
1573
|
+
* state until the store normalizes them.
|
|
1574
|
+
*
|
|
1575
|
+
* A malformed/legacy `scope:'thread'` task without a root cannot reply in a
|
|
1576
|
+
* thread. Treat it as chat-scope so silent runs remain genuinely silent rather
|
|
1577
|
+
* than posting a banner merely to manufacture an anchor.
|
|
1578
|
+
*/
|
|
1579
|
+
export function resolveScheduledTaskScope(task) {
|
|
1580
|
+
if (task.executionPosition === 'topic' && task.rootMessageId)
|
|
1581
|
+
return 'thread';
|
|
1582
|
+
if (task.executionPosition === 'top-level' || task.executionPosition === 'new-topic')
|
|
1583
|
+
return 'chat';
|
|
1584
|
+
if (task.deliver === 'new-topic')
|
|
1585
|
+
return 'chat';
|
|
1586
|
+
if (task.scope === 'chat')
|
|
1587
|
+
return 'chat';
|
|
1588
|
+
return task.rootMessageId ? 'thread' : 'chat';
|
|
1589
|
+
}
|
|
1590
|
+
export function resolveScheduledTaskExecutionPosition(task) {
|
|
1591
|
+
if (task.executionPosition === 'new-topic')
|
|
1592
|
+
return 'new-topic';
|
|
1593
|
+
if (task.executionPosition === 'topic' && task.rootMessageId)
|
|
1594
|
+
return 'topic';
|
|
1595
|
+
if (task.executionPosition === 'top-level')
|
|
1596
|
+
return 'top-level';
|
|
1597
|
+
if (task.deliver === 'new-topic')
|
|
1598
|
+
return 'new-topic';
|
|
1599
|
+
return task.scope !== 'chat' && task.rootMessageId ? 'topic' : 'top-level';
|
|
1600
|
+
}
|
|
1519
1601
|
export async function executeScheduledTask(task, activeSessions, refreshCliVersion) {
|
|
1520
1602
|
// Resolve which bot to use — prefer the task's original bot so replies come from
|
|
1521
1603
|
// the same account the user set up the schedule with.
|
|
@@ -1535,29 +1617,18 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1535
1617
|
}
|
|
1536
1618
|
const larkAppId = bot.config.larkAppId;
|
|
1537
1619
|
const { getChatMode, sendMessage, replyMessage } = await import('../im/lark/client.js');
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
|
|
1541
|
-
const scope = task
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
|
|
1550
|
-
// those combinations; if a stored task still reaches here, fall back to a
|
|
1551
|
-
// loud fire rather than dropping the run.
|
|
1552
|
-
let silent = task.silent === true;
|
|
1553
|
-
if (silent && task.deliver === 'new-topic') {
|
|
1554
|
-
logger.warn(`[scheduler] Task ${task.id} is silent+new-topic (should be rejected at creation); firing loud`);
|
|
1555
|
-
silent = false;
|
|
1556
|
-
}
|
|
1557
|
-
if (silent && scope === 'thread' && !task.rootMessageId) {
|
|
1558
|
-
logger.warn(`[scheduler] Silent task ${task.id} has no thread anchor; firing loud`);
|
|
1559
|
-
silent = false;
|
|
1560
|
-
}
|
|
1620
|
+
// Prefer the explicit three-state position, with scope/deliver fallbacks for
|
|
1621
|
+
// schedules persisted by older versions.
|
|
1622
|
+
const executionPosition = resolveScheduledTaskExecutionPosition(task);
|
|
1623
|
+
const scope = resolveScheduledTaskScope(task);
|
|
1624
|
+
// Silent execution posts no "🕐 task started" banner / creator notice.
|
|
1625
|
+
// A silent fresh-topic run receives a durable virtual anchor below; its real
|
|
1626
|
+
// Lark root is materialized only by the first successful `botmux send`.
|
|
1627
|
+
const silent = task.silent === true;
|
|
1628
|
+
// Every fire gets a stable identity before it enters a worker queue. Besides
|
|
1629
|
+
// silent-output suppression, this identifies the exact shared-topic reply
|
|
1630
|
+
// target when a chat-scope session already has another turn queued.
|
|
1631
|
+
const scheduledTurnId = `schedule:${task.id}:${randomUUID()}`;
|
|
1561
1632
|
// Decide where to route the "🕐 task started" notification and where the
|
|
1562
1633
|
// session conversation lands.
|
|
1563
1634
|
//
|
|
@@ -1570,68 +1641,77 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1570
1641
|
// post in the chat (one-shot session)
|
|
1571
1642
|
//
|
|
1572
1643
|
// Chat-scope (auto-adopt / 普通群): post the start notification straight to
|
|
1573
|
-
// the chat without reply_in_thread
|
|
1644
|
+
// the chat without reply_in_thread. The bot/chat regular-group mode then
|
|
1645
|
+
// decides whether that top-level trigger stays flat, opens a shared topic, or
|
|
1646
|
+
// starts an independent topic/session. Silent top-level fires have no
|
|
1647
|
+
// trigger message, so they retain the ordinary chat-scope behavior.
|
|
1574
1648
|
let anchor;
|
|
1575
1649
|
let isContinuation = false;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1650
|
+
let sharedTopicRootId;
|
|
1651
|
+
if (executionPosition === 'new-topic') {
|
|
1652
|
+
if (silent) {
|
|
1653
|
+
anchor = `schedule-run:${task.id}:${scheduledTurnId.slice(scheduledTurnId.lastIndexOf(':') + 1)}`;
|
|
1654
|
+
isContinuation = false;
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
if (task.creatorRootMessageId && task.creatorChatId !== task.chatId) {
|
|
1658
|
+
const creatorAppId = task.creatorLarkAppId ?? larkAppId;
|
|
1659
|
+
replyMessage(creatorAppId, task.creatorRootMessageId, t('scheduler.task_triggered_target_chat', { name: task.name }, localeForBot(creatorAppId)), 'text', true).catch((err) => {
|
|
1660
|
+
logger.warn(`[scheduler] Failed to notify creator thread ${task.creatorRootMessageId} (${err.message})`);
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
const topicSeed = task.topicTitle?.trim()
|
|
1664
|
+
|| t('scheduler.task_started', { name: task.name }, localeForBot(larkAppId));
|
|
1665
|
+
anchor = await sendMessage(larkAppId, task.chatId, topicSeed);
|
|
1666
|
+
isContinuation = false;
|
|
1667
|
+
}
|
|
1584
1668
|
}
|
|
1585
1669
|
else if (scope === 'chat') {
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1670
|
+
// Explicit task choice: chat scope always starts at the group top level.
|
|
1671
|
+
// A retained rootMessageId is only a bookmark that lets the user switch the
|
|
1672
|
+
// task back to topic execution later; it must not override this choice.
|
|
1589
1673
|
const chatMode = await getChatMode(larkAppId, task.chatId, { forceRefresh: true });
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
// the model's own `botmux send` surfaces the failure.
|
|
1594
|
-
anchor = task.rootMessageId;
|
|
1595
|
-
isContinuation = true;
|
|
1596
|
-
}
|
|
1597
|
-
else {
|
|
1598
|
-
try {
|
|
1599
|
-
await replyMessage(larkAppId, task.rootMessageId, t('scheduler.task_started', { name: task.name }, localeForBot(larkAppId)), 'text', true);
|
|
1600
|
-
anchor = task.rootMessageId;
|
|
1601
|
-
isContinuation = true;
|
|
1602
|
-
}
|
|
1603
|
-
catch (err) {
|
|
1604
|
-
logger.warn(`[scheduler] Failed to reply in converted topic chat ${task.rootMessageId} (${err.message}); falling back to new thread`);
|
|
1605
|
-
anchor = await sendMessage(larkAppId, task.chatId, t('scheduler.task_started', { name: task.name }, localeForBot(larkAppId)));
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1674
|
+
let topLevelTriggerId;
|
|
1675
|
+
if (silent) {
|
|
1676
|
+
// No banner / creator notice — the chat itself is the anchor.
|
|
1608
1677
|
}
|
|
1609
1678
|
else {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1679
|
+
// A cross-chat task keeps the creator informed, but the target chat must
|
|
1680
|
+
// still receive its own top-level trigger. Besides making the execution
|
|
1681
|
+
// visible where it runs, that message is the anchor required by the
|
|
1682
|
+
// target bot/chat's shared or independent-topic regular-group mode.
|
|
1683
|
+
if (task.creatorRootMessageId && task.creatorChatId !== task.chatId) {
|
|
1614
1684
|
const creatorAppId = task.creatorLarkAppId ?? larkAppId;
|
|
1615
1685
|
replyMessage(creatorAppId, task.creatorRootMessageId, t('scheduler.task_triggered_target_chat', { name: task.name }, localeForBot(creatorAppId)), 'text', true).catch((err) => {
|
|
1616
1686
|
logger.warn(`[scheduler] Failed to notify creator thread ${task.creatorRootMessageId} (${err.message})`);
|
|
1617
1687
|
});
|
|
1618
1688
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
}
|
|
1624
|
-
catch (err) {
|
|
1625
|
-
logger.warn(`[scheduler] Failed to post start banner in chat ${task.chatId} (${err.message})`);
|
|
1626
|
-
}
|
|
1689
|
+
try {
|
|
1690
|
+
topLevelTriggerId = await sendMessage(larkAppId, task.chatId, t('scheduler.task_started', { name: task.name }, localeForBot(larkAppId)));
|
|
1691
|
+
}
|
|
1692
|
+
catch (err) {
|
|
1693
|
+
logger.warn(`[scheduler] Failed to post start banner in chat ${task.chatId} (${err.message})`);
|
|
1627
1694
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1695
|
+
}
|
|
1696
|
+
// Mirror ordinary top-level message routing. A topic group always owns a
|
|
1697
|
+
// thread per top-level message; a regular group only forks an independent
|
|
1698
|
+
// thread when its resolved mode is `new-topic`. `shared` keeps the stable
|
|
1699
|
+
// chat-scope session but routes this exact turn's output under the banner.
|
|
1700
|
+
const regularGroupMode = task.chatType === 'p2p'
|
|
1701
|
+
? 'chat'
|
|
1702
|
+
: resolveRegularGroupMode(larkAppId, task.chatId);
|
|
1703
|
+
const opensIndependentTopic = !!topLevelTriggerId
|
|
1704
|
+
&& (chatMode === 'topic' || regularGroupMode === 'new-topic');
|
|
1705
|
+
if (opensIndependentTopic) {
|
|
1706
|
+
anchor = topLevelTriggerId;
|
|
1707
|
+
isContinuation = false;
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1633
1710
|
anchor = task.chatId;
|
|
1634
1711
|
isContinuation = !!activeSessions.get(sessionKey(anchor, larkAppId));
|
|
1712
|
+
if (topLevelTriggerId && chatMode === 'group' && regularGroupMode === 'shared') {
|
|
1713
|
+
sharedTopicRootId = topLevelTriggerId;
|
|
1714
|
+
}
|
|
1635
1715
|
}
|
|
1636
1716
|
}
|
|
1637
1717
|
else {
|
|
@@ -1678,16 +1758,16 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1678
1758
|
const firePrompt = silent
|
|
1679
1759
|
? `${buildSilentScheduleHint(task.name, localeForBot(larkAppId))}\n\n${task.prompt}`
|
|
1680
1760
|
: task.prompt;
|
|
1681
|
-
// Every fire gets a stable identity before it enters a worker queue. Silent
|
|
1682
|
-
// output suppression follows this exact id instead of mutable session state,
|
|
1683
|
-
// so overlapping/queued normal turns retain their own delivery semantics.
|
|
1684
|
-
const scheduledTurnId = `schedule:${task.id}:${randomUUID()}`;
|
|
1685
1761
|
// Inject into a live session if one already exists at this anchor.
|
|
1686
1762
|
const existing = activeSessions.get(sessionKey(anchor, larkAppId));
|
|
1687
1763
|
if (isContinuation && existing?.worker && !existing.worker.killed) {
|
|
1688
1764
|
markSessionActivity(existing);
|
|
1689
1765
|
try {
|
|
1690
1766
|
ensureSessionWhiteboard(existing);
|
|
1767
|
+
if (sharedTopicRootId) {
|
|
1768
|
+
beginReplyTargetTurn(existing, sharedTopicRootId, scheduledTurnId);
|
|
1769
|
+
sessionStore.updateSession(existing.session);
|
|
1770
|
+
}
|
|
1691
1771
|
const input = buildFollowUpCliInput(firePrompt, existing.session.sessionId, {
|
|
1692
1772
|
isAdoptMode: false,
|
|
1693
1773
|
cliId: existing.session.cliId ?? bot.config.cliId,
|
|
@@ -1719,13 +1799,23 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1719
1799
|
// chatId-as-seed for audit (sessionAnchorId() returns chatId via scope). If a
|
|
1720
1800
|
// formerly chat-scope task was redirected into a converted topic chat, promote
|
|
1721
1801
|
// the runtime session to thread-scope so follow-up replies stay in-thread.
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1802
|
+
const deferredFreshTopic = executionPosition === 'new-topic' && silent;
|
|
1803
|
+
const runtimeScope = deferredFreshTopic
|
|
1804
|
+
? 'chat'
|
|
1805
|
+
: scope === 'chat' && anchor !== task.chatId ? 'thread' : scope;
|
|
1725
1806
|
const session = sessionStore.createSession(task.chatId, anchor, `${t('schedule.title_prefix', undefined, localeForBot(larkAppId))} ${task.name}`, task.chatType === 'p2p' ? 'p2p' : 'group');
|
|
1726
1807
|
const now = Date.now();
|
|
1727
1808
|
session.larkAppId = larkAppId;
|
|
1728
1809
|
session.scope = runtimeScope;
|
|
1810
|
+
if (deferredFreshTopic) {
|
|
1811
|
+
session.deferredScheduleRun = {
|
|
1812
|
+
taskId: task.id,
|
|
1813
|
+
turnId: scheduledTurnId,
|
|
1814
|
+
routingAnchor: anchor,
|
|
1815
|
+
...(task.topicTitle?.trim() ? { topicTitle: task.topicTitle.trim() } : {}),
|
|
1816
|
+
createdAt: new Date(now).toISOString(),
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1729
1819
|
session.lastMessageAt = new Date(now).toISOString();
|
|
1730
1820
|
sessionStore.updateSession(session);
|
|
1731
1821
|
messageQueue.ensureQueue(anchor);
|
|
@@ -1744,6 +1834,10 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1744
1834
|
hasHistory: isContinuation,
|
|
1745
1835
|
workingDir: task.workingDir,
|
|
1746
1836
|
};
|
|
1837
|
+
if (sharedTopicRootId) {
|
|
1838
|
+
beginReplyTargetTurn(ds, sharedTopicRootId, scheduledTurnId);
|
|
1839
|
+
sessionStore.updateSession(ds.session);
|
|
1840
|
+
}
|
|
1747
1841
|
ensureSessionWhiteboard(ds);
|
|
1748
1842
|
const prompt = buildNewTopicCliInput(firePrompt, session.sessionId, bot.config.cliId, bot.config.cliPathOverride, undefined, undefined, undefined, undefined, { name: bot.botName, openId: bot.botOpenId }, localeForBot(larkAppId), undefined, { larkAppId, chatId: task.chatId, whiteboardId: ds.session.whiteboardId });
|
|
1749
1843
|
activeSessions.set(sessionKey(anchor, larkAppId), ds);
|
|
@@ -1758,7 +1852,7 @@ export async function executeScheduledTask(task, activeSessions, refreshCliVersi
|
|
|
1758
1852
|
disarmSilentScheduledTurn(ds, scheduledTurnId);
|
|
1759
1853
|
throw err;
|
|
1760
1854
|
}
|
|
1761
|
-
logger.info(`[scheduler] Task "${task.name}" spawned (session: ${session.sessionId}, scope: ${
|
|
1855
|
+
logger.info(`[scheduler] Task "${task.name}" spawned (session: ${session.sessionId}, scope: ${runtimeScope}, anchor: ${anchor}, continuation: ${isContinuation}${silent ? ', silent' : ''})`);
|
|
1762
1856
|
}
|
|
1763
1857
|
// ─── Dashboard「创建会话」spawn / activate ───────────────────────────────────
|
|
1764
1858
|
/** 解析 dashboard 创建会话的 pinned workingDir:本群 oncall 绑定优先(弹框填了工作目录会
|