charness 3.3.1 → 3.4.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/CHANGELOG.md +612 -0
- package/DEVELOPMENT.md +76 -0
- package/README.md +5 -2
- package/SETUP.md +66 -20
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +2 -0
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +15 -2
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +13 -1
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +35 -13
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +1 -0
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/agentLogin/agentLogin.js +5 -0
- package/dist/agentLogin/agentLogin.js.map +1 -1
- package/dist/apiErrorRetry.d.ts +10 -4
- package/dist/apiErrorRetry.d.ts.map +1 -1
- package/dist/apiErrorRetry.js +14 -7
- package/dist/apiErrorRetry.js.map +1 -1
- package/dist/apiRetryKick.d.ts +6 -0
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +2 -1
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts +0 -3
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +337 -173
- package/dist/bot.js.map +1 -1
- package/dist/changelogEntries.d.ts +25 -0
- package/dist/changelogEntries.d.ts.map +1 -0
- package/dist/changelogEntries.js +66 -0
- package/dist/changelogEntries.js.map +1 -0
- package/dist/cli/bot.d.ts +6 -2
- package/dist/cli/bot.d.ts.map +1 -1
- package/dist/cli/bot.js +34 -2
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/hot.d.ts +7 -0
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +53 -3
- package/dist/cli/hot.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/connectors/telegram/bindPicker.d.ts +64 -0
- package/dist/connectors/telegram/bindPicker.d.ts.map +1 -0
- package/dist/connectors/telegram/bindPicker.js +97 -0
- package/dist/connectors/telegram/bindPicker.js.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts +16 -0
- package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/selfUpdate.js +210 -0
- package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts +1 -0
- package/dist/connectors/telegram/commands/stt.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/stt.js +5 -0
- package/dist/connectors/telegram/commands/stt.js.map +1 -1
- package/dist/connectors/telegram/commands/topicCommand.d.ts +75 -0
- package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/topicCommand.js +391 -0
- package/dist/connectors/telegram/commands/topicCommand.js.map +1 -0
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +109 -3
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +109 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +109 -3
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +109 -3
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +109 -3
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +109 -3
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +109 -3
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +109 -3
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +109 -3
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +109 -3
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +109 -3
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +109 -3
- package/dist/i18n/zh.js.map +1 -1
- package/dist/installManager.d.ts +28 -0
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +85 -11
- package/dist/installManager.js.map +1 -1
- package/dist/instanceSettings.d.ts +12 -1
- package/dist/instanceSettings.d.ts.map +1 -1
- package/dist/instanceSettings.js +40 -4
- package/dist/instanceSettings.js.map +1 -1
- package/dist/selfUpdate/bootRollback.d.ts +22 -0
- package/dist/selfUpdate/bootRollback.d.ts.map +1 -0
- package/dist/selfUpdate/bootRollback.js +79 -0
- package/dist/selfUpdate/bootRollback.js.map +1 -0
- package/dist/selfUpdate/commandRunner.d.ts +21 -0
- package/dist/selfUpdate/commandRunner.d.ts.map +1 -0
- package/dist/selfUpdate/commandRunner.js +32 -0
- package/dist/selfUpdate/commandRunner.js.map +1 -0
- package/dist/selfUpdate/constants.d.ts +26 -0
- package/dist/selfUpdate/constants.d.ts.map +1 -0
- package/dist/selfUpdate/constants.js +31 -0
- package/dist/selfUpdate/constants.js.map +1 -0
- package/dist/selfUpdate/gitSource.d.ts +58 -0
- package/dist/selfUpdate/gitSource.d.ts.map +1 -0
- package/dist/selfUpdate/gitSource.js +183 -0
- package/dist/selfUpdate/gitSource.js.map +1 -0
- package/dist/selfUpdate/hostLock.d.ts +16 -0
- package/dist/selfUpdate/hostLock.d.ts.map +1 -0
- package/dist/selfUpdate/hostLock.js +101 -0
- package/dist/selfUpdate/hostLock.js.map +1 -0
- package/dist/selfUpdate/hotSupervisor.d.ts +47 -0
- package/dist/selfUpdate/hotSupervisor.d.ts.map +1 -0
- package/dist/selfUpdate/hotSupervisor.js +125 -0
- package/dist/selfUpdate/hotSupervisor.js.map +1 -0
- package/dist/selfUpdate/installKind.d.ts +45 -0
- package/dist/selfUpdate/installKind.d.ts.map +1 -0
- package/dist/selfUpdate/installKind.js +99 -0
- package/dist/selfUpdate/installKind.js.map +1 -0
- package/dist/selfUpdate/launchEnvironment.d.ts +3 -0
- package/dist/selfUpdate/launchEnvironment.d.ts.map +1 -0
- package/dist/selfUpdate/launchEnvironment.js +16 -0
- package/dist/selfUpdate/launchEnvironment.js.map +1 -0
- package/dist/selfUpdate/npmSource.d.ts +34 -0
- package/dist/selfUpdate/npmSource.d.ts.map +1 -0
- package/dist/selfUpdate/npmSource.js +107 -0
- package/dist/selfUpdate/npmSource.js.map +1 -0
- package/dist/selfUpdate/releaseNotes.d.ts +33 -0
- package/dist/selfUpdate/releaseNotes.d.ts.map +1 -0
- package/dist/selfUpdate/releaseNotes.js +115 -0
- package/dist/selfUpdate/releaseNotes.js.map +1 -0
- package/dist/selfUpdate/restart.d.ts +33 -0
- package/dist/selfUpdate/restart.d.ts.map +1 -0
- package/dist/selfUpdate/restart.js +94 -0
- package/dist/selfUpdate/restart.js.map +1 -0
- package/dist/selfUpdate/selfCheck.d.ts +20 -0
- package/dist/selfUpdate/selfCheck.d.ts.map +1 -0
- package/dist/selfUpdate/selfCheck.js +126 -0
- package/dist/selfUpdate/selfCheck.js.map +1 -0
- package/dist/selfUpdate/semver.d.ts +24 -0
- package/dist/selfUpdate/semver.d.ts.map +1 -0
- package/dist/selfUpdate/semver.js +165 -0
- package/dist/selfUpdate/semver.js.map +1 -0
- package/dist/selfUpdate/types.d.ts +51 -0
- package/dist/selfUpdate/types.d.ts.map +1 -0
- package/dist/selfUpdate/types.js +3 -0
- package/dist/selfUpdate/types.js.map +1 -0
- package/dist/selfUpdate/updateEngine.d.ts +110 -0
- package/dist/selfUpdate/updateEngine.d.ts.map +1 -0
- package/dist/selfUpdate/updateEngine.js +727 -0
- package/dist/selfUpdate/updateEngine.js.map +1 -0
- package/dist/selfUpdate/updateMessages.d.ts +84 -0
- package/dist/selfUpdate/updateMessages.d.ts.map +1 -0
- package/dist/selfUpdate/updateMessages.js +115 -0
- package/dist/selfUpdate/updateMessages.js.map +1 -0
- package/dist/selfUpdate/updateModes.d.ts +17 -0
- package/dist/selfUpdate/updateModes.d.ts.map +1 -0
- package/dist/selfUpdate/updateModes.js +14 -0
- package/dist/selfUpdate/updateModes.js.map +1 -0
- package/dist/selfUpdate/updatePending.d.ts +76 -0
- package/dist/selfUpdate/updatePending.d.ts.map +1 -0
- package/dist/selfUpdate/updatePending.js +131 -0
- package/dist/selfUpdate/updatePending.js.map +1 -0
- package/dist/selfUpdate/updatePolicy.d.ts +115 -0
- package/dist/selfUpdate/updatePolicy.d.ts.map +1 -0
- package/dist/selfUpdate/updatePolicy.js +83 -0
- package/dist/selfUpdate/updatePolicy.js.map +1 -0
- package/dist/selfUpdate/updateRuntime.d.ts +22 -0
- package/dist/selfUpdate/updateRuntime.d.ts.map +1 -0
- package/dist/selfUpdate/updateRuntime.js +148 -0
- package/dist/selfUpdate/updateRuntime.js.map +1 -0
- package/dist/startupPromptBuffer.d.ts +2 -0
- package/dist/startupPromptBuffer.d.ts.map +1 -1
- package/dist/startupPromptBuffer.js +4 -0
- package/dist/startupPromptBuffer.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/promptDelivery.d.ts +10 -4
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +6 -3
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/updateScreenPlan.d.ts +38 -0
- package/dist/utils/updateScreenPlan.d.ts.map +1 -0
- package/dist/utils/updateScreenPlan.js +143 -0
- package/dist/utils/updateScreenPlan.js.map +1 -0
- package/package.json +3 -1
package/dist/bot.js
CHANGED
|
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.COMMANDS_MENU = exports.telegramInbound = exports.routedCommandTriggerDeps = void 0;
|
|
37
|
-
exports.buildBindKeyboard = buildBindKeyboard;
|
|
38
37
|
exports.getStartReadyMessage = getStartReadyMessage;
|
|
39
38
|
exports.checkIsRoutedCommand = checkIsRoutedCommand;
|
|
40
39
|
exports.handleRoutedCommandMessage = handleRoutedCommandMessage;
|
|
@@ -84,6 +83,8 @@ const timezone_1 = require("./utils/timezone");
|
|
|
84
83
|
const timezonePicker_1 = require("./connectors/telegram/timezonePicker");
|
|
85
84
|
const timezoneRecompute_1 = require("./scheduler/timezoneRecompute");
|
|
86
85
|
const validation_1 = require("./validation");
|
|
86
|
+
const bindPicker_1 = require("./connectors/telegram/bindPicker");
|
|
87
|
+
const topicCommand_1 = require("./connectors/telegram/commands/topicCommand");
|
|
87
88
|
const providerDisconnectPlan_1 = require("./utils/providerDisconnectPlan");
|
|
88
89
|
const folderName_1 = require("./folderName");
|
|
89
90
|
const threadRouting_1 = require("./threadRouting");
|
|
@@ -157,6 +158,13 @@ const claudeLivenessAction_1 = require("./utils/claudeLivenessAction");
|
|
|
157
158
|
const isoTimestamp_1 = require("./utils/isoTimestamp");
|
|
158
159
|
const effortLevels_1 = require("./effortLevels");
|
|
159
160
|
const instanceSettings_1 = require("./instanceSettings");
|
|
161
|
+
const constants_1 = require("./selfUpdate/constants");
|
|
162
|
+
const launchEnvironment_1 = require("./selfUpdate/launchEnvironment");
|
|
163
|
+
const restart_1 = require("./selfUpdate/restart");
|
|
164
|
+
const updateEngine_1 = require("./selfUpdate/updateEngine");
|
|
165
|
+
const updateMessages_1 = require("./selfUpdate/updateMessages");
|
|
166
|
+
const updatePolicy_1 = require("./selfUpdate/updatePolicy");
|
|
167
|
+
const updateRuntime_1 = require("./selfUpdate/updateRuntime");
|
|
160
168
|
const threadContextPreamble_1 = require("./threadContextPreamble");
|
|
161
169
|
const pinnedBannerSkipDecision_1 = require("./utils/pinnedBannerSkipDecision");
|
|
162
170
|
const replyQuote_1 = require("./utils/replyQuote");
|
|
@@ -216,6 +224,7 @@ const limitsBoard_1 = require("./connectors/telegram/commands/limitsBoard");
|
|
|
216
224
|
const backendSwitch_1 = require("./connectors/telegram/commands/backendSwitch");
|
|
217
225
|
const operatorSwitchPlan_1 = require("./utils/operatorSwitchPlan");
|
|
218
226
|
const backendSwitchPlan_1 = require("./utils/backendSwitchPlan");
|
|
227
|
+
const selfUpdate_1 = require("./connectors/telegram/commands/selfUpdate");
|
|
219
228
|
const stt_1 = require("./connectors/telegram/commands/stt");
|
|
220
229
|
const resolveBinary_1 = require("./utils/resolveBinary");
|
|
221
230
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -595,6 +604,18 @@ bot.use(async (ctx, next) => {
|
|
|
595
604
|
});
|
|
596
605
|
return next();
|
|
597
606
|
});
|
|
607
|
+
/**
|
|
608
|
+
* Epoch ms of the last update a human sent (a message, a button tap), `null` since the start. The updater restarts the bot only
|
|
609
|
+
* after a quiet period (`quietPeriodMs`) — somebody typing right now is the one case it must not interrupt.
|
|
610
|
+
*/
|
|
611
|
+
let lastHumanInboundAt = null;
|
|
612
|
+
bot.use(async (ctx, next) => {
|
|
613
|
+
if (ctx.message || ctx.callbackQuery)
|
|
614
|
+
lastHumanInboundAt = Date.now();
|
|
615
|
+
return next();
|
|
616
|
+
});
|
|
617
|
+
/** The update engine of this process; built at boot (`startBot`), read by the `/update` command. */
|
|
618
|
+
let updateEngine = null;
|
|
598
619
|
// Locale is a Telegram-chat concern, not a process env knob. Every update runs
|
|
599
620
|
// inside an async i18n context: explicit `/language` override for the DM/group
|
|
600
621
|
// wins, else this sender's Telegram `language_code`, else the last seen Telegram
|
|
@@ -1135,7 +1156,7 @@ function handleApiError(key, cls, options = {}) {
|
|
|
1135
1156
|
}
|
|
1136
1157
|
const entry = apiRetryTimers.get(k);
|
|
1137
1158
|
const prev = entry
|
|
1138
|
-
? {
|
|
1159
|
+
? { attemptsByKind: entry.attemptsByKind, firedAt: entry.firedAt, pending: entry.timer !== null }
|
|
1139
1160
|
: null;
|
|
1140
1161
|
const now = Date.now();
|
|
1141
1162
|
const action = (0, apiErrorRetry_1.decideRetryAction)({
|
|
@@ -1200,7 +1221,7 @@ function handleApiError(key, cls, options = {}) {
|
|
|
1200
1221
|
attempt: action.attempt,
|
|
1201
1222
|
}));
|
|
1202
1223
|
}
|
|
1203
|
-
armApiRetryTimer(key, { kind: cls.kind, attempt: action.attempt, fireAt: action.fireAt, delayMs: action.delayMs, limitWait });
|
|
1224
|
+
armApiRetryTimer(key, { kind: cls.kind, attempt: action.attempt, fireAt: action.fireAt, delayMs: action.delayMs, attemptsByKind: action.attemptsByKind, limitWait });
|
|
1204
1225
|
}
|
|
1205
1226
|
/**
|
|
1206
1227
|
* @description Arm a thread's retry once its notice is posted: keep a pending question last, persist the record
|
|
@@ -1225,6 +1246,7 @@ function armApiRetryTimer(key, arming) {
|
|
|
1225
1246
|
apiRetryTimers.set((0, sessionKey_1.keyToString)(key), {
|
|
1226
1247
|
timer,
|
|
1227
1248
|
attempt: arming.attempt,
|
|
1249
|
+
attemptsByKind: arming.attemptsByKind ?? { [arming.kind]: arming.attempt },
|
|
1228
1250
|
kind: arming.kind,
|
|
1229
1251
|
firedAt: null,
|
|
1230
1252
|
fireAt: arming.fireAt,
|
|
@@ -1720,7 +1742,7 @@ function clearLimitResumeNotice(key) {
|
|
|
1720
1742
|
*/
|
|
1721
1743
|
let claudeProfileAuthStatusSink = null;
|
|
1722
1744
|
// Out-of-band sign-in drivers (`/login`, `/connect` OAuth): `agentLogin/README.md`.
|
|
1723
|
-
const { cancelPendingLogin, checkIsAuthLoginAwaitingCode, cancelClaudeAuthLogin, startClaudeAuthLogin, submitClaudeAuthLoginCode, checkIsOpenCodeOAuthAwaitingReply, cancelOpenCodeOAuthLogin, startOpenCodeOAuthLogin, submitOpenCodeOAuthReply, } = (0, agentLogin_1.createAgentLogin)({
|
|
1745
|
+
const { cancelPendingLogin, checkIsAuthLoginAwaitingCode, cancelClaudeAuthLogin, startClaudeAuthLogin, submitClaudeAuthLoginCode, checkIsOpenCodeOAuthAwaitingReply, cancelOpenCodeOAuthLogin, startOpenCodeOAuthLogin, submitOpenCodeOAuthReply, checkIsAnyLoginInProgress, } = (0, agentLogin_1.createAgentLogin)({
|
|
1724
1746
|
replyToThread,
|
|
1725
1747
|
deleteThreadMessage,
|
|
1726
1748
|
clearAuthNotice,
|
|
@@ -3465,8 +3487,6 @@ function getWorkDirStartDecision(key) {
|
|
|
3465
3487
|
// `validateSubdir` + `BindError` live in `src/validation.ts` so the
|
|
3466
3488
|
// security-critical bits can be unit-tested without booting Telegraf
|
|
3467
3489
|
// (plan §11 Этап 7, R3). The import at the top of this file pulls them in.
|
|
3468
|
-
/** Inline-keyboard page size for `/bind`. 20 fits one phone screen comfortably. */
|
|
3469
|
-
const BIND_PAGE_SIZE = 20;
|
|
3470
3490
|
/**
|
|
3471
3491
|
* @description List immediate subdirectories of `WORK_ROOT` for the `/bind`
|
|
3472
3492
|
* inline-button picker and the `forum_topic_created` welcome message.
|
|
@@ -3518,71 +3538,6 @@ function listAvailableSubdirs(workRoot, limit = 200) {
|
|
|
3518
3538
|
subdirCache.set(cacheKey, { value: result, ts: Date.now() });
|
|
3519
3539
|
return result;
|
|
3520
3540
|
}
|
|
3521
|
-
/**
|
|
3522
|
-
* @description Build a `subdir`-suggestion keyboard for `/bind` and friends.
|
|
3523
|
-
*
|
|
3524
|
-
* Lists are paginated at {@link BIND_PAGE_SIZE} entries: a `WORK_ROOT` with
|
|
3525
|
-
* many subfolders (mono-repo workspace, packages dir, etc.) would otherwise
|
|
3526
|
-
* push the inline keyboard past Telegram's per-message height limit and
|
|
3527
|
-
* lose entries below the fold.
|
|
3528
|
-
*
|
|
3529
|
-
* The nav row appears only when there's more than one page. The middle
|
|
3530
|
-
* `pageLabel` button is a no-op (`bind_page_noop` callback) — it exists
|
|
3531
|
-
* solely so the user sees "2/5" without us having to fake a divider with
|
|
3532
|
-
* a separate message.
|
|
3533
|
-
*
|
|
3534
|
-
* Subdirs whose callback_data exceeds 64 bytes are filtered out upstream
|
|
3535
|
-
* in `listAvailableSubdirs`. `page` is clamped to a valid index so a stale
|
|
3536
|
-
* callback (folders disappeared since the keyboard was sent) just lands
|
|
3537
|
-
* on the last available page.
|
|
3538
|
-
*
|
|
3539
|
-
* When `isBound` the FIRST row is a full-width «leave current dir» button
|
|
3540
|
-
* (`bindLeaveCurrentCallback`, the folded-in `/unbind`); the «create new folder»
|
|
3541
|
-
* button (`bindCreateFolderCallback`) follows. Both callbacks deliberately do
|
|
3542
|
-
* NOT start with `bind_`, so they can't be mistaken for a folder pick by the
|
|
3543
|
-
* `bind_<subdir>` action regex. They ride on every page so they're reachable
|
|
3544
|
-
* regardless of pagination.
|
|
3545
|
-
*/
|
|
3546
|
-
const bindCreateFolderCallback = 'bindCreateFolder';
|
|
3547
|
-
/**
|
|
3548
|
-
* «Leave current dir» button — the folded-in `/unbind`. Like
|
|
3549
|
-
* `bindCreateFolderCallback`, the callback id deliberately does NOT start with
|
|
3550
|
-
* `bind_`, so the `bind_<subdir>` action regex can't mistake it for a folder
|
|
3551
|
-
* pick. Only rendered in a bound topic.
|
|
3552
|
-
*/
|
|
3553
|
-
const bindLeaveCurrentCallback = 'bindLeaveCurrent';
|
|
3554
|
-
function buildBindKeyboard(subdirs, page = 0, pageSize = BIND_PAGE_SIZE, isBound = false) {
|
|
3555
|
-
const { slice, currentPage, totalPages } = (0, validation_1.paginateBindList)(subdirs, page, pageSize);
|
|
3556
|
-
const rows = [
|
|
3557
|
-
[telegraf_1.Markup.button.callback((0, i18n_1.t)('bind.create_button'), bindCreateFolderCallback)],
|
|
3558
|
-
];
|
|
3559
|
-
// A bound topic gets a full-width «leave current dir» row PREPENDED above the
|
|
3560
|
-
// «create new folder» row — this is the folded-in `/unbind`. Its callback
|
|
3561
|
-
// deliberately avoids the `bind_` prefix so the `bind_<subdir>` action regex
|
|
3562
|
-
// can't catch it. The row is omitted when unbound (nothing to leave).
|
|
3563
|
-
if (isBound) {
|
|
3564
|
-
rows.unshift([telegraf_1.Markup.button.callback((0, i18n_1.t)('bind.leave_button'), bindLeaveCurrentCallback)]);
|
|
3565
|
-
}
|
|
3566
|
-
for (let i = 0; i < slice.length; i += 2) {
|
|
3567
|
-
const row = [telegraf_1.Markup.button.callback(`📁 ${slice[i]}`, `bind_${slice[i]}`)];
|
|
3568
|
-
if (slice[i + 1]) {
|
|
3569
|
-
row.push(telegraf_1.Markup.button.callback(`📁 ${slice[i + 1]}`, `bind_${slice[i + 1]}`));
|
|
3570
|
-
}
|
|
3571
|
-
rows.push(row);
|
|
3572
|
-
}
|
|
3573
|
-
if (totalPages > 1) {
|
|
3574
|
-
const nav = [];
|
|
3575
|
-
if (currentPage > 0) {
|
|
3576
|
-
nav.push(telegraf_1.Markup.button.callback('⬅️ Prev', `bind_page_${currentPage - 1}`));
|
|
3577
|
-
}
|
|
3578
|
-
nav.push(telegraf_1.Markup.button.callback(`${currentPage + 1}/${totalPages}`, 'bind_page_noop'));
|
|
3579
|
-
if (currentPage < totalPages - 1) {
|
|
3580
|
-
nav.push(telegraf_1.Markup.button.callback('Next ➡️', `bind_page_${currentPage + 1}`));
|
|
3581
|
-
}
|
|
3582
|
-
rows.push(nav);
|
|
3583
|
-
}
|
|
3584
|
-
return telegraf_1.Markup.inlineKeyboard(rows);
|
|
3585
|
-
}
|
|
3586
3541
|
/**
|
|
3587
3542
|
* @description Bot-level wrapper around the pure sweep helper in
|
|
3588
3543
|
* `createAdapter.ts`. Binds the production adapter resolver so callers
|
|
@@ -4009,12 +3964,13 @@ async function ensureAgentSessionNow(key, options) {
|
|
|
4009
3964
|
if (failureAction === 'fail') {
|
|
4010
3965
|
return { ok: false, reason: 'resume-failed', message: (0, i18n_1.t)('agent.resume_failed_fresh') };
|
|
4011
3966
|
}
|
|
4012
|
-
// The previous conversation is lost to this topic: say so in one line, then start fresh.
|
|
3967
|
+
// The previous conversation is lost to this topic: say so in one line, then start fresh. A fresh start that
|
|
3968
|
+
// fails too says only why: "a fresh session was started" would be false.
|
|
4013
3969
|
const startMessage = await startAgentSession(key, options.args);
|
|
3970
|
+
if (!(0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key))
|
|
3971
|
+
return { ok: false, reason: 'start-failed', message: startMessage };
|
|
4014
3972
|
const message = [(0, i18n_1.t)('agent.resume_failed_fresh'), startMessage].filter(Boolean).join('\n');
|
|
4015
|
-
return
|
|
4016
|
-
? { ok: true, message, isFresh: true }
|
|
4017
|
-
: { ok: false, reason: 'start-failed', message };
|
|
3973
|
+
return { ok: true, message, isFresh: true };
|
|
4018
3974
|
}
|
|
4019
3975
|
case 'start': {
|
|
4020
3976
|
await switchThreadAdapter(key, plan.adapterName);
|
|
@@ -4111,6 +4067,7 @@ async function suspendThreadSession(key) {
|
|
|
4111
4067
|
if (!adapter.checkIsActive(key) || adapter.checkIsWorking?.(key))
|
|
4112
4068
|
return;
|
|
4113
4069
|
startupPromptBuffer.markStarting(kStr);
|
|
4070
|
+
console.log(`[compact-on-idle] ${kStr} stopping the idle process; a prompt that comes meanwhile is held`);
|
|
4114
4071
|
try {
|
|
4115
4072
|
await adapter.suspendSession(key);
|
|
4116
4073
|
console.log(`[compact-on-idle] ${kStr} process stopped; the session sleeps and resumes on the next message`);
|
|
@@ -4150,12 +4107,16 @@ function handleAgentTurnEnded(key) {
|
|
|
4150
4107
|
* out through `sendNotice` (the topic by default).
|
|
4151
4108
|
*/
|
|
4152
4109
|
async function resumeSleepingSessionForPrompt(key, sendNotice = (text) => replyToThread(key, text)) {
|
|
4110
|
+
return (await wakeSleepingSessionForPrompt(key, sendNotice)).isAwake;
|
|
4111
|
+
}
|
|
4112
|
+
/** @description {@link resumeSleepingSessionForPrompt}, also saying whether it posted a notice (a start that failed says why). */
|
|
4113
|
+
async function wakeSleepingSessionForPrompt(key, sendNotice = (text) => replyToThread(key, text)) {
|
|
4153
4114
|
if (checkIsGeneral(key) || !state.getBinding(key) || getResumableSessionId(key) === null)
|
|
4154
|
-
return false;
|
|
4115
|
+
return { isAwake: false, isNoticePosted: false };
|
|
4155
4116
|
const ensured = await ensureAgentSession(key);
|
|
4156
4117
|
if (ensured.message)
|
|
4157
4118
|
await sendNotice(ensured.message);
|
|
4158
|
-
return ensured.ok && (0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key);
|
|
4119
|
+
return { isAwake: ensured.ok && (0, createAdapter_1.getThreadAdapter)(key).checkIsActive(key), isNoticePosted: Boolean(ensured.message) };
|
|
4159
4120
|
}
|
|
4160
4121
|
/**
|
|
4161
4122
|
* @description Replay prompts buffered during the startup window to the now-active
|
|
@@ -4488,6 +4449,13 @@ const { checkIsThreadCompacting, noteThreadActivity, noteThreadUserActivity, mar
|
|
|
4488
4449
|
function checkIsRoutedCommand(name) {
|
|
4489
4450
|
return commandRouter.checkIsRegistered(name);
|
|
4490
4451
|
}
|
|
4452
|
+
/** `/esc` and its other names: the interrupt, which also cancels a pending sign-in or `/topic` prompt first. */
|
|
4453
|
+
const escapeCommandNames = ['esc', 'escape', 'interrupt'];
|
|
4454
|
+
/** @description Whether `messageText` is one of the {@link escapeCommandNames} (`@botmention` tolerated). */
|
|
4455
|
+
function checkIsEscapeCommandText(messageText) {
|
|
4456
|
+
const [commandToken = ''] = (0, utils_1.stripCommandBotMention)(messageText.trim()).split(/\s+/, 1);
|
|
4457
|
+
return escapeCommandNames.includes(commandToken.replace(/^\//, '').toLowerCase());
|
|
4458
|
+
}
|
|
4491
4459
|
/**
|
|
4492
4460
|
* @description Running ANY command exits the single-purpose input modes an
|
|
4493
4461
|
* earlier command armed:
|
|
@@ -4496,6 +4464,7 @@ function checkIsRoutedCommand(name) {
|
|
|
4496
4464
|
* it afterwards via its own callback);
|
|
4497
4465
|
* - `/connect`'s pending provider connect — cancelled out loud, unless the
|
|
4498
4466
|
* command IS another `/connect` (which continues the flow);
|
|
4467
|
+
* - `/topic`'s pending name prompts (the topic's, the new folder's) — but `/esc`, which cancels them with a notice of its own;
|
|
4499
4468
|
* - an in-flight `/reminders` wizard, at whatever step. Unconditional rather
|
|
4500
4469
|
* than only during the step-4 text wait: that is what makes a repeat
|
|
4501
4470
|
* `/reminders` retire the previous wizard (relabelling its message) instead
|
|
@@ -4505,6 +4474,9 @@ function checkIsRoutedCommand(name) {
|
|
|
4505
4474
|
async function exitPendingInputModes(key, messageText) {
|
|
4506
4475
|
const keyString = (0, sessionKey_1.keyToString)(key);
|
|
4507
4476
|
awaitingFolderName.delete(keyString);
|
|
4477
|
+
// `/topic`'s name prompts end silently, except for `/esc`, which cancels them out loud in its own handler.
|
|
4478
|
+
if (!checkIsEscapeCommandText(messageText))
|
|
4479
|
+
cancelTopicInput(key);
|
|
4508
4480
|
const hadPendingProviderConnect = pendingProviderConnects.delete(keyString);
|
|
4509
4481
|
if (hadPendingProviderConnect && !(0, recvPreviewRedaction_1.checkIsConnectCommandText)(messageText)) {
|
|
4510
4482
|
await replyToThread(key, (0, i18n_1.t)('connect.cancelled'));
|
|
@@ -4885,6 +4857,8 @@ function armFolderCreation(key) {
|
|
|
4885
4857
|
const kStr = (0, sessionKey_1.keyToString)(key);
|
|
4886
4858
|
awaitingModelSelection.delete(kStr);
|
|
4887
4859
|
awaitingSessionSelection.delete(kStr);
|
|
4860
|
+
// The `/topic` prompts are other readers of the next plain message.
|
|
4861
|
+
cancelTopicInput(key);
|
|
4888
4862
|
// A stale pending question would otherwise consume the message AFTER the
|
|
4889
4863
|
// folder name as an answer to the old (pre-rebind) agent question.
|
|
4890
4864
|
clearPendingQuestion(key);
|
|
@@ -4902,21 +4876,18 @@ function mapNewFolderError(reason) {
|
|
|
4902
4876
|
}
|
|
4903
4877
|
}
|
|
4904
4878
|
/**
|
|
4905
|
-
* @description Create a new folder under WORK_ROOT from a user-typed name
|
|
4906
|
-
*
|
|
4907
|
-
*
|
|
4908
|
-
*
|
|
4909
|
-
*
|
|
4910
|
-
*
|
|
4911
|
-
* Returns `{ ok }` so the caller knows whether to disarm the await-name mode:
|
|
4912
|
-
* an invalid name keeps the thread armed for a retry; success or a real
|
|
4913
|
-
* filesystem failure disarms it.
|
|
4879
|
+
* @description Create a new folder under WORK_ROOT from a user-typed name:
|
|
4880
|
+
* validate the name first (no traversal/slashes/dots), then `mkdir` (recursive:
|
|
4881
|
+
* an already-existing folder is fine — the caller just uses it, and the topic is
|
|
4882
|
+
* told so). A refusal or a filesystem failure is reported to `key`'s topic here;
|
|
4883
|
+
* the outcome says which, so the caller knows whether a retry is still wanted
|
|
4884
|
+
* (`invalid`) or not (`failed`).
|
|
4914
4885
|
*/
|
|
4915
|
-
async function
|
|
4886
|
+
async function createWorkFolder(key, rawName) {
|
|
4916
4887
|
const validated = (0, folderName_1.validateNewFolderName)(rawName);
|
|
4917
4888
|
if (!validated.ok) {
|
|
4918
4889
|
await replyToThread(key, mapNewFolderError(validated.reason));
|
|
4919
|
-
return {
|
|
4890
|
+
return { outcome: 'invalid' };
|
|
4920
4891
|
}
|
|
4921
4892
|
const targetPath = path.join(ENV.workRoot, validated.name);
|
|
4922
4893
|
const alreadyExisted = fs.existsSync(targetPath);
|
|
@@ -4927,7 +4898,7 @@ async function createAndBindFolder(key, rawName) {
|
|
|
4927
4898
|
await replyToThread(key, (0, i18n_1.t)('bind.create_failed', {
|
|
4928
4899
|
error: e instanceof Error ? e.message : String(e),
|
|
4929
4900
|
}));
|
|
4930
|
-
return {
|
|
4901
|
+
return { outcome: 'failed' };
|
|
4931
4902
|
}
|
|
4932
4903
|
// Folder freshly on disk → the cached subdir list is stale; drop it so the
|
|
4933
4904
|
// next picker render includes the new folder.
|
|
@@ -4935,12 +4906,43 @@ async function createAndBindFolder(key, rawName) {
|
|
|
4935
4906
|
if (alreadyExisted) {
|
|
4936
4907
|
await replyToThread(key, (0, i18n_1.t)('bind.create_exists', { subdir: validated.name }));
|
|
4937
4908
|
}
|
|
4938
|
-
|
|
4909
|
+
return { outcome: 'ready', subdir: validated.name };
|
|
4910
|
+
}
|
|
4911
|
+
/**
|
|
4912
|
+
* @description Create a new folder under WORK_ROOT from a user-typed name and
|
|
4913
|
+
* bind the thread to it ({@link createWorkFolder}), routing through
|
|
4914
|
+
* `applyBinding` for the same containment/symlink defence + welcome stack the
|
|
4915
|
+
* picker uses.
|
|
4916
|
+
*
|
|
4917
|
+
* Returns `{ ok }` so the caller knows whether to disarm the await-name mode:
|
|
4918
|
+
* an invalid name keeps the thread armed for a retry; success or a real
|
|
4919
|
+
* filesystem failure disarms it.
|
|
4920
|
+
*/
|
|
4921
|
+
async function createAndBindFolder(key, rawName) {
|
|
4922
|
+
const folder = await createWorkFolder(key, rawName);
|
|
4923
|
+
if (folder.outcome === 'invalid')
|
|
4924
|
+
return { ok: false };
|
|
4925
|
+
if (folder.outcome === 'failed')
|
|
4926
|
+
return { ok: true };
|
|
4927
|
+
const result = await applyBinding(key, folder.subdir);
|
|
4939
4928
|
await replyToThread(key, result.message);
|
|
4940
4929
|
if (result.ok)
|
|
4941
4930
|
await sendBindingWelcome(key, result.subdir);
|
|
4942
4931
|
return { ok: true };
|
|
4943
4932
|
}
|
|
4933
|
+
/**
|
|
4934
|
+
* @description Bind a topic the bot has just created (`/topic`) to `subdir` and greet it with the usual binding welcome:
|
|
4935
|
+
* the folder's stats and the start buttons. No agent starts by itself: it is started there as in any bound topic. The binding's own notice
|
|
4936
|
+
* is left out: its warning about other topics on the folder would warn of what the person just asked for. The topic's name
|
|
4937
|
+
* rides on the binding for the thread-context preamble. Resolves why it could not bind, `null` once it is bound.
|
|
4938
|
+
*/
|
|
4939
|
+
async function bindNewTopic(key, subdir, topicName) {
|
|
4940
|
+
const result = await applyBinding(key, subdir, { topicName });
|
|
4941
|
+
if (!result.ok)
|
|
4942
|
+
return result.message;
|
|
4943
|
+
await sendBindingWelcome(key, result.subdir);
|
|
4944
|
+
return null;
|
|
4945
|
+
}
|
|
4944
4946
|
command('bind', async (_ctx, key, parsed) => {
|
|
4945
4947
|
if (checkIsGeneral(key)) {
|
|
4946
4948
|
await replyToThread(key, (0, i18n_1.t)('bind.in_general'));
|
|
@@ -4964,7 +4966,7 @@ command('bind', async (_ctx, key, parsed) => {
|
|
|
4964
4966
|
// «create new folder» button, which is the only way to bootstrap a folder
|
|
4965
4967
|
// from an empty WORK_ROOT without the slash form. When already bound it
|
|
4966
4968
|
// also carries the «leave current dir» button (the folded-in /unbind).
|
|
4967
|
-
await replyToThread(key, usage, buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!binding));
|
|
4969
|
+
await replyToThread(key, usage, (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!binding));
|
|
4968
4970
|
return;
|
|
4969
4971
|
}
|
|
4970
4972
|
const result = await applyBinding(key, arg);
|
|
@@ -5151,26 +5153,27 @@ async function getCliVersion(cmd, args = ['--version']) {
|
|
|
5151
5153
|
}
|
|
5152
5154
|
}
|
|
5153
5155
|
command('version', async (_ctx, key) => {
|
|
5154
|
-
//
|
|
5155
|
-
//
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
}
|
|
5161
|
-
catch { /* fall through to unknown */ }
|
|
5156
|
+
// The version this process STARTED as, read once at boot by the updater's runtime: an in-place install rewrites package.json
|
|
5157
|
+
// under a running bot, and the new number must not show before the restart that makes it true.
|
|
5158
|
+
const updateStatus = updateEngine?.getStatus();
|
|
5159
|
+
const botVersion = updateStatus?.running.version ?? (0, i18n_1.t)('version.unknown');
|
|
5160
|
+
const offer = updateStatus?.offer;
|
|
5161
|
+
const availableUpdate = offer && offer.action !== 'skipped' && offer.action !== 'needsNode' ? offer.target : null;
|
|
5162
5162
|
const [tmuxV, claudeV, opencodeV] = await Promise.all([
|
|
5163
5163
|
getCliVersion('tmux', ['-V']),
|
|
5164
5164
|
getCliVersion('claude'),
|
|
5165
5165
|
getCliVersion('opencode'),
|
|
5166
5166
|
]);
|
|
5167
|
-
|
|
5167
|
+
const report = (0, i18n_1.t)('version.report', {
|
|
5168
5168
|
bot: botVersion,
|
|
5169
5169
|
node: process.version,
|
|
5170
5170
|
tmux: tmuxV,
|
|
5171
5171
|
claude: claudeV,
|
|
5172
5172
|
opencode: opencodeV,
|
|
5173
|
-
})
|
|
5173
|
+
});
|
|
5174
|
+
await replyToThread(key, availableUpdate === null ? report : `${report}\n${(0, i18n_1.t)('version.updateAvailable', { version: availableUpdate })}`, {
|
|
5175
|
+
parse_mode: 'Markdown',
|
|
5176
|
+
});
|
|
5174
5177
|
});
|
|
5175
5178
|
command('whoami', async (ctx, key) => {
|
|
5176
5179
|
const userId = ctx.from?.id ?? 0;
|
|
@@ -5239,7 +5242,7 @@ command('help', async (_ctx, key) => {
|
|
|
5239
5242
|
const binding = state.getBinding(key);
|
|
5240
5243
|
if (!binding) {
|
|
5241
5244
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
5242
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
5245
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
5243
5246
|
await replyToThread(key, (0, i18n_1.t)('help.thread_unbound'), {
|
|
5244
5247
|
parse_mode: 'Markdown',
|
|
5245
5248
|
...(extra ?? {}),
|
|
@@ -5584,7 +5587,7 @@ async function handleAgentStart(key, adapterName, args) {
|
|
|
5584
5587
|
// natural-language path in the text handler (plan §11 Этап 4).
|
|
5585
5588
|
if (!state.getBinding(key)) {
|
|
5586
5589
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
5587
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
5590
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
5588
5591
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
5589
5592
|
return;
|
|
5590
5593
|
}
|
|
@@ -6433,7 +6436,7 @@ const { registerClaudeAccountCommands, registerClaudeAccountCallbacks, checkIsAw
|
|
|
6433
6436
|
applyAutoSwitch,
|
|
6434
6437
|
});
|
|
6435
6438
|
claudeProfileAuthStatusSink = recordClaudeProfileAuthStatus;
|
|
6436
|
-
const { registerSttCommands, registerSttCallbacks, startSttAutoInstall, resumeSttInstallAtBoot, getAwaitedSttStep, submitSttInput, cancelSttInput, } = (0, stt_1.createStt)({
|
|
6439
|
+
const { registerSttCommands, registerSttCallbacks, startSttAutoInstall, resumeSttInstallAtBoot, getAwaitedSttStep, checkIsAnySttInputAwaited, submitSttInput, cancelSttInput, } = (0, stt_1.createStt)({
|
|
6437
6440
|
...botCore,
|
|
6438
6441
|
getConfigPath: () => (0, sttSettings_1.getSttConfigPath)(),
|
|
6439
6442
|
getDataDir: state_1.resolveDataDir,
|
|
@@ -6446,10 +6449,30 @@ const { registerSttCommands, registerSttCallbacks, startSttAutoInstall, resumeSt
|
|
|
6446
6449
|
return groupId !== null ? (0, sessionKeyCodec_1.makeTelegramKey)(groupId, threadRouting_1.GENERAL_THREAD_ID) : null;
|
|
6447
6450
|
},
|
|
6448
6451
|
});
|
|
6452
|
+
const { registerTopicCommand, registerTopicCallbacks, checkIsAwaitingTopicInput, submitTopicInput, cancelTopicInput, cancelTopicWizard, } = (0, topicCommand_1.createTopicCommand)({
|
|
6453
|
+
...botCore,
|
|
6454
|
+
checkIsDmKey,
|
|
6455
|
+
disarmFolderCreation: (key) => {
|
|
6456
|
+
awaitingFolderName.delete((0, sessionKey_1.keyToString)(key));
|
|
6457
|
+
},
|
|
6458
|
+
listAvailableSubdirs: () => listAvailableSubdirs(ENV.workRoot),
|
|
6459
|
+
createWorkFolder,
|
|
6460
|
+
bindNewTopic,
|
|
6461
|
+
});
|
|
6449
6462
|
registerClaudeAccountCommands();
|
|
6463
|
+
registerTopicCommand();
|
|
6450
6464
|
registerFallbackChainCommands();
|
|
6451
6465
|
registerLimitsBoardCommands();
|
|
6452
6466
|
registerSttCommands();
|
|
6467
|
+
const { registerSelfUpdateCommands, registerSelfUpdateCallbacks } = (0, selfUpdate_1.createSelfUpdate)({
|
|
6468
|
+
...botCore,
|
|
6469
|
+
getUpdateEngine: () => updateEngine,
|
|
6470
|
+
getSettingsPath: () => (0, instanceSettings_1.getInstanceSettingsPath)(),
|
|
6471
|
+
now: () => Date.now(),
|
|
6472
|
+
// The instance's time zone is `process.env.TZ` (`/timezone`), which `toLocaleTimeString` follows.
|
|
6473
|
+
formatClock: (epochMs) => new Date(epochMs).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }),
|
|
6474
|
+
});
|
|
6475
|
+
registerSelfUpdateCommands();
|
|
6453
6476
|
registerCompactionCommands();
|
|
6454
6477
|
/**
|
|
6455
6478
|
* @description Build the agent-picker keyboard.
|
|
@@ -6483,7 +6506,7 @@ async function handleSessionsList(key) {
|
|
|
6483
6506
|
}
|
|
6484
6507
|
if (!state.getBinding(key)) {
|
|
6485
6508
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
6486
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
6509
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
6487
6510
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
6488
6511
|
return;
|
|
6489
6512
|
}
|
|
@@ -6743,8 +6766,8 @@ command('tab', async (_ctx, key) => {
|
|
|
6743
6766
|
else
|
|
6744
6767
|
await replyToThread(key, `Not supported for ${adapter.label}`);
|
|
6745
6768
|
});
|
|
6746
|
-
command(
|
|
6747
|
-
// A pending «Add account» name prompt,
|
|
6769
|
+
command(escapeCommandNames, async (_ctx, key) => {
|
|
6770
|
+
// A pending «Add account» name prompt, a pending sign-in (`/login`, `/connect` OAuth) and a `/topic` wizard take
|
|
6748
6771
|
// priority: the first /esc only cancels it, so the topic stops waiting for typed input.
|
|
6749
6772
|
if (cancelProfileNameCapture(key)) {
|
|
6750
6773
|
await replyToThread(key, (0, i18n_1.t)('account.askNameCancelled'));
|
|
@@ -6754,12 +6777,21 @@ command(['esc', 'escape'], async (_ctx, key) => {
|
|
|
6754
6777
|
await replyToThread(key, (0, i18n_1.t)('login.cancelled'));
|
|
6755
6778
|
return;
|
|
6756
6779
|
}
|
|
6780
|
+
// A `/topic` wizard is ended at whatever step it waits (it says so itself).
|
|
6781
|
+
if (await cancelTopicWizard(key))
|
|
6782
|
+
return;
|
|
6757
6783
|
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
6758
6784
|
if (adapter.sendEscape) {
|
|
6759
6785
|
// The operator interrupted the turn: its open request closes silently.
|
|
6760
6786
|
cancelConversationRequest(key);
|
|
6761
6787
|
markNeedsNewMessage(key);
|
|
6788
|
+
// A turn blocked on a question ends with it: the question is closed on the agent first (an open OpenCode one is
|
|
6789
|
+
// re-found at every reattach; a Claude stream-json one is denied), then the turn is stopped — neither waits for Telegram. Only
|
|
6790
|
+
// then the question's buttons are retired, as when a prompt walks away from it (that edit goes through the send pacer).
|
|
6791
|
+
adapter.rejectQuestion?.(key);
|
|
6762
6792
|
adapter.sendEscape(key);
|
|
6793
|
+
if (pendingQuestions.has((0, sessionKey_1.keyToString)(key)))
|
|
6794
|
+
await retirePendingQuestion(key);
|
|
6763
6795
|
}
|
|
6764
6796
|
else {
|
|
6765
6797
|
await replyToThread(key, `Not supported for ${adapter.label}`);
|
|
@@ -7030,10 +7062,10 @@ command('clear_messages', async (ctx, key) => {
|
|
|
7030
7062
|
// agent.
|
|
7031
7063
|
const botCommands = new Set([
|
|
7032
7064
|
'start', 'claude', 'opencode', 'oc', 'terminal', 'agent', 'sessions', 'resume', 'cancel', 'model', 'connect',
|
|
7033
|
-
'disconnect', 'stop', 'stopall', 'stop-all', 'status', 'c', 'y', 'n', 'enter', 'up', 'down', 'tab', 'esc', 'escape', 'output', 'clear_messages',
|
|
7034
|
-
'bind', 'unbind', 'where', 'ls', 'list', 'new', 'clear_session', 'whoami', 'version', 'help', 'language', 'lang',
|
|
7065
|
+
'disconnect', 'stop', 'stopall', 'stop-all', 'status', 'c', 'y', 'n', 'enter', 'up', 'down', 'tab', 'esc', 'escape', 'interrupt', 'output', 'clear_messages',
|
|
7066
|
+
'bind', 'topic', 'unbind', 'where', 'ls', 'list', 'new', 'clear_session', 'whoami', 'version', 'help', 'language', 'lang',
|
|
7035
7067
|
'doctor', 'mcp', 'rename_session', 'compact', 'compact_on_idle', 'compact_summary', 'auto_continue_limits', 'limits', 'limits_autoswitch', 'trace', 'timestamps', 'timezone', 'schedule', 'reminders', 'thinking', 'tool_results',
|
|
7036
|
-
'subagent', 'claude_mode', 'effort', 'verbosity', 'quit', 'q', 'quit-all', 'quitall', 'pair', 'account', 'accounts', 'fallback', 'stt', 'voice', 'speech',
|
|
7068
|
+
'subagent', 'claude_mode', 'effort', 'verbosity', 'quit', 'q', 'quit-all', 'quitall', 'pair', 'account', 'accounts', 'fallback', 'stt', 'voice', 'speech', 'update',
|
|
7037
7069
|
]);
|
|
7038
7070
|
/**
|
|
7039
7071
|
* @description The bare `/clear` is no longer bot-owned (it was renamed to
|
|
@@ -7066,6 +7098,7 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7066
7098
|
const cmd = text.slice(1).split(' ')[0].split('@')[0].toLowerCase();
|
|
7067
7099
|
// A slash command ends a pending «Add account» name prompt (the command itself runs normally).
|
|
7068
7100
|
cancelProfileNameCapture(key);
|
|
7101
|
+
cancelTopicInput(key);
|
|
7069
7102
|
// ... and an armed `/stt` address or key step.
|
|
7070
7103
|
await cancelSttInput(key);
|
|
7071
7104
|
if (botCommands.has(cmd))
|
|
@@ -7117,6 +7150,12 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7117
7150
|
await submitProfileName(key, text);
|
|
7118
7151
|
return;
|
|
7119
7152
|
}
|
|
7153
|
+
// `/topic` asked for the new topic's name, or for the name of a folder to create → this message is the answer (a bad
|
|
7154
|
+
// name keeps the prompt armed and says so; a slash command above already ended it).
|
|
7155
|
+
if (checkIsAwaitingTopicInput(key) && !text.startsWith('/')) {
|
|
7156
|
+
await submitTopicInput(key, text);
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7120
7159
|
// `/stt` asked for a server address or an API key → this message is the answer. A plausible key is deleted at
|
|
7121
7160
|
// once and never echoed; anything else re-arms the step. A slash command above already disarmed it.
|
|
7122
7161
|
if (getAwaitedSttStep(key) !== null && !text.startsWith('/')) {
|
|
@@ -7282,7 +7321,7 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7282
7321
|
});
|
|
7283
7322
|
if (!result.ok && result.reason === 'unbound') {
|
|
7284
7323
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
7285
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7324
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7286
7325
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
7287
7326
|
return;
|
|
7288
7327
|
}
|
|
@@ -7363,7 +7402,7 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7363
7402
|
const binding = state.getBinding(key);
|
|
7364
7403
|
if (!binding) {
|
|
7365
7404
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
7366
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7405
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7367
7406
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
7368
7407
|
return;
|
|
7369
7408
|
}
|
|
@@ -7494,7 +7533,7 @@ async function processVoiceJob(key, fileId, durationSeconds, requesterId, sentAt
|
|
|
7494
7533
|
});
|
|
7495
7534
|
if (!result.ok && result.reason === 'unbound') {
|
|
7496
7535
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
7497
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7536
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7498
7537
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
7499
7538
|
return;
|
|
7500
7539
|
}
|
|
@@ -7514,7 +7553,7 @@ async function processVoiceJob(key, fileId, durationSeconds, requesterId, sentAt
|
|
|
7514
7553
|
const binding = state.getBinding(key);
|
|
7515
7554
|
if (!binding) {
|
|
7516
7555
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
7517
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7556
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7518
7557
|
await replyToThread(key, (0, i18n_1.t)('thread.no_binding'), extra);
|
|
7519
7558
|
return;
|
|
7520
7559
|
}
|
|
@@ -7582,7 +7621,7 @@ async function checkFileIntakeGatePassed(key, isStarting, sendHint) {
|
|
|
7582
7621
|
const binding = state.getBinding(key);
|
|
7583
7622
|
if (!binding) {
|
|
7584
7623
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
7585
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7624
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7586
7625
|
await sendHint((0, i18n_1.t)('thread.no_binding'), extra);
|
|
7587
7626
|
return false;
|
|
7588
7627
|
}
|
|
@@ -7917,7 +7956,7 @@ bot.on((0, filters_1.message)('forum_topic_created'), async (ctx) => {
|
|
|
7917
7956
|
// Fall through to picker.
|
|
7918
7957
|
}
|
|
7919
7958
|
}
|
|
7920
|
-
const extra = buildBindKeyboard(subdirs, 0, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7959
|
+
const extra = (0, bindPicker_1.buildBindKeyboard)(subdirs, 0, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
7921
7960
|
await replyToThread(key, (0, i18n_1.t)('thread.welcome_pick'), extra);
|
|
7922
7961
|
});
|
|
7923
7962
|
/**
|
|
@@ -8031,7 +8070,7 @@ bot.action(/^bind_page_(\d+)$/, async (ctx) => {
|
|
|
8031
8070
|
}
|
|
8032
8071
|
const page = parseInt(ctx.match[1], 10);
|
|
8033
8072
|
const subdirs = listAvailableSubdirs(ENV.workRoot);
|
|
8034
|
-
const keyboard = buildBindKeyboard(subdirs, page, BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
8073
|
+
const keyboard = (0, bindPicker_1.buildBindKeyboard)(subdirs, page, bindPicker_1.BIND_PAGE_SIZE, !!state.getBinding(key));
|
|
8035
8074
|
try {
|
|
8036
8075
|
await ctx.editMessageReplyMarkup(keyboard.reply_markup);
|
|
8037
8076
|
}
|
|
@@ -8044,14 +8083,14 @@ bot.action(/^bind_page_(\d+)$/, async (ctx) => {
|
|
|
8044
8083
|
await ctx.answerCbQuery();
|
|
8045
8084
|
});
|
|
8046
8085
|
// Middle "N/M" pill in the nav row — pure UI, no state change.
|
|
8047
|
-
bot.action(
|
|
8086
|
+
bot.action(bindPicker_1.bindPageNoopCallback, async (ctx) => {
|
|
8048
8087
|
await ctx.answerCbQuery();
|
|
8049
8088
|
});
|
|
8050
8089
|
// «Create new folder» — first option in the /bind picker. Arms the thread's
|
|
8051
8090
|
// await-folder-name mode; the next text message is the name (see the text
|
|
8052
8091
|
// handler). Callback id deliberately avoids the `bind_` prefix so it can't be
|
|
8053
8092
|
// matched by the `bind_<subdir>` regex below.
|
|
8054
|
-
bot.action(bindCreateFolderCallback, async (ctx) => {
|
|
8093
|
+
bot.action(bindPicker_1.bindCreateFolderCallback, async (ctx) => {
|
|
8055
8094
|
const key = await authoriseContext(ctx);
|
|
8056
8095
|
if (!key) {
|
|
8057
8096
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.access_denied'));
|
|
@@ -8068,7 +8107,7 @@ bot.action(bindCreateFolderCallback, async (ctx) => {
|
|
|
8068
8107
|
// «Leave current dir» — the folded-in `/unbind`, shown only in a bound topic.
|
|
8069
8108
|
// Callback id deliberately avoids the `bind_` prefix so it can't be matched by
|
|
8070
8109
|
// the `bind_<subdir>` regex below.
|
|
8071
|
-
bot.action(bindLeaveCurrentCallback, async (ctx) => {
|
|
8110
|
+
bot.action(bindPicker_1.bindLeaveCurrentCallback, async (ctx) => {
|
|
8072
8111
|
const key = await authoriseContext(ctx);
|
|
8073
8112
|
if (!key) {
|
|
8074
8113
|
await ctx.answerCbQuery((0, i18n_1.t)('cb.access_denied'));
|
|
@@ -8081,6 +8120,8 @@ bot.action(bindLeaveCurrentCallback, async (ctx) => {
|
|
|
8081
8120
|
await ctx.answerCbQuery();
|
|
8082
8121
|
await unbindThread(key);
|
|
8083
8122
|
});
|
|
8123
|
+
// `/topic`'s buttons: patterns of their own (`topic_…`), disjoint from the `bind_<subdir>` regex below.
|
|
8124
|
+
registerTopicCallbacks();
|
|
8084
8125
|
bot.action(/^bind_(.+)$/, async (ctx) => {
|
|
8085
8126
|
const key = await authoriseContext(ctx);
|
|
8086
8127
|
if (!key) {
|
|
@@ -8239,6 +8280,7 @@ registerClaudeAccountCallbacks();
|
|
|
8239
8280
|
registerFallbackChainCallbacks();
|
|
8240
8281
|
registerLimitsBoardCallbacks();
|
|
8241
8282
|
registerSttCallbacks();
|
|
8283
|
+
registerSelfUpdateCallbacks();
|
|
8242
8284
|
// «⏭ Skip once» on the `/limits` board and the `/limits_autoswitch` screen: drop THIS armed resume and
|
|
8243
8285
|
// leave the on/off setting alone. The episode is identified by the `fireAt` baked
|
|
8244
8286
|
// into the callback data, so an untouched older picker can never cancel a LATER
|
|
@@ -8302,7 +8344,10 @@ bot.action(/^reask_(\d+)$/, async (ctx) => {
|
|
|
8302
8344
|
await withThreadLocale(key, () => deliverPromptOrBuffer(key, label, true));
|
|
8303
8345
|
return;
|
|
8304
8346
|
}
|
|
8305
|
-
await withThreadLocale(key, () => deliverActivePrompt(key, adapter, label, null));
|
|
8347
|
+
const outcome = await withThreadLocale(key, () => deliverActivePrompt(key, adapter, label, null));
|
|
8348
|
+
// The label reached no session (the topic was told why): the question is still open, so are its buttons.
|
|
8349
|
+
if (outcome === 'noSession' && labels && !reAskedQuestionOptions.has((0, sessionKey_1.keyToString)(key)))
|
|
8350
|
+
reAskedQuestionOptions.set((0, sessionKey_1.keyToString)(key), labels);
|
|
8306
8351
|
});
|
|
8307
8352
|
registerReminderCallbacks();
|
|
8308
8353
|
bot.action(/^ccmode_(.+)$/, async (ctx) => {
|
|
@@ -8503,13 +8548,13 @@ async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyCo
|
|
|
8503
8548
|
// the quote — the reply context is deliberately dropped here. Not a request
|
|
8504
8549
|
// either: the turn it unblocks continues under the open one.
|
|
8505
8550
|
await applyQuestionAnswer(key, route.labels);
|
|
8506
|
-
return;
|
|
8551
|
+
return null;
|
|
8507
8552
|
}
|
|
8508
8553
|
// Free-form text cancels the question and is a fresh prompt — a request (S7).
|
|
8509
8554
|
const opening = await openTopicRequest(key, text, intake);
|
|
8510
8555
|
await cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, replyContext, opening.header);
|
|
8511
8556
|
trackTopicRequestTurn(key, opening);
|
|
8512
|
-
return;
|
|
8557
|
+
return null;
|
|
8513
8558
|
}
|
|
8514
8559
|
// A bare `/clear` forwarded to the agent wipes its context (Claude TUI), so
|
|
8515
8560
|
// the next prompt must re-carry the thread-context preamble. Reset the marker
|
|
@@ -8543,45 +8588,53 @@ async function deliverActivePrompt(key, adapter, text, intake, sentAtMs, replyCo
|
|
|
8543
8588
|
buffer: async () => {
|
|
8544
8589
|
await deliverPromptOrBuffer(key, `${opening.header}${getPromptWithThreadReplyQuote(key, text, replyContext)}`, true);
|
|
8545
8590
|
},
|
|
8546
|
-
wake: () =>
|
|
8591
|
+
wake: () => wakeSleepingSessionForPrompt(key),
|
|
8547
8592
|
forward: () => forwardPromptToAgent(key, (0, createAdapter_1.getThreadAdapter)(key), text, sentAtMs, { replyContext, requestHeader: opening.header }),
|
|
8548
|
-
reportNoSession: async () => {
|
|
8593
|
+
reportNoSession: async (isAlreadyTold) => {
|
|
8549
8594
|
// The prompt reaches no agent: the request it opened would only wait for a turn that never comes.
|
|
8550
8595
|
if (opening.requestId !== null)
|
|
8551
8596
|
await topicRequestLedger?.closeRequest(opening.requestId, 'cancelled');
|
|
8552
|
-
|
|
8597
|
+
if (!isAlreadyTold)
|
|
8598
|
+
await replyToThread(key, (0, i18n_1.t)('agent.no_session'));
|
|
8553
8599
|
},
|
|
8554
8600
|
});
|
|
8555
8601
|
if (outcome !== 'noSession')
|
|
8556
8602
|
trackTopicRequestTurn(key, opening);
|
|
8603
|
+
return outcome;
|
|
8557
8604
|
}
|
|
8558
8605
|
/**
|
|
8559
|
-
* @description
|
|
8560
|
-
*
|
|
8561
|
-
*
|
|
8562
|
-
*
|
|
8563
|
-
* aborted question), abort the wedged OpenCode turn (`SIGINT` → `/session/:id/
|
|
8564
|
-
* abort`), post the "previous question cancelled" notice, then forward.
|
|
8606
|
+
* @description Retire the on-screen state of a pending OpenCode question the user walked away from: clear the bot's
|
|
8607
|
+
* pending-question state and neutralize the stale buttons message (edit it to a "cancelled" label so a late tap can't
|
|
8608
|
+
* re-answer an aborted question). Closing the question on the server is the caller's, before it aborts the turn.
|
|
8609
|
+
* Returns whether the buttons message itself was relabelled — it IS the cancellation notice then.
|
|
8565
8610
|
*
|
|
8566
|
-
* The
|
|
8567
|
-
*
|
|
8568
|
-
* `messageId` + header are captured BEFORE clearing, since `clearPendingQuestion`
|
|
8569
|
-
* drops the state.
|
|
8611
|
+
* The edit runs through `runQuestionLifecycleOp` (same serializer as the answer path) so it can't race a concurrent
|
|
8612
|
+
* re-post-to-bottom. The `messageId` + header are captured BEFORE clearing, since `clearPendingQuestion` drops the state.
|
|
8570
8613
|
*/
|
|
8571
|
-
async function
|
|
8614
|
+
async function retirePendingQuestion(key) {
|
|
8572
8615
|
const pending = pendingQuestions.get((0, sessionKey_1.keyToString)(key));
|
|
8573
8616
|
const cancelledMessageId = pending?.messageId ?? null;
|
|
8574
8617
|
const cancelledQuestion = pending?.data.questions[pending.currentIndex];
|
|
8575
|
-
// Whether we relabel the on-screen question bubble itself to "❌ … cancelled"
|
|
8576
|
-
// below. When we do, that IS the cancellation notice — a second standalone
|
|
8577
|
-
// "previous question cancelled" line is pure noise (the reported triple
|
|
8578
|
-
// message). The standalone notice is kept only for the no-bubble case.
|
|
8579
|
-
const didLabelQuestionMessage = cancelledMessageId !== null && cancelledQuestion !== undefined;
|
|
8580
8618
|
clearPendingQuestion(key);
|
|
8581
|
-
if (cancelledMessageId
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
}
|
|
8619
|
+
if (cancelledMessageId === null || !cancelledQuestion)
|
|
8620
|
+
return false;
|
|
8621
|
+
const header = cancelledQuestion.header || cancelledQuestion.question;
|
|
8622
|
+
await runQuestionLifecycleOp(key, () => editThreadMessage(key, cancelledMessageId, (0, i18n_1.t)('agent.question_cancelled_msg_label', { header })));
|
|
8623
|
+
return true;
|
|
8624
|
+
}
|
|
8625
|
+
/**
|
|
8626
|
+
* @description Cancel a pending OpenCode question because the user sent free-form
|
|
8627
|
+
* input instead of answering it, then deliver that input as a fresh prompt. In
|
|
8628
|
+
* order: retire the question's on-screen state ({@link retirePendingQuestion}),
|
|
8629
|
+
* close it on the server, abort the wedged OpenCode turn (`SIGINT` → `/session/:id/
|
|
8630
|
+
* abort`), post the "previous question cancelled" notice, then forward.
|
|
8631
|
+
*/
|
|
8632
|
+
async function cancelPendingQuestionAndForward(key, adapter, text, sentAtMs, replyContext, requestHeader) {
|
|
8633
|
+
// When the question bubble itself was relabelled to "❌ … cancelled", that IS the
|
|
8634
|
+
// cancellation notice — a second standalone "previous question cancelled" line is
|
|
8635
|
+
// pure noise (the reported triple message). The standalone notice is kept only
|
|
8636
|
+
// for the no-bubble case.
|
|
8637
|
+
const didLabelQuestionMessage = await retirePendingQuestion(key);
|
|
8585
8638
|
// Close the question on the OpenCode server too (not just the bot-local +
|
|
8586
8639
|
// Telegram state above): an abandoned-but-still-open question keeps getting
|
|
8587
8640
|
// re-found by `restoreOpenQuestion` (`GET /question` on every reattach) and
|
|
@@ -9942,6 +9995,7 @@ exports.COMMANDS_MENU = [
|
|
|
9942
9995
|
{ command: 'help', description: '❓ Context-aware help' },
|
|
9943
9996
|
{ command: 'doctor', description: '🔍 Self-diagnostics' },
|
|
9944
9997
|
{ command: 'bind', description: '📁 Bind thread to a subfolder' },
|
|
9998
|
+
{ command: 'topic', description: '🧵 New topic on a folder (name, then folder)' },
|
|
9945
9999
|
{ command: 'ls', description: '📂 List WORK_ROOT subfolders' },
|
|
9946
10000
|
{ command: 'list', description: '🧵 List all bound threads' },
|
|
9947
10001
|
{ command: 'mcp', description: '🔌 List active MCP servers' },
|
|
@@ -9969,6 +10023,7 @@ exports.COMMANDS_MENU = [
|
|
|
9969
10023
|
{ command: 'limits', description: '🚧 Usage limits — switch this topic' },
|
|
9970
10024
|
{ command: 'limits_autoswitch', description: '🔁 Automatic switching on a usage limit' },
|
|
9971
10025
|
{ command: 'stt', description: '🎙 Voice recognition: engines, language, local engine' },
|
|
10026
|
+
{ command: 'update', description: '🔄 Bot updates: automatic, notify only, off' },
|
|
9972
10027
|
{ command: 'schedule', description: '⏰ Schedule a prompt (agent does the work)' },
|
|
9973
10028
|
{ command: 'reminders', description: '🔔 Reminders — add, list, delete (no agent)' },
|
|
9974
10029
|
{ command: 'status', description: '📊 Show status' },
|
|
@@ -9984,7 +10039,8 @@ exports.COMMANDS_MENU = [
|
|
|
9984
10039
|
{ command: 'up', description: '⬆️ Arrow Up' },
|
|
9985
10040
|
{ command: 'down', description: '⬇️ Arrow Down' },
|
|
9986
10041
|
{ command: 'tab', description: '⇥ Tab' },
|
|
9987
|
-
{ command: '
|
|
10042
|
+
{ command: 'interrupt', description: '⏹ Interrupt the running turn — the next message steers it (alias /esc)' },
|
|
10043
|
+
{ command: 'esc', description: '⎋ Escape (interrupt / dismiss / cancel a pending login; alias /interrupt)' },
|
|
9988
10044
|
{ command: 'y', description: '✅ Send "y"' },
|
|
9989
10045
|
{ command: 'n', description: '❌ Send "n"' },
|
|
9990
10046
|
{ command: 'c', description: '🛑 Ctrl+C' },
|
|
@@ -11043,6 +11099,66 @@ async function resolveBotAdminRights(groupId, botUserId) {
|
|
|
11043
11099
|
return null;
|
|
11044
11100
|
}
|
|
11045
11101
|
}
|
|
11102
|
+
/**
|
|
11103
|
+
* @description The General topic the updater talks in: the paired group's General when a group is paired, else the owner's DM
|
|
11104
|
+
* General (`null` when neither exists — a Jira-only instance has no Telegram to say anything in).
|
|
11105
|
+
*/
|
|
11106
|
+
function getUpdateGeneralKey() {
|
|
11107
|
+
const groupId = getAllowedGroupId();
|
|
11108
|
+
if (groupId !== null)
|
|
11109
|
+
return (0, sessionKeyCodec_1.makeTelegramKey)(groupId, threadRouting_1.GENERAL_THREAD_ID);
|
|
11110
|
+
const ownerUserId = getOwnerUserId();
|
|
11111
|
+
return Number.isFinite(ownerUserId) ? (0, sessionKeyCodec_1.makeTelegramKey)(ownerUserId, threadRouting_1.DM_GENERAL_THREAD_ID) : null;
|
|
11112
|
+
}
|
|
11113
|
+
/**
|
|
11114
|
+
* @description Say an update message in General, worded in that chat's language: pinned when it asks the operator to act, with
|
|
11115
|
+
* the install tap (the version printed on the button is the version it installs) when it offers one. A failed send or pin is
|
|
11116
|
+
* logged — the updater never fails because Telegram did.
|
|
11117
|
+
*/
|
|
11118
|
+
async function postUpdateMessageToGeneral(message) {
|
|
11119
|
+
if (!ENV.isTelegramServed)
|
|
11120
|
+
return;
|
|
11121
|
+
const generalKey = getUpdateGeneralKey();
|
|
11122
|
+
if (generalKey === null) {
|
|
11123
|
+
console.log(`[update] no General topic to post "${message.kind}" in`);
|
|
11124
|
+
return;
|
|
11125
|
+
}
|
|
11126
|
+
const rendered = withThreadLocale(generalKey, () => (0, updateMessages_1.renderUpdateMessage)(message, (code, vars) => (0, i18n_1.t)(code, vars)));
|
|
11127
|
+
const extra = rendered.buttons.length > 0
|
|
11128
|
+
? telegraf_1.Markup.inlineKeyboard(rendered.buttons.map((button) => [telegraf_1.Markup.button.callback(button.label, button.callbackData)]))
|
|
11129
|
+
: {};
|
|
11130
|
+
const messageId = await replyToThread(generalKey, rendered.text, extra);
|
|
11131
|
+
if (messageId !== null && rendered.pin)
|
|
11132
|
+
await pinMessageQuiet(generalKey, messageId, { disableNotification: false });
|
|
11133
|
+
}
|
|
11134
|
+
/**
|
|
11135
|
+
* @description Whether the bot may be restarted now, from what it knows: no agent working in any bound topic (`checkIsWorking`, else
|
|
11136
|
+
* `checkIsBusy`), no input an adapter has not taken in, nothing held in the startup buffer, no sign-in, no in-memory wizard or pasted
|
|
11137
|
+
* key, and no human message within the quiet period. A sleeping or idle-but-alive session does not count: it survives the restart.
|
|
11138
|
+
*/
|
|
11139
|
+
function getUpdateIdleVerdict(isUserRequested) {
|
|
11140
|
+
const keys = getServedBindings().map(({ key }) => key);
|
|
11141
|
+
const isAnyAgentWorking = keys.some((key) => {
|
|
11142
|
+
const adapter = (0, createAdapter_1.getThreadAdapter)(key);
|
|
11143
|
+
return adapter.checkIsWorking ? adapter.checkIsWorking(key) : adapter.checkIsBusy?.(key) === true;
|
|
11144
|
+
});
|
|
11145
|
+
return (0, updatePolicy_1.getIdleVerdict)({
|
|
11146
|
+
isAnyAgentWorking,
|
|
11147
|
+
hasUnconsumedInput: keys.some((key) => (0, createAdapter_1.getThreadAdapter)(key).checkHasUnconsumedInput?.(key) === true),
|
|
11148
|
+
hasBufferedStartupPrompt: startupPromptBuffer.checkIsAnyPending(),
|
|
11149
|
+
isSignInRunning: checkIsAnyLoginInProgress(),
|
|
11150
|
+
isWizardOpen: reminderWizards.size > 0 ||
|
|
11151
|
+
awaitingFolderName.size > 0 ||
|
|
11152
|
+
awaitingModelSelection.size > 0 ||
|
|
11153
|
+
awaitingSessionSelection.size > 0 ||
|
|
11154
|
+
pendingProviderConnects.size > 0 ||
|
|
11155
|
+
checkIsAnySttInputAwaited(),
|
|
11156
|
+
lastHumanMessageAt: lastHumanInboundAt,
|
|
11157
|
+
now: Date.now(),
|
|
11158
|
+
quietMs: constants_1.quietPeriodMs,
|
|
11159
|
+
isUserRequested,
|
|
11160
|
+
});
|
|
11161
|
+
}
|
|
11046
11162
|
/**
|
|
11047
11163
|
* @description Boot-time readiness status (plan 2026-07-12): on startup tell the
|
|
11048
11164
|
* owner whether the bot can process messages, or list the setup steps still
|
|
@@ -11703,36 +11819,82 @@ async function startBot() {
|
|
|
11703
11819
|
// `stopOpenCodeServer()` or `adapter.stopSession()` here — tmux and
|
|
11704
11820
|
// opencode survive the bot process by design and are picked back up
|
|
11705
11821
|
// by `reattachExistingSessions` on the next boot.
|
|
11822
|
+
// The ordered sequence is the same for a signal and for an update's restart; only what happens AFTER it differs
|
|
11823
|
+
// (`exit` ends the process, the restart replaces it with a fresh image of the same install).
|
|
11824
|
+
const runShutdown = (signal, exit) => (0, shutdown_1.gracefulShutdown)({
|
|
11825
|
+
signal,
|
|
11826
|
+
// Without Telegram there is no polling to stop, and `bot.stop` throws when it never launched.
|
|
11827
|
+
bot: ENV.isTelegramServed ? bot : { stop: () => { } },
|
|
11828
|
+
state,
|
|
11829
|
+
releaseLock: instanceLock_1.releaseLock,
|
|
11830
|
+
exit,
|
|
11831
|
+
drainPendingUpdates: () => updateDispatcher.drainIdle(updateDrainTimeoutMs),
|
|
11832
|
+
finalizePendingOutput: () => finalizePendingOutputOnShutdown(),
|
|
11833
|
+
clearTransientFrames: () => sweepTransientFramesOnShutdown(),
|
|
11834
|
+
// A hot worker's reload keeps the sidecar for the next worker to adopt; the supervisor stops it for good.
|
|
11835
|
+
stopSidecars: (0, installManager_1.checkIsOpenCodeExternalHost)() ? undefined : () => sttOwner.stop(),
|
|
11836
|
+
cleanupTimers: () => {
|
|
11837
|
+
clearInterval(inMemoryGcInterval);
|
|
11838
|
+
clearInterval(heartbeatInterval);
|
|
11839
|
+
clearInterval(fileSweepInterval);
|
|
11840
|
+
clearInterval(rateSummaryInterval);
|
|
11841
|
+
// Scheduler (S8): clear every armed job timer; persisted nextRunAt
|
|
11842
|
+
// means the next boot's rearmAll picks them back up (catch-up replay).
|
|
11843
|
+
schedulerEngine?.shutdown();
|
|
11844
|
+
requestWakeUpEngine?.stop();
|
|
11845
|
+
jiraConnector?.stop();
|
|
11846
|
+
if (isSchedulerMcpStarted)
|
|
11847
|
+
void schedulerMcpHandle.stop().catch(() => { });
|
|
11848
|
+
updateEngine?.stop();
|
|
11849
|
+
},
|
|
11850
|
+
});
|
|
11706
11851
|
const shutdown = (signal) => {
|
|
11707
|
-
void (
|
|
11708
|
-
signal,
|
|
11709
|
-
// Without Telegram there is no polling to stop, and `bot.stop` throws when it never launched.
|
|
11710
|
-
bot: ENV.isTelegramServed ? bot : { stop: () => { } },
|
|
11711
|
-
state,
|
|
11712
|
-
releaseLock: instanceLock_1.releaseLock,
|
|
11713
|
-
exit: (code) => process.exit(code),
|
|
11714
|
-
drainPendingUpdates: () => updateDispatcher.drainIdle(updateDrainTimeoutMs),
|
|
11715
|
-
finalizePendingOutput: () => finalizePendingOutputOnShutdown(),
|
|
11716
|
-
clearTransientFrames: () => sweepTransientFramesOnShutdown(),
|
|
11717
|
-
// A hot worker's reload keeps the sidecar for the next worker to adopt; the supervisor stops it for good.
|
|
11718
|
-
stopSidecars: (0, installManager_1.checkIsOpenCodeExternalHost)() ? undefined : () => sttOwner.stop(),
|
|
11719
|
-
cleanupTimers: () => {
|
|
11720
|
-
clearInterval(inMemoryGcInterval);
|
|
11721
|
-
clearInterval(heartbeatInterval);
|
|
11722
|
-
clearInterval(fileSweepInterval);
|
|
11723
|
-
clearInterval(rateSummaryInterval);
|
|
11724
|
-
// Scheduler (S8): clear every armed job timer; persisted nextRunAt
|
|
11725
|
-
// means the next boot's rearmAll picks them back up (catch-up replay).
|
|
11726
|
-
schedulerEngine?.shutdown();
|
|
11727
|
-
requestWakeUpEngine?.stop();
|
|
11728
|
-
jiraConnector?.stop();
|
|
11729
|
-
if (isSchedulerMcpStarted)
|
|
11730
|
-
void schedulerMcpHandle.stop().catch(() => { });
|
|
11731
|
-
},
|
|
11732
|
-
});
|
|
11852
|
+
void runShutdown(signal, (code) => process.exit(code));
|
|
11733
11853
|
};
|
|
11734
11854
|
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
11735
11855
|
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
11856
|
+
// 7b. Automatic updates (`src/selfUpdate/`): the engine finds a new version, installs it when nobody is using the bot and
|
|
11857
|
+
// restarts it in place — the same sequence as a signal, then `process.execve` with the launch environment.
|
|
11858
|
+
const updateRuntime = await (0, updateRuntime_1.createUpdateRuntime)({
|
|
11859
|
+
dataDir: path.dirname(state.stateFilePath),
|
|
11860
|
+
packageRoot: path.resolve(__dirname, '..'),
|
|
11861
|
+
execPath: process.execPath,
|
|
11862
|
+
platform: process.platform,
|
|
11863
|
+
nodeVersion: process.versions.node,
|
|
11864
|
+
argv: process.argv,
|
|
11865
|
+
isHotMode: (0, installManager_1.checkIsOpenCodeExternalHost)(),
|
|
11866
|
+
});
|
|
11867
|
+
const readUpdateSettings = (0, instanceSettings_1.createUpdateSettingsReader)(() => (0, instanceSettings_1.getInstanceSettingsPath)(), (line) => console.error(line));
|
|
11868
|
+
updateEngine = (0, updateEngine_1.createUpdateEngine)({
|
|
11869
|
+
...updateRuntime,
|
|
11870
|
+
now: () => Date.now(),
|
|
11871
|
+
setTimer: (callback, delayMs) => {
|
|
11872
|
+
const timer = setTimeout(callback, delayMs);
|
|
11873
|
+
timer.unref();
|
|
11874
|
+
return timer;
|
|
11875
|
+
},
|
|
11876
|
+
clearTimer: (handle) => clearTimeout(handle),
|
|
11877
|
+
log: (line) => console.log(line),
|
|
11878
|
+
getSettings: readUpdateSettings,
|
|
11879
|
+
state: {
|
|
11880
|
+
getRecord: () => state.getSelfUpdateRecord(),
|
|
11881
|
+
setRecord: (record) => state.setSelfUpdateRecord(record),
|
|
11882
|
+
getLastRun: () => state.getLastRunVersion(),
|
|
11883
|
+
setLastRun: (lastRun) => state.setLastRunVersion(lastRun),
|
|
11884
|
+
},
|
|
11885
|
+
getIdleVerdict: getUpdateIdleVerdict,
|
|
11886
|
+
postToGeneral: postUpdateMessageToGeneral,
|
|
11887
|
+
restartInPlace: () => runShutdown('update', () => (0, restart_1.reexecProcess)(updateRuntime.reexecTarget, (0, launchEnvironment_1.getLaunchEnvironment)(), (0, restart_1.createDefaultReexecDeps)())),
|
|
11888
|
+
});
|
|
11889
|
+
const startUpdates = async () => {
|
|
11890
|
+
try {
|
|
11891
|
+
await updateEngine?.runBootStep();
|
|
11892
|
+
}
|
|
11893
|
+
catch (e) {
|
|
11894
|
+
console.warn('[update] boot step failed:', e instanceof Error ? e.message : e);
|
|
11895
|
+
}
|
|
11896
|
+
updateEngine?.start();
|
|
11897
|
+
};
|
|
11736
11898
|
// 8. Launch. `dropPendingUpdates` follows the boot classifier: on a hot
|
|
11737
11899
|
// reload we KEEP the buffered backlog so a message typed during the
|
|
11738
11900
|
// ~1s reload window still routes to its live agent; on a cold start
|
|
@@ -11749,6 +11911,7 @@ async function startBot() {
|
|
|
11749
11911
|
if (!ENV.isTelegramServed) {
|
|
11750
11912
|
// No polling: the MCP server and the connector timers keep the process alive.
|
|
11751
11913
|
console.log('[boot] running without Telegram (connectors: ' + [...ENV.servedPlatforms].join(', ') + ')');
|
|
11914
|
+
await startUpdates();
|
|
11752
11915
|
return;
|
|
11753
11916
|
}
|
|
11754
11917
|
try {
|
|
@@ -11757,6 +11920,7 @@ async function startBot() {
|
|
|
11757
11920
|
catch (e) {
|
|
11758
11921
|
console.warn('[startup-status] failed:', e instanceof Error ? e.message : e);
|
|
11759
11922
|
}
|
|
11923
|
+
await startUpdates();
|
|
11760
11924
|
// The local speech engine: clean up after an earlier run, re-attach to / start its install. Starts no process.
|
|
11761
11925
|
void bootSttLocalEngine().catch((e) => console.warn('[STT] boot step failed:', e instanceof Error ? e.message : e));
|
|
11762
11926
|
console.log(`Launching Telegraf bot (long polling, dropPendingUpdates=${bootMode.dropPendingUpdates})...`);
|