clisbot 0.1.41 → 0.1.43
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/README.md +28 -67
- package/config/clisbot.json.template +1 -1
- package/dist/main.js +1090 -386
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -59566,6 +59566,139 @@ var agentAuthOverrideSchema = exports_external.object({
|
|
|
59566
59566
|
roles: exports_external.record(exports_external.string(), authRoleOverrideSchema).default({})
|
|
59567
59567
|
});
|
|
59568
59568
|
|
|
59569
|
+
// src/config/runtime-monitor-backoff.ts
|
|
59570
|
+
var LEGACY_DEFAULT_RUNTIME_MONITOR_RESTART_BACKOFF = {
|
|
59571
|
+
fastRetry: {
|
|
59572
|
+
delaySeconds: 10,
|
|
59573
|
+
maxRestarts: 3
|
|
59574
|
+
},
|
|
59575
|
+
stages: [
|
|
59576
|
+
{
|
|
59577
|
+
delayMinutes: 15,
|
|
59578
|
+
maxRestarts: 4
|
|
59579
|
+
},
|
|
59580
|
+
{
|
|
59581
|
+
delayMinutes: 30,
|
|
59582
|
+
maxRestarts: 4
|
|
59583
|
+
}
|
|
59584
|
+
]
|
|
59585
|
+
};
|
|
59586
|
+
var DEFAULT_RUNTIME_MONITOR_RESTART_BACKOFF = {
|
|
59587
|
+
fastRetry: {
|
|
59588
|
+
delaySeconds: 10,
|
|
59589
|
+
maxRestarts: 3
|
|
59590
|
+
},
|
|
59591
|
+
stages: [
|
|
59592
|
+
{
|
|
59593
|
+
delayMinutes: 1,
|
|
59594
|
+
maxRestarts: 2
|
|
59595
|
+
},
|
|
59596
|
+
{
|
|
59597
|
+
delayMinutes: 3,
|
|
59598
|
+
maxRestarts: 2
|
|
59599
|
+
},
|
|
59600
|
+
{
|
|
59601
|
+
delayMinutes: 5,
|
|
59602
|
+
maxRestarts: 2
|
|
59603
|
+
},
|
|
59604
|
+
{
|
|
59605
|
+
delayMinutes: 10,
|
|
59606
|
+
maxRestarts: 3
|
|
59607
|
+
},
|
|
59608
|
+
{
|
|
59609
|
+
delayMinutes: 15,
|
|
59610
|
+
maxRestarts: 4
|
|
59611
|
+
},
|
|
59612
|
+
{
|
|
59613
|
+
delayMinutes: 30,
|
|
59614
|
+
maxRestarts: 4
|
|
59615
|
+
}
|
|
59616
|
+
]
|
|
59617
|
+
};
|
|
59618
|
+
var RUNTIME_MONITOR_RESTART_RESET_AFTER_MS = 15 * 60000;
|
|
59619
|
+
function cloneRestartBackoff(restartBackoff) {
|
|
59620
|
+
return {
|
|
59621
|
+
fastRetry: {
|
|
59622
|
+
...restartBackoff.fastRetry
|
|
59623
|
+
},
|
|
59624
|
+
stages: restartBackoff.stages.map((stage) => ({
|
|
59625
|
+
...stage
|
|
59626
|
+
}))
|
|
59627
|
+
};
|
|
59628
|
+
}
|
|
59629
|
+
function matchesRestartBackoffShape(left, right) {
|
|
59630
|
+
if (left.fastRetry.delaySeconds !== right.fastRetry.delaySeconds || left.fastRetry.maxRestarts !== right.fastRetry.maxRestarts || left.stages.length !== right.stages.length) {
|
|
59631
|
+
return false;
|
|
59632
|
+
}
|
|
59633
|
+
return left.stages.every((stage, index) => {
|
|
59634
|
+
const other = right.stages[index];
|
|
59635
|
+
return other != null && stage.delayMinutes === other.delayMinutes && stage.maxRestarts === other.maxRestarts;
|
|
59636
|
+
});
|
|
59637
|
+
}
|
|
59638
|
+
function getDefaultRuntimeMonitorRestartBackoff() {
|
|
59639
|
+
return cloneRestartBackoff(DEFAULT_RUNTIME_MONITOR_RESTART_BACKOFF);
|
|
59640
|
+
}
|
|
59641
|
+
function normalizeRuntimeMonitorRestartBackoff(restartBackoff) {
|
|
59642
|
+
if (matchesRestartBackoffShape(restartBackoff, LEGACY_DEFAULT_RUNTIME_MONITOR_RESTART_BACKOFF)) {
|
|
59643
|
+
return getDefaultRuntimeMonitorRestartBackoff();
|
|
59644
|
+
}
|
|
59645
|
+
return cloneRestartBackoff(restartBackoff);
|
|
59646
|
+
}
|
|
59647
|
+
function getConfiguredRuntimeMonitorRestartBudget(restartBackoff) {
|
|
59648
|
+
const normalized = normalizeRuntimeMonitorRestartBackoff(restartBackoff);
|
|
59649
|
+
return normalized.fastRetry.maxRestarts + normalized.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
|
|
59650
|
+
}
|
|
59651
|
+
function getRuntimeMonitorRestartPlan(restartBackoff, restartNumber) {
|
|
59652
|
+
const normalized = normalizeRuntimeMonitorRestartBackoff(restartBackoff);
|
|
59653
|
+
const totalConfiguredRestarts = getConfiguredRuntimeMonitorRestartBudget(normalized);
|
|
59654
|
+
const fastRetryMaxRestarts = normalized.fastRetry.maxRestarts;
|
|
59655
|
+
if (restartNumber >= 1 && restartNumber <= fastRetryMaxRestarts) {
|
|
59656
|
+
return {
|
|
59657
|
+
mode: "fast-retry",
|
|
59658
|
+
stageIndex: -1,
|
|
59659
|
+
delayMs: normalized.fastRetry.delaySeconds * 1000,
|
|
59660
|
+
restartAttemptInStage: restartNumber,
|
|
59661
|
+
restartsRemaining: Math.max(0, totalConfiguredRestarts - restartNumber),
|
|
59662
|
+
totalConfiguredRestarts,
|
|
59663
|
+
stageMaxRestarts: fastRetryMaxRestarts,
|
|
59664
|
+
repeatingFinalStage: false
|
|
59665
|
+
};
|
|
59666
|
+
}
|
|
59667
|
+
let completedRestarts = fastRetryMaxRestarts;
|
|
59668
|
+
for (let index = 0;index < normalized.stages.length; index += 1) {
|
|
59669
|
+
const stage = normalized.stages[index];
|
|
59670
|
+
const stageStart = completedRestarts + 1;
|
|
59671
|
+
const stageEnd = completedRestarts + stage.maxRestarts;
|
|
59672
|
+
if (restartNumber >= stageStart && restartNumber <= stageEnd) {
|
|
59673
|
+
return {
|
|
59674
|
+
mode: "backoff",
|
|
59675
|
+
stageIndex: index,
|
|
59676
|
+
delayMs: stage.delayMinutes * 60000,
|
|
59677
|
+
restartAttemptInStage: restartNumber - completedRestarts,
|
|
59678
|
+
restartsRemaining: Math.max(0, totalConfiguredRestarts - restartNumber),
|
|
59679
|
+
totalConfiguredRestarts,
|
|
59680
|
+
stageMaxRestarts: stage.maxRestarts,
|
|
59681
|
+
repeatingFinalStage: false
|
|
59682
|
+
};
|
|
59683
|
+
}
|
|
59684
|
+
completedRestarts = stageEnd;
|
|
59685
|
+
}
|
|
59686
|
+
const finalStage = normalized.stages.at(-1);
|
|
59687
|
+
if (!finalStage) {
|
|
59688
|
+
return null;
|
|
59689
|
+
}
|
|
59690
|
+
return {
|
|
59691
|
+
mode: "backoff",
|
|
59692
|
+
stageIndex: normalized.stages.length - 1,
|
|
59693
|
+
delayMs: finalStage.delayMinutes * 60000,
|
|
59694
|
+
restartAttemptInStage: Math.max(1, restartNumber - completedRestarts),
|
|
59695
|
+
restartsRemaining: 0,
|
|
59696
|
+
totalConfiguredRestarts,
|
|
59697
|
+
stageMaxRestarts: finalStage.maxRestarts,
|
|
59698
|
+
repeatingFinalStage: true
|
|
59699
|
+
};
|
|
59700
|
+
}
|
|
59701
|
+
|
|
59569
59702
|
// src/config/schema.ts
|
|
59570
59703
|
var defaultSessionIdPattern = "\\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\b";
|
|
59571
59704
|
var runnerSessionIdCreateSchema = exports_external.object({
|
|
@@ -60017,6 +60150,7 @@ var appControlLoopSchema = exports_external.object({
|
|
|
60017
60150
|
defaultIntervalMinutes: exports_external.number().int().positive().optional(),
|
|
60018
60151
|
maxTimes: exports_external.number().int().positive().optional()
|
|
60019
60152
|
});
|
|
60153
|
+
var defaultRuntimeMonitorRestartBackoff = getDefaultRuntimeMonitorRestartBackoff();
|
|
60020
60154
|
var appControlRuntimeMonitorSchema = exports_external.object({
|
|
60021
60155
|
restartBackoff: exports_external.object({
|
|
60022
60156
|
fastRetry: exports_external.object({
|
|
@@ -60029,32 +60163,8 @@ var appControlRuntimeMonitorSchema = exports_external.object({
|
|
|
60029
60163
|
stages: exports_external.array(exports_external.object({
|
|
60030
60164
|
delayMinutes: exports_external.number().int().positive().default(15),
|
|
60031
60165
|
maxRestarts: exports_external.number().int().positive().default(4)
|
|
60032
|
-
})).min(1).default(
|
|
60033
|
-
|
|
60034
|
-
delayMinutes: 15,
|
|
60035
|
-
maxRestarts: 4
|
|
60036
|
-
},
|
|
60037
|
-
{
|
|
60038
|
-
delayMinutes: 30,
|
|
60039
|
-
maxRestarts: 4
|
|
60040
|
-
}
|
|
60041
|
-
])
|
|
60042
|
-
}).default({
|
|
60043
|
-
fastRetry: {
|
|
60044
|
-
delaySeconds: 10,
|
|
60045
|
-
maxRestarts: 3
|
|
60046
|
-
},
|
|
60047
|
-
stages: [
|
|
60048
|
-
{
|
|
60049
|
-
delayMinutes: 15,
|
|
60050
|
-
maxRestarts: 4
|
|
60051
|
-
},
|
|
60052
|
-
{
|
|
60053
|
-
delayMinutes: 30,
|
|
60054
|
-
maxRestarts: 4
|
|
60055
|
-
}
|
|
60056
|
-
]
|
|
60057
|
-
}),
|
|
60166
|
+
})).min(1).default(defaultRuntimeMonitorRestartBackoff.stages)
|
|
60167
|
+
}).default(defaultRuntimeMonitorRestartBackoff),
|
|
60058
60168
|
ownerAlerts: exports_external.object({
|
|
60059
60169
|
enabled: exports_external.boolean().default(true),
|
|
60060
60170
|
minIntervalMinutes: exports_external.number().int().positive().default(30)
|
|
@@ -60222,10 +60332,10 @@ var agentsDefaultsSchema = exports_external.object({
|
|
|
60222
60332
|
});
|
|
60223
60333
|
var clisbotConfigSchema = exports_external.object({
|
|
60224
60334
|
meta: exports_external.object({
|
|
60225
|
-
schemaVersion: exports_external.string().min(1).default("0.1.
|
|
60335
|
+
schemaVersion: exports_external.string().min(1).default("0.1.43"),
|
|
60226
60336
|
lastTouchedAt: exports_external.string().optional()
|
|
60227
60337
|
}).default({
|
|
60228
|
-
schemaVersion: "0.1.
|
|
60338
|
+
schemaVersion: "0.1.43"
|
|
60229
60339
|
}),
|
|
60230
60340
|
app: exports_external.object({
|
|
60231
60341
|
session: appSessionSchema.default({
|
|
@@ -60248,22 +60358,7 @@ var clisbotConfigSchema = exports_external.object({
|
|
|
60248
60358
|
maxActiveLoops: 10
|
|
60249
60359
|
}),
|
|
60250
60360
|
runtimeMonitor: appControlRuntimeMonitorSchema.default({
|
|
60251
|
-
restartBackoff:
|
|
60252
|
-
fastRetry: {
|
|
60253
|
-
delaySeconds: 10,
|
|
60254
|
-
maxRestarts: 3
|
|
60255
|
-
},
|
|
60256
|
-
stages: [
|
|
60257
|
-
{
|
|
60258
|
-
delayMinutes: 15,
|
|
60259
|
-
maxRestarts: 4
|
|
60260
|
-
},
|
|
60261
|
-
{
|
|
60262
|
-
delayMinutes: 30,
|
|
60263
|
-
maxRestarts: 4
|
|
60264
|
-
}
|
|
60265
|
-
]
|
|
60266
|
-
},
|
|
60361
|
+
restartBackoff: defaultRuntimeMonitorRestartBackoff,
|
|
60267
60362
|
ownerAlerts: {
|
|
60268
60363
|
enabled: true,
|
|
60269
60364
|
minIntervalMinutes: 30
|
|
@@ -60283,22 +60378,7 @@ var clisbotConfigSchema = exports_external.object({
|
|
|
60283
60378
|
maxActiveLoops: 10
|
|
60284
60379
|
},
|
|
60285
60380
|
runtimeMonitor: {
|
|
60286
|
-
restartBackoff:
|
|
60287
|
-
fastRetry: {
|
|
60288
|
-
delaySeconds: 10,
|
|
60289
|
-
maxRestarts: 3
|
|
60290
|
-
},
|
|
60291
|
-
stages: [
|
|
60292
|
-
{
|
|
60293
|
-
delayMinutes: 15,
|
|
60294
|
-
maxRestarts: 4
|
|
60295
|
-
},
|
|
60296
|
-
{
|
|
60297
|
-
delayMinutes: 30,
|
|
60298
|
-
maxRestarts: 4
|
|
60299
|
-
}
|
|
60300
|
-
]
|
|
60301
|
-
},
|
|
60381
|
+
restartBackoff: defaultRuntimeMonitorRestartBackoff,
|
|
60302
60382
|
ownerAlerts: {
|
|
60303
60383
|
enabled: true,
|
|
60304
60384
|
minIntervalMinutes: 30
|
|
@@ -60326,22 +60406,7 @@ var clisbotConfigSchema = exports_external.object({
|
|
|
60326
60406
|
maxActiveLoops: 10
|
|
60327
60407
|
},
|
|
60328
60408
|
runtimeMonitor: {
|
|
60329
|
-
restartBackoff:
|
|
60330
|
-
fastRetry: {
|
|
60331
|
-
delaySeconds: 10,
|
|
60332
|
-
maxRestarts: 3
|
|
60333
|
-
},
|
|
60334
|
-
stages: [
|
|
60335
|
-
{
|
|
60336
|
-
delayMinutes: 15,
|
|
60337
|
-
maxRestarts: 4
|
|
60338
|
-
},
|
|
60339
|
-
{
|
|
60340
|
-
delayMinutes: 30,
|
|
60341
|
-
maxRestarts: 4
|
|
60342
|
-
}
|
|
60343
|
-
]
|
|
60344
|
-
},
|
|
60409
|
+
restartBackoff: defaultRuntimeMonitorRestartBackoff,
|
|
60345
60410
|
ownerAlerts: {
|
|
60346
60411
|
enabled: true,
|
|
60347
60412
|
minIntervalMinutes: 30
|
|
@@ -61022,6 +61087,8 @@ function materializeLoadedConfig(expandedConfigPath, validated) {
|
|
|
61022
61087
|
socketPath: expandHomePath(validated.agents.defaults.runner.defaults.tmux.socketPath || getDefaultTmuxSocketPath())
|
|
61023
61088
|
}
|
|
61024
61089
|
};
|
|
61090
|
+
runtimeRaw.app.control.runtimeMonitor.restartBackoff = normalizeRuntimeMonitorRestartBackoff(runtimeRaw.app.control.runtimeMonitor.restartBackoff);
|
|
61091
|
+
runtimeRaw.control.runtimeMonitor.restartBackoff = normalizeRuntimeMonitorRestartBackoff(runtimeRaw.control.runtimeMonitor.restartBackoff);
|
|
61025
61092
|
return {
|
|
61026
61093
|
configPath: expandedConfigPath,
|
|
61027
61094
|
processedEventsPath: getDefaultProcessedEventsPath(),
|
|
@@ -61104,9 +61171,10 @@ function renderDefaultConfigTemplate(options = {}) {
|
|
|
61104
61171
|
const sessionStorePath = collapseHomePath(getDefaultSessionStorePath());
|
|
61105
61172
|
const workspaceTemplate = collapseHomePath(getDefaultWorkspaceTemplate());
|
|
61106
61173
|
const defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
61174
|
+
const defaultRuntimeMonitorRestartBackoff2 = getDefaultRuntimeMonitorRestartBackoff();
|
|
61107
61175
|
return JSON.stringify({
|
|
61108
61176
|
meta: {
|
|
61109
|
-
schemaVersion: "0.1.
|
|
61177
|
+
schemaVersion: "0.1.43",
|
|
61110
61178
|
lastTouchedAt: new Date().toISOString()
|
|
61111
61179
|
},
|
|
61112
61180
|
app: {
|
|
@@ -61148,22 +61216,7 @@ function renderDefaultConfigTemplate(options = {}) {
|
|
|
61148
61216
|
defaultTimezone
|
|
61149
61217
|
},
|
|
61150
61218
|
runtimeMonitor: {
|
|
61151
|
-
restartBackoff:
|
|
61152
|
-
fastRetry: {
|
|
61153
|
-
delaySeconds: 10,
|
|
61154
|
-
maxRestarts: 3
|
|
61155
|
-
},
|
|
61156
|
-
stages: [
|
|
61157
|
-
{
|
|
61158
|
-
delayMinutes: 15,
|
|
61159
|
-
maxRestarts: 4
|
|
61160
|
-
},
|
|
61161
|
-
{
|
|
61162
|
-
delayMinutes: 30,
|
|
61163
|
-
maxRestarts: 4
|
|
61164
|
-
}
|
|
61165
|
-
]
|
|
61166
|
-
},
|
|
61219
|
+
restartBackoff: defaultRuntimeMonitorRestartBackoff2,
|
|
61167
61220
|
ownerAlerts: {
|
|
61168
61221
|
enabled: true,
|
|
61169
61222
|
minIntervalMinutes: 30
|
|
@@ -63382,11 +63435,14 @@ function shellQuote2(value) {
|
|
|
63382
63435
|
}
|
|
63383
63436
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
63384
63437
|
}
|
|
63385
|
-
function getClisbotMainScriptPath() {
|
|
63386
|
-
|
|
63438
|
+
function getClisbotMainScriptPath(moduleUrl = import.meta.url) {
|
|
63439
|
+
if (isPackagedRuntime(moduleUrl)) {
|
|
63440
|
+
return fileURLToPath3(moduleUrl);
|
|
63441
|
+
}
|
|
63442
|
+
return fileURLToPath3(new URL("../main.ts", moduleUrl));
|
|
63387
63443
|
}
|
|
63388
|
-
function isPackagedRuntime() {
|
|
63389
|
-
const currentModulePath = fileURLToPath3(
|
|
63444
|
+
function isPackagedRuntime(moduleUrl = import.meta.url) {
|
|
63445
|
+
const currentModulePath = fileURLToPath3(moduleUrl);
|
|
63390
63446
|
return currentModulePath.includes(`${sep}dist${sep}`);
|
|
63391
63447
|
}
|
|
63392
63448
|
function getClisbotWrapperPath() {
|
|
@@ -63401,9 +63457,9 @@ function getClisbotPromptCommand() {
|
|
|
63401
63457
|
function getClisbotWrapperDir() {
|
|
63402
63458
|
return dirname8(getClisbotWrapperPath());
|
|
63403
63459
|
}
|
|
63404
|
-
function renderClisbotWrapperScript() {
|
|
63460
|
+
function renderClisbotWrapperScript(options = {}) {
|
|
63405
63461
|
const execPath = process.execPath;
|
|
63406
|
-
const mainScriptPath = getClisbotMainScriptPath();
|
|
63462
|
+
const mainScriptPath = getClisbotMainScriptPath(options.moduleUrl);
|
|
63407
63463
|
const cliName = getRenderedCliName();
|
|
63408
63464
|
return [
|
|
63409
63465
|
"#!/usr/bin/env bash",
|
|
@@ -64076,9 +64132,11 @@ function parseAgentCommand(text, options = {}) {
|
|
|
64076
64132
|
};
|
|
64077
64133
|
}
|
|
64078
64134
|
if (lowered === "transcript") {
|
|
64135
|
+
const transcriptMode = withoutSlash.slice(command.length).trim().toLowerCase() === "full" ? "full" : "default";
|
|
64079
64136
|
return {
|
|
64080
64137
|
type: "control",
|
|
64081
|
-
name: "transcript"
|
|
64138
|
+
name: "transcript",
|
|
64139
|
+
mode: transcriptMode
|
|
64082
64140
|
};
|
|
64083
64141
|
}
|
|
64084
64142
|
if (lowered === "attach") {
|
|
@@ -64123,6 +64181,10 @@ function parseAgentCommand(text, options = {}) {
|
|
|
64123
64181
|
if (lowered === "followup") {
|
|
64124
64182
|
return parseFollowUpSlashCommand(withoutSlash.slice(command.length).trim().toLowerCase());
|
|
64125
64183
|
}
|
|
64184
|
+
if (lowered === "mention") {
|
|
64185
|
+
const scope = withoutSlash.slice(command.length).trim().toLowerCase();
|
|
64186
|
+
return parseFollowUpSlashCommand(scope ? `mention-only ${scope}` : "mention-only");
|
|
64187
|
+
}
|
|
64126
64188
|
if (lowered === "pause") {
|
|
64127
64189
|
return parseFollowUpSlashCommand("pause");
|
|
64128
64190
|
}
|
|
@@ -64339,7 +64401,8 @@ function renderAgentControlSlashHelp() {
|
|
|
64339
64401
|
"- `/status`: show the current route status and operator setup commands",
|
|
64340
64402
|
"- `/help`: show available control slash commands",
|
|
64341
64403
|
"- `/whoami`: show the current platform, route, and sender identity details",
|
|
64342
|
-
"- `/transcript`: show
|
|
64404
|
+
"- `/transcript`: show a short recent session snapshot when the route verbose policy allows it",
|
|
64405
|
+
"- `/transcript full`: show a longer session snapshot when you need the full pane context",
|
|
64343
64406
|
"- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
|
|
64344
64407
|
"- `/detach`: stop live updates for this thread while still posting the final result here",
|
|
64345
64408
|
"- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
|
|
@@ -64347,11 +64410,11 @@ function renderAgentControlSlashHelp() {
|
|
|
64347
64410
|
"- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
|
|
64348
64411
|
"- `/followup status`: show the current conversation follow-up policy",
|
|
64349
64412
|
"- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
|
|
64350
|
-
"- `/followup mention-only`: require explicit mention for each later turn",
|
|
64351
|
-
"- `/followup
|
|
64352
|
-
"- `/followup
|
|
64353
|
-
"- `/pause`:
|
|
64354
|
-
"- `/resume`:
|
|
64413
|
+
"- `/followup mention-only` or `/mention`: require explicit mention for each later turn",
|
|
64414
|
+
"- `/followup mention-only channel` or `/mention channel`: persist mention-only as the default for the current channel or group",
|
|
64415
|
+
"- `/followup mention-only all` or `/mention all`: persist mention-only as the default for all routed conversations on this bot",
|
|
64416
|
+
"- `/followup pause` or `/pause`: stop passive follow-up until the next explicit mention",
|
|
64417
|
+
"- `/followup resume` or `/resume`: clear the runtime override and restore config defaults",
|
|
64355
64418
|
"- `/streaming status|on|off|latest|all`: show or change streaming mode for this surface",
|
|
64356
64419
|
"- `/responsemode status`: show the configured response mode for this surface",
|
|
64357
64420
|
"- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
|
|
@@ -64401,31 +64464,44 @@ function parseWatchCommand(raw) {
|
|
|
64401
64464
|
durationMs: parsedDurationMs ?? undefined
|
|
64402
64465
|
};
|
|
64403
64466
|
}
|
|
64467
|
+
function parseFollowUpScope(raw) {
|
|
64468
|
+
if (raw === "channel") {
|
|
64469
|
+
return "channel";
|
|
64470
|
+
}
|
|
64471
|
+
if (raw === "all") {
|
|
64472
|
+
return "all";
|
|
64473
|
+
}
|
|
64474
|
+
return "conversation";
|
|
64475
|
+
}
|
|
64404
64476
|
function parseFollowUpSlashCommand(action) {
|
|
64405
|
-
|
|
64477
|
+
const [rawAction = "", rawScope = ""] = action.split(/\s+/, 2).map((token) => token.trim().toLowerCase());
|
|
64478
|
+
const scope = parseFollowUpScope(rawScope);
|
|
64479
|
+
if (!rawAction || rawAction === "status") {
|
|
64406
64480
|
return {
|
|
64407
64481
|
type: "control",
|
|
64408
64482
|
name: "followup",
|
|
64409
64483
|
action: "status"
|
|
64410
64484
|
};
|
|
64411
64485
|
}
|
|
64412
|
-
if (
|
|
64486
|
+
if (rawAction === "auto") {
|
|
64413
64487
|
return {
|
|
64414
64488
|
type: "control",
|
|
64415
64489
|
name: "followup",
|
|
64416
64490
|
action: "auto",
|
|
64417
|
-
mode: "auto"
|
|
64491
|
+
mode: "auto",
|
|
64492
|
+
scope
|
|
64418
64493
|
};
|
|
64419
64494
|
}
|
|
64420
|
-
if (
|
|
64495
|
+
if (rawAction === "mention-only") {
|
|
64421
64496
|
return {
|
|
64422
64497
|
type: "control",
|
|
64423
64498
|
name: "followup",
|
|
64424
64499
|
action: "mention-only",
|
|
64425
|
-
mode: "mention-only"
|
|
64500
|
+
mode: "mention-only",
|
|
64501
|
+
scope
|
|
64426
64502
|
};
|
|
64427
64503
|
}
|
|
64428
|
-
if (
|
|
64504
|
+
if (rawAction === "pause") {
|
|
64429
64505
|
return {
|
|
64430
64506
|
type: "control",
|
|
64431
64507
|
name: "followup",
|
|
@@ -64433,7 +64509,7 @@ function parseFollowUpSlashCommand(action) {
|
|
|
64433
64509
|
mode: "paused"
|
|
64434
64510
|
};
|
|
64435
64511
|
}
|
|
64436
|
-
if (
|
|
64512
|
+
if (rawAction === "resume") {
|
|
64437
64513
|
return {
|
|
64438
64514
|
type: "control",
|
|
64439
64515
|
name: "followup",
|
|
@@ -65126,7 +65202,7 @@ var REPLY_COMMAND = `{{reply_command_base}}
|
|
|
65126
65202
|
<user-facing reply>
|
|
65127
65203
|
__CLISBOT_MESSAGE__
|
|
65128
65204
|
)" \\
|
|
65129
|
-
[--
|
|
65205
|
+
[--file /absolute/path/to/file]`;
|
|
65130
65206
|
var REPLY_RULES = `When replying to the user:
|
|
65131
65207
|
- put the user-facing message inside the --message body of that command
|
|
65132
65208
|
{{progress_rules_block}}- {{final_rule_line}}`;
|
|
@@ -65159,8 +65235,10 @@ var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
|
|
|
65159
65235
|
{{thread_clause}} --input md \\
|
|
65160
65236
|
--render native \\
|
|
65161
65237
|
`;
|
|
65162
|
-
var SLACK_REPLY_STYLE_HINT =
|
|
65163
|
-
|
|
65238
|
+
var SLACK_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
|
|
65239
|
+
Keep each paragraph, list, or code block under 2500 chars.`;
|
|
65240
|
+
var TELEGRAM_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
|
|
65241
|
+
Keep the Markdown body under 3000 chars.`;
|
|
65164
65242
|
var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
|
|
65165
65243
|
var EMPTY_ACCOUNT_CLAUSE = "";
|
|
65166
65244
|
var SLACK_THREAD_CLAUSE = " --thread-id {{thread_ts}} \\\n";
|
|
@@ -66706,6 +66784,20 @@ function renderSlackTranscript(params) {
|
|
|
66706
66784
|
${body}
|
|
66707
66785
|
\`\`\``;
|
|
66708
66786
|
}
|
|
66787
|
+
function renderCompactChannelTranscript(params) {
|
|
66788
|
+
const body = escapeCodeFence(truncateTail(params.snapshot || "(no tmux output yet)", params.maxChars));
|
|
66789
|
+
const fullCommand = params.fullCommand ?? "/transcript full";
|
|
66790
|
+
return [
|
|
66791
|
+
"Transcript",
|
|
66792
|
+
"",
|
|
66793
|
+
"Recent session snapshot:",
|
|
66794
|
+
"```",
|
|
66795
|
+
body,
|
|
66796
|
+
"```",
|
|
66797
|
+
`Use \`${fullCommand}\` if you want the longer pane snapshot.`
|
|
66798
|
+
].join(`
|
|
66799
|
+
`);
|
|
66800
|
+
}
|
|
66709
66801
|
var renderSlackSnapshot = renderSlackTranscript;
|
|
66710
66802
|
var renderChannelSnapshot = renderSlackSnapshot;
|
|
66711
66803
|
function resolveDetachedInteractionNote(params) {
|
|
@@ -66742,12 +66834,16 @@ var PASTE_SETTLE_POLL_INTERVAL_MS = 40;
|
|
|
66742
66834
|
var PASTE_SETTLE_QUIET_WINDOW_MS = 60;
|
|
66743
66835
|
var PASTE_SETTLE_MULTILINE_MAX_WAIT_MS = 800;
|
|
66744
66836
|
var PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS = 80;
|
|
66837
|
+
var PASTE_CONFIRM_MAX_ATTEMPTS = 3;
|
|
66745
66838
|
var PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS = 40;
|
|
66746
66839
|
var PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS = 160;
|
|
66747
66840
|
var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
|
|
66748
66841
|
var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
|
|
66749
66842
|
var SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS = 40;
|
|
66750
66843
|
var SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS = 320;
|
|
66844
|
+
var POST_STATUS_SETTLE_POLL_INTERVAL_MS = 40;
|
|
66845
|
+
var POST_STATUS_SETTLE_QUIET_WINDOW_MS = 80;
|
|
66846
|
+
var POST_STATUS_SETTLE_MAX_WAIT_MS = 240;
|
|
66751
66847
|
var TMUX_MISSING_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
|
|
66752
66848
|
var TMUX_MISSING_SESSION_PATTERN = /(?:can't find session:|no server running on )/i;
|
|
66753
66849
|
var TMUX_SERVER_UNAVAILABLE_PATTERN = /(?:No such file or directory|error connecting to|failed to connect to server)/i;
|
|
@@ -66760,44 +66856,51 @@ class TmuxBootstrapSessionLostError extends Error {
|
|
|
66760
66856
|
this.name = "TmuxBootstrapSessionLostError";
|
|
66761
66857
|
}
|
|
66762
66858
|
}
|
|
66859
|
+
|
|
66860
|
+
class TmuxPasteUnconfirmedError extends Error {
|
|
66861
|
+
attempts;
|
|
66862
|
+
constructor(attempts) {
|
|
66863
|
+
super(`tmux paste was not confirmed after ${attempts} delivery attempts. clisbot did not send Enter because the prompt was not truthfully visible in the pane.`);
|
|
66864
|
+
this.attempts = attempts;
|
|
66865
|
+
this.name = "TmuxPasteUnconfirmedError";
|
|
66866
|
+
}
|
|
66867
|
+
}
|
|
66868
|
+
|
|
66869
|
+
class TmuxSubmitUnconfirmedError extends Error {
|
|
66870
|
+
constructor() {
|
|
66871
|
+
super("tmux submit was not confirmed after Enter. The pane state did not change, so clisbot did not treat the prompt as truthfully submitted.");
|
|
66872
|
+
this.name = "TmuxSubmitUnconfirmedError";
|
|
66873
|
+
}
|
|
66874
|
+
}
|
|
66763
66875
|
async function submitTmuxSessionInput(params) {
|
|
66764
66876
|
const prePasteState = await params.tmux.getPaneState(params.sessionName);
|
|
66765
66877
|
const captureLines = estimatePasteCaptureLines(params.text);
|
|
66766
66878
|
const prePasteSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
|
|
66767
|
-
await
|
|
66768
|
-
const pasteSettlement = await waitForPanePasteSettlement({
|
|
66879
|
+
const pasteDelivery = await deliverTmuxPasteWithConfirmation({
|
|
66769
66880
|
tmux: params.tmux,
|
|
66770
66881
|
sessionName: params.sessionName,
|
|
66771
|
-
baseline: prePasteState,
|
|
66772
66882
|
text: params.text,
|
|
66773
|
-
|
|
66883
|
+
baselineState: prePasteState,
|
|
66884
|
+
baselineSnapshot: prePasteSnapshot,
|
|
66885
|
+
captureLines,
|
|
66886
|
+
promptSubmitDelayMs: params.promptSubmitDelayMs,
|
|
66887
|
+
timingContext: params.timingContext
|
|
66774
66888
|
});
|
|
66775
|
-
|
|
66776
|
-
|
|
66777
|
-
logLatencyDebug("tmux-paste-retry", params.timingContext, {
|
|
66778
|
-
sessionName: params.sessionName
|
|
66779
|
-
});
|
|
66780
|
-
const snapshotConfirmed = await waitForPanePasteSnapshotConfirmation({
|
|
66781
|
-
tmux: params.tmux,
|
|
66889
|
+
if (!pasteDelivery.confirmed) {
|
|
66890
|
+
logLatencyDebug("tmux-paste-unconfirmed", params.timingContext, {
|
|
66782
66891
|
sessionName: params.sessionName,
|
|
66783
|
-
|
|
66784
|
-
captureLines
|
|
66892
|
+
attempts: pasteDelivery.attempts
|
|
66785
66893
|
});
|
|
66786
|
-
|
|
66787
|
-
logLatencyDebug("tmux-paste-unconfirmed", params.timingContext, {
|
|
66788
|
-
sessionName: params.sessionName
|
|
66789
|
-
});
|
|
66790
|
-
preSubmitState = prePasteState;
|
|
66791
|
-
} else {
|
|
66792
|
-
preSubmitState = await params.tmux.getPaneState(params.sessionName);
|
|
66793
|
-
}
|
|
66894
|
+
throw new TmuxPasteUnconfirmedError(pasteDelivery.attempts);
|
|
66794
66895
|
}
|
|
66896
|
+
const preSubmitState = pasteDelivery.state;
|
|
66897
|
+
const preSubmitSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
|
|
66795
66898
|
await params.tmux.sendKey(params.sessionName, "Enter");
|
|
66796
66899
|
if (await waitForPaneSubmitConfirmation({
|
|
66797
66900
|
tmux: params.tmux,
|
|
66798
66901
|
sessionName: params.sessionName,
|
|
66799
66902
|
baseline: preSubmitState,
|
|
66800
|
-
baselineSnapshot:
|
|
66903
|
+
baselineSnapshot: preSubmitSnapshot,
|
|
66801
66904
|
captureLines
|
|
66802
66905
|
})) {
|
|
66803
66906
|
return;
|
|
@@ -66810,18 +66913,15 @@ async function submitTmuxSessionInput(params) {
|
|
|
66810
66913
|
tmux: params.tmux,
|
|
66811
66914
|
sessionName: params.sessionName,
|
|
66812
66915
|
baseline: preSubmitState,
|
|
66813
|
-
baselineSnapshot:
|
|
66916
|
+
baselineSnapshot: preSubmitSnapshot,
|
|
66814
66917
|
captureLines
|
|
66815
66918
|
})) {
|
|
66816
66919
|
return;
|
|
66817
66920
|
}
|
|
66818
|
-
if (!pasteSettlement.visible) {
|
|
66819
|
-
throw new Error("tmux paste was not confirmed before Enter, and submission still could not be confirmed after Enter. clisbot did not treat the prompt as truthfully delivered.");
|
|
66820
|
-
}
|
|
66821
66921
|
logLatencyDebug("tmux-submit-unconfirmed", params.timingContext, {
|
|
66822
66922
|
sessionName: params.sessionName
|
|
66823
66923
|
});
|
|
66824
|
-
throw new
|
|
66924
|
+
throw new TmuxSubmitUnconfirmedError;
|
|
66825
66925
|
}
|
|
66826
66926
|
async function captureTmuxSessionIdentity(params) {
|
|
66827
66927
|
await submitTmuxSessionInput({
|
|
@@ -66864,6 +66964,14 @@ async function captureTmuxSessionIdentity(params) {
|
|
|
66864
66964
|
}
|
|
66865
66965
|
const sessionId = extractSessionId(snapshot, params.pattern);
|
|
66866
66966
|
if (sessionId) {
|
|
66967
|
+
await waitForTmuxPaneSettle({
|
|
66968
|
+
tmux: params.tmux,
|
|
66969
|
+
sessionName: params.sessionName,
|
|
66970
|
+
captureLines: params.captureLines,
|
|
66971
|
+
pollIntervalMs: POST_STATUS_SETTLE_POLL_INTERVAL_MS,
|
|
66972
|
+
quietWindowMs: POST_STATUS_SETTLE_QUIET_WINDOW_MS,
|
|
66973
|
+
maxWaitMs: POST_STATUS_SETTLE_MAX_WAIT_MS
|
|
66974
|
+
});
|
|
66867
66975
|
return sessionId;
|
|
66868
66976
|
}
|
|
66869
66977
|
}
|
|
@@ -67018,6 +67126,49 @@ async function waitForPaneSubmitSnapshotConfirmation(params) {
|
|
|
67018
67126
|
await sleep(Math.min(SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
|
|
67019
67127
|
}
|
|
67020
67128
|
}
|
|
67129
|
+
async function deliverTmuxPasteWithConfirmation(params) {
|
|
67130
|
+
for (let attempt = 1;attempt <= PASTE_CONFIRM_MAX_ATTEMPTS; attempt += 1) {
|
|
67131
|
+
if (attempt > 1) {
|
|
67132
|
+
logLatencyDebug("tmux-paste-retry", params.timingContext, {
|
|
67133
|
+
sessionName: params.sessionName,
|
|
67134
|
+
attempt
|
|
67135
|
+
});
|
|
67136
|
+
}
|
|
67137
|
+
await params.tmux.sendLiteral(params.sessionName, params.text);
|
|
67138
|
+
const pasteSettlement = await waitForPanePasteSettlement({
|
|
67139
|
+
tmux: params.tmux,
|
|
67140
|
+
sessionName: params.sessionName,
|
|
67141
|
+
baseline: params.baselineState,
|
|
67142
|
+
text: params.text,
|
|
67143
|
+
minDelayMs: params.promptSubmitDelayMs
|
|
67144
|
+
});
|
|
67145
|
+
if (pasteSettlement.visible) {
|
|
67146
|
+
return {
|
|
67147
|
+
confirmed: true,
|
|
67148
|
+
state: pasteSettlement.state,
|
|
67149
|
+
attempts: attempt
|
|
67150
|
+
};
|
|
67151
|
+
}
|
|
67152
|
+
const snapshotConfirmed = await waitForPanePasteSnapshotConfirmation({
|
|
67153
|
+
tmux: params.tmux,
|
|
67154
|
+
sessionName: params.sessionName,
|
|
67155
|
+
baselineSnapshot: params.baselineSnapshot,
|
|
67156
|
+
captureLines: params.captureLines
|
|
67157
|
+
});
|
|
67158
|
+
if (snapshotConfirmed) {
|
|
67159
|
+
return {
|
|
67160
|
+
confirmed: true,
|
|
67161
|
+
state: await params.tmux.getPaneState(params.sessionName),
|
|
67162
|
+
attempts: attempt
|
|
67163
|
+
};
|
|
67164
|
+
}
|
|
67165
|
+
}
|
|
67166
|
+
return {
|
|
67167
|
+
confirmed: false,
|
|
67168
|
+
state: params.baselineState,
|
|
67169
|
+
attempts: PASTE_CONFIRM_MAX_ATTEMPTS
|
|
67170
|
+
};
|
|
67171
|
+
}
|
|
67021
67172
|
async function waitForPanePasteSettlement(params) {
|
|
67022
67173
|
await sleep(params.minDelayMs);
|
|
67023
67174
|
let currentState = await params.tmux.getPaneState(params.sessionName);
|
|
@@ -67063,6 +67214,41 @@ async function waitForPanePasteSnapshotConfirmation(params) {
|
|
|
67063
67214
|
await sleep(Math.min(PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS, remainingMs));
|
|
67064
67215
|
}
|
|
67065
67216
|
}
|
|
67217
|
+
async function waitForTmuxPaneSettle(params) {
|
|
67218
|
+
let previousSnapshot = "";
|
|
67219
|
+
let previousState = null;
|
|
67220
|
+
let lastChangeAt = Date.now();
|
|
67221
|
+
const deadline = Date.now() + params.maxWaitMs;
|
|
67222
|
+
while (true) {
|
|
67223
|
+
let snapshot = "";
|
|
67224
|
+
let state;
|
|
67225
|
+
try {
|
|
67226
|
+
snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
|
|
67227
|
+
state = await params.tmux.getPaneState(params.sessionName);
|
|
67228
|
+
} catch (error) {
|
|
67229
|
+
if (isRetryableBootstrapTargetError(error)) {
|
|
67230
|
+
if (Date.now() >= deadline) {
|
|
67231
|
+
return;
|
|
67232
|
+
}
|
|
67233
|
+
await sleep(params.pollIntervalMs);
|
|
67234
|
+
continue;
|
|
67235
|
+
}
|
|
67236
|
+
if (isBootstrapSessionGoneError(error)) {
|
|
67237
|
+
throw buildBootstrapSessionLostError(params.sessionName, error);
|
|
67238
|
+
}
|
|
67239
|
+
throw error;
|
|
67240
|
+
}
|
|
67241
|
+
if (snapshot !== previousSnapshot || !previousState || !arePaneStatesEqual(previousState, state)) {
|
|
67242
|
+
previousSnapshot = snapshot;
|
|
67243
|
+
previousState = state;
|
|
67244
|
+
lastChangeAt = Date.now();
|
|
67245
|
+
}
|
|
67246
|
+
if (Date.now() - lastChangeAt >= params.quietWindowMs || Date.now() >= deadline) {
|
|
67247
|
+
return;
|
|
67248
|
+
}
|
|
67249
|
+
await sleep(params.pollIntervalMs);
|
|
67250
|
+
}
|
|
67251
|
+
}
|
|
67066
67252
|
function estimatePasteCaptureLines(text) {
|
|
67067
67253
|
return Math.max(40, Math.min(160, text.split(`
|
|
67068
67254
|
`).length + 24));
|
|
@@ -67284,6 +67470,9 @@ function isBootstrapSessionLostError(error) {
|
|
|
67284
67470
|
function isRecoverableStartupSessionLoss(error) {
|
|
67285
67471
|
return isMissingTmuxSessionError(error) || isTmuxServerUnavailableError(error) || isBootstrapSessionLostError(error);
|
|
67286
67472
|
}
|
|
67473
|
+
function isFreshStartRetryablePromptDeliveryError(error) {
|
|
67474
|
+
return error instanceof TmuxPasteUnconfirmedError || error instanceof TmuxSubmitUnconfirmedError;
|
|
67475
|
+
}
|
|
67287
67476
|
|
|
67288
67477
|
class RunnerService {
|
|
67289
67478
|
loadedConfig;
|
|
@@ -67355,7 +67544,15 @@ class RunnerService {
|
|
|
67355
67544
|
runnerCommand: resolved.runner.command
|
|
67356
67545
|
});
|
|
67357
67546
|
}
|
|
67358
|
-
|
|
67547
|
+
let sessionId;
|
|
67548
|
+
try {
|
|
67549
|
+
sessionId = await this.captureSessionIdentity(resolved);
|
|
67550
|
+
} catch (error) {
|
|
67551
|
+
if (isFreshStartRetryablePromptDeliveryError(error)) {
|
|
67552
|
+
this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
|
|
67553
|
+
}
|
|
67554
|
+
throw error;
|
|
67555
|
+
}
|
|
67359
67556
|
if (sessionId) {
|
|
67360
67557
|
this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
|
|
67361
67558
|
} else {
|
|
@@ -67398,7 +67595,7 @@ class RunnerService {
|
|
|
67398
67595
|
});
|
|
67399
67596
|
}
|
|
67400
67597
|
async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
|
|
67401
|
-
if (!isRecoverableStartupSessionLoss(error)) {
|
|
67598
|
+
if (!isRecoverableStartupSessionLoss(error) && !isFreshStartRetryablePromptDeliveryError(error)) {
|
|
67402
67599
|
return null;
|
|
67403
67600
|
}
|
|
67404
67601
|
return this.retryFreshStartWithClearedSessionId(target, resolved, remainingFreshRetries);
|
|
@@ -67528,7 +67725,6 @@ class RunnerService {
|
|
|
67528
67725
|
sessionName: resolved.sessionName,
|
|
67529
67726
|
stateDir: this.loadedConfig.stateDir
|
|
67530
67727
|
});
|
|
67531
|
-
this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
|
|
67532
67728
|
try {
|
|
67533
67729
|
await this.tmux.newSession({
|
|
67534
67730
|
sessionName: resolved.sessionName,
|
|
@@ -67640,6 +67836,9 @@ class RunnerService {
|
|
|
67640
67836
|
canRecoverMidRun(error) {
|
|
67641
67837
|
return isRecoverableStartupSessionLoss(error);
|
|
67642
67838
|
}
|
|
67839
|
+
canRetryPromptAfterFreshStart(error) {
|
|
67840
|
+
return isFreshStartRetryablePromptDeliveryError(error);
|
|
67841
|
+
}
|
|
67643
67842
|
async reopenRunContext(target, timingContext) {
|
|
67644
67843
|
const resolved = this.resolveTarget(target);
|
|
67645
67844
|
const existing = await this.sessionState.getEntry(resolved.sessionKey);
|
|
@@ -67654,7 +67853,10 @@ class RunnerService {
|
|
|
67654
67853
|
return;
|
|
67655
67854
|
});
|
|
67656
67855
|
await this.sessionState.clearSessionIdEntry(resolved, { runnerCommand: resolved.runner.command });
|
|
67657
|
-
return this.
|
|
67856
|
+
return this.ensureRunnerReady(target, {
|
|
67857
|
+
allowFreshRetryBeforePrompt: false,
|
|
67858
|
+
timingContext
|
|
67859
|
+
});
|
|
67658
67860
|
}
|
|
67659
67861
|
async captureTranscript(target) {
|
|
67660
67862
|
const resolved = this.resolveTarget(target);
|
|
@@ -68295,6 +68497,55 @@ class SessionService {
|
|
|
68295
68497
|
}
|
|
68296
68498
|
this.activeRuns.delete(run.resolved.sessionKey);
|
|
68297
68499
|
}
|
|
68500
|
+
async recoverPromptDeliveryFailure(sessionKey, params, error) {
|
|
68501
|
+
if (!params.prompt || params.promptRetryAttempt || !this.runnerSessions.canRetryPromptAfterFreshStart(error)) {
|
|
68502
|
+
return false;
|
|
68503
|
+
}
|
|
68504
|
+
const run = this.getRun(sessionKey, params.runId);
|
|
68505
|
+
if (!run) {
|
|
68506
|
+
return true;
|
|
68507
|
+
}
|
|
68508
|
+
const target = {
|
|
68509
|
+
agentId: run.resolved.agentId,
|
|
68510
|
+
sessionKey: run.resolved.sessionKey
|
|
68511
|
+
};
|
|
68512
|
+
await this.notifyRecoveryStep(run, "Prompt delivery did not settle truthfully in the current runner session. clisbot is opening one fresh runner session and retrying the prompt once.");
|
|
68513
|
+
try {
|
|
68514
|
+
const fresh = await this.runnerSessions.startFreshSession(target, params.timingContext);
|
|
68515
|
+
const currentRun = this.getRun(sessionKey, params.runId);
|
|
68516
|
+
if (!currentRun) {
|
|
68517
|
+
return true;
|
|
68518
|
+
}
|
|
68519
|
+
const restartedAt = Date.now();
|
|
68520
|
+
currentRun.resolved = fresh.resolved;
|
|
68521
|
+
currentRun.steeringReady = false;
|
|
68522
|
+
currentRun.startedAt = restartedAt;
|
|
68523
|
+
currentRun.latestUpdate = this.createRunUpdate({
|
|
68524
|
+
resolved: currentRun.resolved,
|
|
68525
|
+
status: currentRun.latestUpdate.status === "detached" ? "detached" : "running",
|
|
68526
|
+
snapshot: "",
|
|
68527
|
+
fullSnapshot: fresh.initialSnapshot,
|
|
68528
|
+
initialSnapshot: fresh.initialSnapshot,
|
|
68529
|
+
note: "Retrying the prompt in one fresh runner session.",
|
|
68530
|
+
forceVisible: true
|
|
68531
|
+
});
|
|
68532
|
+
await this.sessionState.setSessionRuntime(currentRun.resolved, {
|
|
68533
|
+
state: "running",
|
|
68534
|
+
startedAt: restartedAt
|
|
68535
|
+
});
|
|
68536
|
+
await this.notifyRunObservers(currentRun, currentRun.latestUpdate);
|
|
68537
|
+
this.startRunMonitor(sessionKey, {
|
|
68538
|
+
...params,
|
|
68539
|
+
promptRetryAttempt: 1,
|
|
68540
|
+
initialSnapshot: fresh.initialSnapshot,
|
|
68541
|
+
startedAt: restartedAt
|
|
68542
|
+
});
|
|
68543
|
+
return true;
|
|
68544
|
+
} catch (freshError) {
|
|
68545
|
+
await this.failActiveRun(sessionKey, run.runId, await this.runnerSessions.mapRunError(freshError, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
|
|
68546
|
+
return true;
|
|
68547
|
+
}
|
|
68548
|
+
}
|
|
68298
68549
|
async recoverLostMidRun(sessionKey, params, error) {
|
|
68299
68550
|
if (!this.runnerSessions.canRecoverMidRun(error)) {
|
|
68300
68551
|
return false;
|
|
@@ -68450,6 +68701,16 @@ class SessionService {
|
|
|
68450
68701
|
}
|
|
68451
68702
|
});
|
|
68452
68703
|
} catch (error) {
|
|
68704
|
+
if (await this.recoverPromptDeliveryFailure(sessionKey, {
|
|
68705
|
+
runId: params.runId,
|
|
68706
|
+
prompt: params.prompt,
|
|
68707
|
+
startedAt: params.startedAt,
|
|
68708
|
+
detachedAlready: params.detachedAlready,
|
|
68709
|
+
timingContext: params.timingContext,
|
|
68710
|
+
promptRetryAttempt: params.promptRetryAttempt
|
|
68711
|
+
}, error)) {
|
|
68712
|
+
return;
|
|
68713
|
+
}
|
|
68453
68714
|
if (await this.recoverLostMidRun(sessionKey, {
|
|
68454
68715
|
runId: params.runId,
|
|
68455
68716
|
timingContext: params.timingContext,
|
|
@@ -69331,33 +69592,178 @@ async function setConversationStreaming(params) {
|
|
|
69331
69592
|
};
|
|
69332
69593
|
}
|
|
69333
69594
|
|
|
69334
|
-
// src/channels/
|
|
69335
|
-
|
|
69336
|
-
|
|
69337
|
-
var MESSAGE_TOOL_PREVIEW_SIGNAL_POLL_MS = 100;
|
|
69338
|
-
function renderSensitiveCommandDisabledMessage() {
|
|
69339
|
-
return [
|
|
69340
|
-
"Shell execution is not allowed for your current role on this agent.",
|
|
69341
|
-
"Ask an app or agent admin to grant `shellExecute` if this surface should allow `/bash`."
|
|
69342
|
-
].join(`
|
|
69343
|
-
`);
|
|
69344
|
-
}
|
|
69345
|
-
function renderTranscriptDisabledMessage() {
|
|
69346
|
-
return [
|
|
69347
|
-
"Transcript inspection is disabled for this route.",
|
|
69348
|
-
'Set `verbose: "minimal"` on the route or channel to allow `/transcript`.'
|
|
69349
|
-
].join(`
|
|
69350
|
-
`);
|
|
69595
|
+
// src/channels/follow-up-mode-config.ts
|
|
69596
|
+
function getEditableConfigPath6() {
|
|
69597
|
+
return process.env.CLISBOT_CONFIG_PATH;
|
|
69351
69598
|
}
|
|
69352
|
-
function
|
|
69353
|
-
|
|
69354
|
-
|
|
69355
|
-
|
|
69356
|
-
|
|
69357
|
-
|
|
69599
|
+
function getOrCreateFollowUp(source) {
|
|
69600
|
+
const existing = source.followUp;
|
|
69601
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
69602
|
+
return existing;
|
|
69603
|
+
}
|
|
69604
|
+
const created = {};
|
|
69605
|
+
source.followUp = created;
|
|
69606
|
+
return created;
|
|
69358
69607
|
}
|
|
69359
|
-
function
|
|
69360
|
-
|
|
69608
|
+
function createTelegramRouteOverride2() {
|
|
69609
|
+
return {
|
|
69610
|
+
enabled: true,
|
|
69611
|
+
allowUsers: [],
|
|
69612
|
+
blockUsers: [],
|
|
69613
|
+
topics: {}
|
|
69614
|
+
};
|
|
69615
|
+
}
|
|
69616
|
+
function getOrCreateTelegramGroupRoute2(bot, chatId) {
|
|
69617
|
+
const existingGroup = bot.groups[chatId];
|
|
69618
|
+
if (existingGroup) {
|
|
69619
|
+
return existingGroup;
|
|
69620
|
+
}
|
|
69621
|
+
const createdGroup = createTelegramRouteOverride2();
|
|
69622
|
+
bot.groups[chatId] = createdGroup;
|
|
69623
|
+
return createdGroup;
|
|
69624
|
+
}
|
|
69625
|
+
function resolveSlackFollowUpModeTarget(config, params) {
|
|
69626
|
+
const botId = resolveSlackBotId(config.bots.slack, params.botId);
|
|
69627
|
+
const bot = getSlackBotRecord(config.bots.slack, botId);
|
|
69628
|
+
if (!bot) {
|
|
69629
|
+
throw new Error(`Unknown Slack bot: ${botId}`);
|
|
69630
|
+
}
|
|
69631
|
+
if (params.scope === "all") {
|
|
69632
|
+
return {
|
|
69633
|
+
get: () => bot.followUp?.mode,
|
|
69634
|
+
set: (value) => {
|
|
69635
|
+
getOrCreateFollowUp(bot).mode = value;
|
|
69636
|
+
},
|
|
69637
|
+
label: `slack bot ${botId}`
|
|
69638
|
+
};
|
|
69639
|
+
}
|
|
69640
|
+
if (params.identity.conversationKind === "dm") {
|
|
69641
|
+
const targetId = params.identity.senderId?.trim() || params.identity.channelId?.trim();
|
|
69642
|
+
if (!targetId) {
|
|
69643
|
+
throw new Error("Slack follow-up channel scope requires a senderId or channelId.");
|
|
69644
|
+
}
|
|
69645
|
+
const routeKey2 = `dm:${targetId}`;
|
|
69646
|
+
const existingRoute = resolveDirectMessageExactRoute(bot.directMessages, targetId) ?? (bot.directMessages[routeKey2] = createDirectMessageBehaviorOverride());
|
|
69647
|
+
return {
|
|
69648
|
+
get: () => existingRoute.followUp?.mode ?? bot.followUp?.mode,
|
|
69649
|
+
set: (value) => {
|
|
69650
|
+
getOrCreateFollowUp(existingRoute).mode = value;
|
|
69651
|
+
},
|
|
69652
|
+
label: `slack ${routeKey2}`
|
|
69653
|
+
};
|
|
69654
|
+
}
|
|
69655
|
+
const routeKind = params.identity.conversationKind === "group" ? "group" : "channel";
|
|
69656
|
+
const channelId = params.identity.channelId?.trim();
|
|
69657
|
+
if (!channelId) {
|
|
69658
|
+
throw new Error("Slack follow-up channel scope requires a channelId.");
|
|
69659
|
+
}
|
|
69660
|
+
const routeKey = `${routeKind}:${channelId}`;
|
|
69661
|
+
const route = bot.groups[routeKey];
|
|
69662
|
+
if (!route) {
|
|
69663
|
+
throw new Error(`Route not configured yet: slack ${routeKey}. Add the route first.`);
|
|
69664
|
+
}
|
|
69665
|
+
return {
|
|
69666
|
+
get: () => route.followUp?.mode ?? bot.followUp?.mode,
|
|
69667
|
+
set: (value) => {
|
|
69668
|
+
getOrCreateFollowUp(route).mode = value;
|
|
69669
|
+
},
|
|
69670
|
+
label: `slack ${routeKey}`
|
|
69671
|
+
};
|
|
69672
|
+
}
|
|
69673
|
+
function resolveTelegramFollowUpModeTarget(config, params) {
|
|
69674
|
+
const botId = resolveTelegramBotId(config.bots.telegram, params.botId);
|
|
69675
|
+
const bot = getTelegramBotRecord(config.bots.telegram, botId);
|
|
69676
|
+
if (!bot) {
|
|
69677
|
+
throw new Error(`Unknown Telegram bot: ${botId}`);
|
|
69678
|
+
}
|
|
69679
|
+
if (params.scope === "all") {
|
|
69680
|
+
return {
|
|
69681
|
+
get: () => bot.followUp?.mode,
|
|
69682
|
+
set: (value) => {
|
|
69683
|
+
getOrCreateFollowUp(bot).mode = value;
|
|
69684
|
+
},
|
|
69685
|
+
label: `telegram bot ${botId}`
|
|
69686
|
+
};
|
|
69687
|
+
}
|
|
69688
|
+
if (params.identity.conversationKind === "dm") {
|
|
69689
|
+
const targetId = params.identity.senderId?.trim() || params.identity.chatId?.trim();
|
|
69690
|
+
if (!targetId) {
|
|
69691
|
+
throw new Error("Telegram follow-up channel scope requires a senderId or chatId.");
|
|
69692
|
+
}
|
|
69693
|
+
const routeKey = `dm:${targetId}`;
|
|
69694
|
+
const existingRoute = resolveDirectMessageExactRoute(bot.directMessages, targetId) ?? (bot.directMessages[routeKey] = createDirectMessageBehaviorOverride());
|
|
69695
|
+
return {
|
|
69696
|
+
get: () => existingRoute.followUp?.mode ?? bot.followUp?.mode,
|
|
69697
|
+
set: (value) => {
|
|
69698
|
+
getOrCreateFollowUp(existingRoute).mode = value;
|
|
69699
|
+
},
|
|
69700
|
+
label: `telegram ${routeKey}`
|
|
69701
|
+
};
|
|
69702
|
+
}
|
|
69703
|
+
const chatId = params.identity.chatId?.trim();
|
|
69704
|
+
if (!chatId) {
|
|
69705
|
+
throw new Error("Telegram follow-up channel scope requires a chatId.");
|
|
69706
|
+
}
|
|
69707
|
+
const group = getOrCreateTelegramGroupRoute2(bot, chatId);
|
|
69708
|
+
return {
|
|
69709
|
+
get: () => group.followUp?.mode ?? bot.followUp?.mode,
|
|
69710
|
+
set: (value) => {
|
|
69711
|
+
getOrCreateFollowUp(group).mode = value;
|
|
69712
|
+
},
|
|
69713
|
+
label: `telegram group:${chatId}`
|
|
69714
|
+
};
|
|
69715
|
+
}
|
|
69716
|
+
function resolveConfiguredFollowUpModeTarget(config, params) {
|
|
69717
|
+
if (params.channel === "slack") {
|
|
69718
|
+
return resolveSlackFollowUpModeTarget(config, params);
|
|
69719
|
+
}
|
|
69720
|
+
return resolveTelegramFollowUpModeTarget(config, params);
|
|
69721
|
+
}
|
|
69722
|
+
async function setScopedConversationFollowUpMode(params) {
|
|
69723
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
|
|
69724
|
+
const target = resolveConfiguredFollowUpModeTarget(config, {
|
|
69725
|
+
channel: params.identity.platform,
|
|
69726
|
+
botId: resolveChannelIdentityBotId(params.identity),
|
|
69727
|
+
scope: params.scope,
|
|
69728
|
+
identity: params.identity
|
|
69729
|
+
});
|
|
69730
|
+
target.set(params.mode);
|
|
69731
|
+
await writeEditableConfig(configPath, config);
|
|
69732
|
+
return {
|
|
69733
|
+
configPath,
|
|
69734
|
+
label: target.label,
|
|
69735
|
+
followUpMode: params.mode
|
|
69736
|
+
};
|
|
69737
|
+
}
|
|
69738
|
+
|
|
69739
|
+
// src/channels/interaction-processing.ts
|
|
69740
|
+
var MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS = 3000;
|
|
69741
|
+
var MESSAGE_TOOL_FINAL_GRACE_POLL_MS = 100;
|
|
69742
|
+
var MESSAGE_TOOL_PREVIEW_SIGNAL_POLL_MS = 100;
|
|
69743
|
+
var TRANSCRIPT_PREVIEW_MAX_CHARS = 1200;
|
|
69744
|
+
function renderSensitiveCommandDisabledMessage() {
|
|
69745
|
+
return [
|
|
69746
|
+
"Shell execution is not allowed for your current role on this agent.",
|
|
69747
|
+
"Ask an app or agent admin to grant `shellExecute` if this surface should allow `/bash`."
|
|
69748
|
+
].join(`
|
|
69749
|
+
`);
|
|
69750
|
+
}
|
|
69751
|
+
function renderTranscriptDisabledMessage() {
|
|
69752
|
+
return [
|
|
69753
|
+
"Transcript inspection is disabled for this route.",
|
|
69754
|
+
'Set `verbose: "minimal"` on the route or channel to allow `/transcript`.'
|
|
69755
|
+
].join(`
|
|
69756
|
+
`);
|
|
69757
|
+
}
|
|
69758
|
+
function renderStartupSteeringUnavailableMessage() {
|
|
69759
|
+
return [
|
|
69760
|
+
"The active run is still starting and cannot accept steering input yet.",
|
|
69761
|
+
"Send a normal follow-up message to keep it ordered behind the first prompt, or wait until startup finishes before using `/steer`."
|
|
69762
|
+
].join(`
|
|
69763
|
+
`);
|
|
69764
|
+
}
|
|
69765
|
+
function renderPrincipalFormat(identity) {
|
|
69766
|
+
if (identity.platform === "slack") {
|
|
69361
69767
|
return "slack:<nativeUserId>";
|
|
69362
69768
|
}
|
|
69363
69769
|
return "telegram:<nativeUserId>";
|
|
@@ -69439,7 +69845,7 @@ function renderRouteStatusMessage(params) {
|
|
|
69439
69845
|
lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
|
|
69440
69846
|
}
|
|
69441
69847
|
}
|
|
69442
|
-
lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every <duration>`", "- `/followup status`", "- `/streaming status|on|off|latest|all`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop help`, `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue help`, `/queue <message>`, `/steer <message>`", "- `/queue list`, `/queue clear`", params.route.verbose === "off" ? "- `/transcript` disabled on this route (`verbose: off`)" : "- `/transcript` enabled on this route (`verbose: minimal`)", "- `/bash` requires `shellExecute`");
|
|
69848
|
+
lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every <duration>`", "- `/followup status`, `/mention`, `/mention channel`, `/mention all`", "- `/streaming status|on|off|latest|all`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop help`, `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue help`, `/queue <message>`, `/steer <message>`", "- `/queue list`, `/queue clear`", params.route.verbose === "off" ? "- `/transcript` disabled on this route (`verbose: off`)" : "- `/transcript` enabled on this route (`verbose: minimal`)", "- `/bash` requires `shellExecute`");
|
|
69443
69849
|
return lines.join(`
|
|
69444
69850
|
`);
|
|
69445
69851
|
}
|
|
@@ -69486,6 +69892,29 @@ function renderAdditionalMessageModeStatusMessage(params) {
|
|
|
69486
69892
|
return lines.join(`
|
|
69487
69893
|
`);
|
|
69488
69894
|
}
|
|
69895
|
+
function renderFollowUpModeUpdateMessage(params) {
|
|
69896
|
+
if (!params.persisted) {
|
|
69897
|
+
if (params.mode === "paused") {
|
|
69898
|
+
return "Follow-up paused for this conversation until the next explicit mention.";
|
|
69899
|
+
}
|
|
69900
|
+
return `Follow-up mode set to \`${params.mode}\` for this conversation.`;
|
|
69901
|
+
}
|
|
69902
|
+
const lines = [
|
|
69903
|
+
`Updated follow-up mode for \`${params.persisted.label}\`.`,
|
|
69904
|
+
`config.followUp.mode: \`${params.persisted.followUpMode}\``,
|
|
69905
|
+
`config: \`${params.persisted.configPath}\``,
|
|
69906
|
+
`currentConversation.overrideMode: \`${params.mode}\``,
|
|
69907
|
+
"The current conversation changes immediately.",
|
|
69908
|
+
"If config reload is enabled, the broader default should apply automatically shortly."
|
|
69909
|
+
];
|
|
69910
|
+
if (params.scope === "all") {
|
|
69911
|
+
lines.splice(4, 0, "This persists the bot-wide default for later routed conversations on this bot.");
|
|
69912
|
+
} else if (params.scope === "channel") {
|
|
69913
|
+
lines.splice(4, 0, "This persists the default for the current channel, group, or DM container.");
|
|
69914
|
+
}
|
|
69915
|
+
return lines.join(`
|
|
69916
|
+
`);
|
|
69917
|
+
}
|
|
69489
69918
|
function buildChannelObserverId(identity) {
|
|
69490
69919
|
return [
|
|
69491
69920
|
identity.platform,
|
|
@@ -69769,9 +70198,6 @@ async function executePromptDelivery(params) {
|
|
|
69769
70198
|
observerId: params.observerId,
|
|
69770
70199
|
timingContext: params.timingContext,
|
|
69771
70200
|
onUpdate: async (update) => {
|
|
69772
|
-
if (!update.forceVisible && !paneManagedDelivery && !messageToolPreview) {
|
|
69773
|
-
return;
|
|
69774
|
-
}
|
|
69775
70201
|
if (update.status === "running" && !loggedFirstRunningUpdate) {
|
|
69776
70202
|
loggedFirstRunningUpdate = true;
|
|
69777
70203
|
logLatencyDebug("channel-first-running-update", params.timingContext, {
|
|
@@ -69787,6 +70213,9 @@ async function executePromptDelivery(params) {
|
|
|
69787
70213
|
if (update.status === "running") {
|
|
69788
70214
|
renderedQueueStart = await maybeRenderQueueStartNotification();
|
|
69789
70215
|
}
|
|
70216
|
+
if (!update.forceVisible && !paneManagedDelivery && !messageToolPreview) {
|
|
70217
|
+
return;
|
|
70218
|
+
}
|
|
69790
70219
|
if (params.route.streaming === "off" && update.status === "running" && !update.forceVisible) {
|
|
69791
70220
|
return;
|
|
69792
70221
|
}
|
|
@@ -70132,14 +70561,18 @@ async function processChannelInteraction(params) {
|
|
|
70132
70561
|
return interactionResult;
|
|
70133
70562
|
}
|
|
70134
70563
|
const transcript = await params.agentService.captureTranscript(params.sessionTarget);
|
|
70135
|
-
await params.postText(renderChannelSnapshot({
|
|
70564
|
+
await params.postText(slashCommand.mode === "full" ? renderChannelSnapshot({
|
|
70136
70565
|
agentId: transcript.agentId,
|
|
70137
70566
|
sessionName: transcript.sessionName,
|
|
70138
70567
|
workspacePath: transcript.workspacePath,
|
|
70139
70568
|
status: "completed",
|
|
70140
70569
|
snapshot: transcript.snapshot || "(no tmux output yet)",
|
|
70141
70570
|
maxChars: params.maxChars,
|
|
70142
|
-
note: "transcript command"
|
|
70571
|
+
note: "transcript command (full)"
|
|
70572
|
+
}) : renderCompactChannelTranscript({
|
|
70573
|
+
snapshot: transcript.snapshot || "(no tmux output yet)",
|
|
70574
|
+
maxChars: Math.min(params.maxChars, TRANSCRIPT_PREVIEW_MAX_CHARS),
|
|
70575
|
+
fullCommand: "/transcript full"
|
|
70143
70576
|
}));
|
|
70144
70577
|
return interactionResult;
|
|
70145
70578
|
}
|
|
@@ -70202,8 +70635,25 @@ async function processChannelInteraction(params) {
|
|
|
70202
70635
|
await params.agentService.resetConversationFollowUpMode(params.sessionTarget);
|
|
70203
70636
|
await params.postText("Follow-up policy reset to route defaults for this conversation.");
|
|
70204
70637
|
} else if (slashCommand.mode) {
|
|
70205
|
-
|
|
70206
|
-
|
|
70638
|
+
if (slashCommand.scope === "channel" || slashCommand.scope === "all") {
|
|
70639
|
+
await params.agentService.setConversationFollowUpMode(params.sessionTarget, slashCommand.mode);
|
|
70640
|
+
const persisted = await setScopedConversationFollowUpMode({
|
|
70641
|
+
identity: params.identity,
|
|
70642
|
+
scope: slashCommand.scope,
|
|
70643
|
+
mode: slashCommand.mode
|
|
70644
|
+
});
|
|
70645
|
+
await params.postText(renderFollowUpModeUpdateMessage({
|
|
70646
|
+
scope: slashCommand.scope,
|
|
70647
|
+
mode: slashCommand.mode,
|
|
70648
|
+
persisted
|
|
70649
|
+
}));
|
|
70650
|
+
} else {
|
|
70651
|
+
await params.agentService.setConversationFollowUpMode(params.sessionTarget, slashCommand.mode);
|
|
70652
|
+
await params.postText(renderFollowUpModeUpdateMessage({
|
|
70653
|
+
scope: "conversation",
|
|
70654
|
+
mode: slashCommand.mode
|
|
70655
|
+
}));
|
|
70656
|
+
}
|
|
70207
70657
|
}
|
|
70208
70658
|
await params.agentService.recordConversationReply(params.sessionTarget);
|
|
70209
70659
|
return interactionResult;
|
|
@@ -71518,7 +71968,7 @@ function renderSlackRouteChoiceMessage(params) {
|
|
|
71518
71968
|
`- ${renderCliCommand(`routes add --channel slack channel:${params.channelId} --bot default`, { inline: true })}`,
|
|
71519
71969
|
`- ${renderCliCommand(`routes set-agent --channel slack channel:${params.channelId} --bot default --agent <id>`, { inline: true })}`,
|
|
71520
71970
|
"",
|
|
71521
|
-
`After that, ${botReference} and send \`\\start
|
|
71971
|
+
`After that, ${botReference} and send \`\\start\`, \`\\status\`, or \`\\mention\` here.`
|
|
71522
71972
|
].join(`
|
|
71523
71973
|
`);
|
|
71524
71974
|
}
|
|
@@ -71526,7 +71976,7 @@ function renderSlackMentionRequiredMessage(botLabel) {
|
|
|
71526
71976
|
const botReference = botLabel?.trim() ? `mention this bot (${botLabel.trim()})` : "mention this bot";
|
|
71527
71977
|
return [
|
|
71528
71978
|
"clisbot: this Slack channel requires a bot mention for new commands.",
|
|
71529
|
-
`Try ${botReference} and send \`\\
|
|
71979
|
+
`Try ${botReference} and send \`\\status\` or \`\\mention\` here.`,
|
|
71530
71980
|
"After the bot replies in a thread, normal follow-up messages there can continue according to the follow-up policy."
|
|
71531
71981
|
].join(`
|
|
71532
71982
|
`);
|
|
@@ -71705,9 +72155,6 @@ async function resolveSlackFiles(params) {
|
|
|
71705
72155
|
if (currentFiles.length > 0) {
|
|
71706
72156
|
return currentFiles;
|
|
71707
72157
|
}
|
|
71708
|
-
if (params.threadTs && params.messageTs && params.threadTs !== params.messageTs) {
|
|
71709
|
-
return fetchSlackMessageFiles(params.client, params.channelId, params.threadTs);
|
|
71710
|
-
}
|
|
71711
72158
|
return [];
|
|
71712
72159
|
}
|
|
71713
72160
|
async function fetchSlackMessageFiles(client, channelId, messageTs) {
|
|
@@ -74302,7 +74749,7 @@ function renderTelegramRouteChoiceMessage(params) {
|
|
|
74302
74749
|
if (params.includeConfigPath) {
|
|
74303
74750
|
lines.push("", topicId != null ? `Config path: \`bots.telegram.default.groups."${chatId}".topics."${topicId}"\`` : `Config path: \`bots.telegram.default.groups."${chatId}"\``);
|
|
74304
74751
|
} else {
|
|
74305
|
-
lines.push("", "After that, routed commands such as `/status`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
|
|
74752
|
+
lines.push("", "After that, routed commands such as `/status`, `/mention`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
|
|
74306
74753
|
}
|
|
74307
74754
|
return lines.join(`
|
|
74308
74755
|
`);
|
|
@@ -74368,6 +74815,7 @@ var TELEGRAM_FULL_COMMANDS = [
|
|
|
74368
74815
|
{ command: "stop", description: "Interrupt current run" },
|
|
74369
74816
|
{ command: "nudge", description: "Send one extra Enter to the session" },
|
|
74370
74817
|
{ command: "followup", description: "Show or change follow-up mode" },
|
|
74818
|
+
{ command: "mention", description: "Require explicit mention for later turns" },
|
|
74371
74819
|
{ command: "pause", description: "Pause passive follow-up for this conversation" },
|
|
74372
74820
|
{ command: "resume", description: "Restore route follow-up defaults for this conversation" },
|
|
74373
74821
|
{ command: "streaming", description: "Show or change streaming mode" },
|
|
@@ -74379,6 +74827,15 @@ var TELEGRAM_FULL_COMMANDS = [
|
|
|
74379
74827
|
{ command: "bash", description: "Run bash in the agent workspace" }
|
|
74380
74828
|
];
|
|
74381
74829
|
var TELEGRAM_STARTUP_CONFLICT_MAX_WAIT_MS = 6000;
|
|
74830
|
+
var TELEGRAM_POLLING_CONFLICT_BACKOFF_MAX_DELAY_MS = 30000;
|
|
74831
|
+
var TELEGRAM_POLLING_CONFLICT_SLEEP_SLICE_MS = 250;
|
|
74832
|
+
var TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_DELAY_MS = 60000;
|
|
74833
|
+
var TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_REPEAT_MS = 15 * 60000;
|
|
74834
|
+
function computeTelegramPollingConflictBackoffDelayMs(baseDelayMs, attempt) {
|
|
74835
|
+
const safeBaseDelayMs = Math.max(1, baseDelayMs);
|
|
74836
|
+
const boundedAttempt = Math.max(1, attempt);
|
|
74837
|
+
return Math.min(safeBaseDelayMs * 2 ** (boundedAttempt - 1), TELEGRAM_POLLING_CONFLICT_BACKOFF_MAX_DELAY_MS);
|
|
74838
|
+
}
|
|
74382
74839
|
function renderTelegramUnroutedRouteMessage(params) {
|
|
74383
74840
|
const lines = params.mode === "whoami" ? [
|
|
74384
74841
|
"Who am I",
|
|
@@ -74460,6 +74917,8 @@ class TelegramPollingService {
|
|
|
74460
74917
|
nextUpdateId;
|
|
74461
74918
|
loopPromise;
|
|
74462
74919
|
activePollController;
|
|
74920
|
+
pollingConflictActive = false;
|
|
74921
|
+
pollingConflictAttempt = 0;
|
|
74463
74922
|
inFlightUpdates = new Set;
|
|
74464
74923
|
processingIndicators = new ConversationProcessingIndicatorCoordinator;
|
|
74465
74924
|
constructor(loadedConfig, agentService, processedEventsStore, activityStore, botId = "default", botCredentials, reportLifecycle) {
|
|
@@ -74509,7 +74968,13 @@ class TelegramPollingService {
|
|
|
74509
74968
|
this.botUserId = me.id;
|
|
74510
74969
|
this.botUsername = me.username ?? "";
|
|
74511
74970
|
console.log(`telegram bot @${this.botUsername || this.botUserId} (${this.botId})`);
|
|
74512
|
-
|
|
74971
|
+
try {
|
|
74972
|
+
await this.initializeOffset();
|
|
74973
|
+
} catch (error) {
|
|
74974
|
+
if (!isTelegramPollingConflict(error)) {
|
|
74975
|
+
throw error;
|
|
74976
|
+
}
|
|
74977
|
+
}
|
|
74513
74978
|
await this.registerCommands();
|
|
74514
74979
|
this.running = true;
|
|
74515
74980
|
this.loopPromise = this.pollLoop();
|
|
@@ -74549,6 +75014,7 @@ class TelegramPollingService {
|
|
|
74549
75014
|
timeoutMs: (telegramConfig.polling.timeoutSeconds + 5) * 1000
|
|
74550
75015
|
});
|
|
74551
75016
|
this.activePollController = undefined;
|
|
75017
|
+
await this.recoverFromPollingConflictIfNeeded();
|
|
74552
75018
|
const dispatched = dispatchTelegramUpdates({
|
|
74553
75019
|
updates,
|
|
74554
75020
|
handleUpdate: (update) => this.handleUpdate(update),
|
|
@@ -74568,24 +75034,54 @@ class TelegramPollingService {
|
|
|
74568
75034
|
return;
|
|
74569
75035
|
}
|
|
74570
75036
|
if (isTelegramPollingConflict(error)) {
|
|
74571
|
-
this.
|
|
74572
|
-
|
|
74573
|
-
connection: "failed",
|
|
74574
|
-
summary: "Telegram polling stopped because another instance is already using this bot token.",
|
|
74575
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
74576
|
-
actions: [
|
|
74577
|
-
"stop the other Telegram poller that is using the same bot token",
|
|
74578
|
-
`run ${renderCliCommand("start", { inline: true })} again after the token is no longer in use elsewhere`
|
|
74579
|
-
]
|
|
74580
|
-
});
|
|
74581
|
-
console.error("telegram polling stopped: another bot instance is already calling getUpdates for this token");
|
|
74582
|
-
return;
|
|
75037
|
+
await this.handlePollingConflict(error, telegramConfig.polling.retryDelayMs);
|
|
75038
|
+
continue;
|
|
74583
75039
|
}
|
|
74584
75040
|
console.error("telegram polling error", error);
|
|
74585
75041
|
await sleep(telegramConfig.polling.retryDelayMs);
|
|
74586
75042
|
}
|
|
74587
75043
|
}
|
|
74588
75044
|
}
|
|
75045
|
+
async handlePollingConflict(error, retryDelayMs) {
|
|
75046
|
+
this.pollingConflictAttempt += 1;
|
|
75047
|
+
const nextDelayMs = computeTelegramPollingConflictBackoffDelayMs(retryDelayMs, this.pollingConflictAttempt);
|
|
75048
|
+
if (!this.pollingConflictActive) {
|
|
75049
|
+
this.pollingConflictActive = true;
|
|
75050
|
+
await this.reportLifecycle?.({
|
|
75051
|
+
connection: "failed",
|
|
75052
|
+
summary: "Telegram polling is temporarily blocked because another poller is already using this bot token.",
|
|
75053
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
75054
|
+
actions: [
|
|
75055
|
+
"stop the other Telegram poller that is using the same bot token if it is unintended",
|
|
75056
|
+
"clisbot will keep retrying automatically with backoff until Telegram polling can recover"
|
|
75057
|
+
],
|
|
75058
|
+
ownerAlertAfterMs: TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_DELAY_MS,
|
|
75059
|
+
ownerAlertRepeatMs: TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_REPEAT_MS
|
|
75060
|
+
});
|
|
75061
|
+
console.error("telegram polling blocked: another bot instance is already calling getUpdates for this token; retrying with backoff");
|
|
75062
|
+
}
|
|
75063
|
+
await this.waitForPollingConflictRetryDelay(nextDelayMs);
|
|
75064
|
+
}
|
|
75065
|
+
async recoverFromPollingConflictIfNeeded() {
|
|
75066
|
+
if (!this.pollingConflictActive) {
|
|
75067
|
+
return;
|
|
75068
|
+
}
|
|
75069
|
+
this.pollingConflictActive = false;
|
|
75070
|
+
this.pollingConflictAttempt = 0;
|
|
75071
|
+
await this.reportLifecycle?.({
|
|
75072
|
+
connection: "active",
|
|
75073
|
+
detail: "Telegram polling recovered after a polling-conflict retry."
|
|
75074
|
+
});
|
|
75075
|
+
console.log("telegram polling recovered after polling conflict");
|
|
75076
|
+
}
|
|
75077
|
+
async waitForPollingConflictRetryDelay(delayMs) {
|
|
75078
|
+
let remainingMs = Math.max(0, delayMs);
|
|
75079
|
+
while (this.running && remainingMs > 0) {
|
|
75080
|
+
const sliceMs = Math.min(remainingMs, TELEGRAM_POLLING_CONFLICT_SLEEP_SLICE_MS);
|
|
75081
|
+
await sleep(sliceMs);
|
|
75082
|
+
remainingMs -= sliceMs;
|
|
75083
|
+
}
|
|
75084
|
+
}
|
|
74589
75085
|
trackInFlightUpdate(task) {
|
|
74590
75086
|
this.inFlightUpdates.add(task);
|
|
74591
75087
|
task.finally(() => {
|
|
@@ -75449,97 +75945,7 @@ function installRuntimeConsoleTimestamps() {
|
|
|
75449
75945
|
};
|
|
75450
75946
|
}
|
|
75451
75947
|
|
|
75452
|
-
// src/control/
|
|
75453
|
-
var defaultRuntimeMonitorDependencies = {
|
|
75454
|
-
loadConfig,
|
|
75455
|
-
listChannelPlugins,
|
|
75456
|
-
writePid: async (pidPath, pid = process.pid) => {
|
|
75457
|
-
await ensureDir2(dirname12(pidPath));
|
|
75458
|
-
await writeTextFile(pidPath, `${pid}
|
|
75459
|
-
`);
|
|
75460
|
-
},
|
|
75461
|
-
readState: readRuntimeMonitorState,
|
|
75462
|
-
writeState: writeRuntimeMonitorState,
|
|
75463
|
-
removePid: (pidPath) => rmSync2(pidPath, { force: true }),
|
|
75464
|
-
removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync2(runtimeCredentialsPath, { force: true }),
|
|
75465
|
-
sleep,
|
|
75466
|
-
now: () => Date.now(),
|
|
75467
|
-
spawnChild: (command, args, options) => spawn2(command, args, {
|
|
75468
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
75469
|
-
env: options.env
|
|
75470
|
-
}),
|
|
75471
|
-
sendSignal: kill
|
|
75472
|
-
};
|
|
75473
|
-
function isProcessAlive(pid) {
|
|
75474
|
-
try {
|
|
75475
|
-
kill(pid, 0);
|
|
75476
|
-
return true;
|
|
75477
|
-
} catch {
|
|
75478
|
-
return false;
|
|
75479
|
-
}
|
|
75480
|
-
}
|
|
75481
|
-
async function readRuntimeMonitorState(statePath = getDefaultRuntimeMonitorStatePath()) {
|
|
75482
|
-
if (!await fileExists(statePath)) {
|
|
75483
|
-
return null;
|
|
75484
|
-
}
|
|
75485
|
-
try {
|
|
75486
|
-
const raw = await readTextFile(statePath);
|
|
75487
|
-
if (!raw.trim()) {
|
|
75488
|
-
return null;
|
|
75489
|
-
}
|
|
75490
|
-
return JSON.parse(raw);
|
|
75491
|
-
} catch {
|
|
75492
|
-
return null;
|
|
75493
|
-
}
|
|
75494
|
-
}
|
|
75495
|
-
async function writeRuntimeMonitorState(statePath, state) {
|
|
75496
|
-
await ensureDir2(dirname12(statePath));
|
|
75497
|
-
await writeTextFile(statePath, `${JSON.stringify(state, null, 2)}
|
|
75498
|
-
`);
|
|
75499
|
-
}
|
|
75500
|
-
function dedupe(values) {
|
|
75501
|
-
return [...new Set(values.filter(Boolean))];
|
|
75502
|
-
}
|
|
75503
|
-
function summarizeExit(params) {
|
|
75504
|
-
if (params.signal) {
|
|
75505
|
-
return `signal ${params.signal}`;
|
|
75506
|
-
}
|
|
75507
|
-
return `code ${params.code ?? 0}`;
|
|
75508
|
-
}
|
|
75509
|
-
function getRestartPlan(config, restartNumber) {
|
|
75510
|
-
const fastRetryMaxRestarts = config.fastRetry.maxRestarts;
|
|
75511
|
-
const totalRestarts = fastRetryMaxRestarts + config.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
|
|
75512
|
-
if (restartNumber >= 1 && restartNumber <= fastRetryMaxRestarts) {
|
|
75513
|
-
return {
|
|
75514
|
-
mode: "fast-retry",
|
|
75515
|
-
stageIndex: -1,
|
|
75516
|
-
delayMs: config.fastRetry.delaySeconds * 1000,
|
|
75517
|
-
restartAttemptInStage: restartNumber,
|
|
75518
|
-
restartsRemaining: totalRestarts - restartNumber,
|
|
75519
|
-
totalRestarts,
|
|
75520
|
-
stageMaxRestarts: fastRetryMaxRestarts
|
|
75521
|
-
};
|
|
75522
|
-
}
|
|
75523
|
-
let completedRestarts = fastRetryMaxRestarts;
|
|
75524
|
-
for (let index = 0;index < config.stages.length; index += 1) {
|
|
75525
|
-
const stage = config.stages[index];
|
|
75526
|
-
const stageStart = completedRestarts + 1;
|
|
75527
|
-
const stageEnd = completedRestarts + stage.maxRestarts;
|
|
75528
|
-
if (restartNumber >= stageStart && restartNumber <= stageEnd) {
|
|
75529
|
-
return {
|
|
75530
|
-
mode: "backoff",
|
|
75531
|
-
stageIndex: index,
|
|
75532
|
-
delayMs: stage.delayMinutes * 60000,
|
|
75533
|
-
restartAttemptInStage: restartNumber - completedRestarts,
|
|
75534
|
-
restartsRemaining: totalRestarts - restartNumber,
|
|
75535
|
-
totalRestarts,
|
|
75536
|
-
stageMaxRestarts: stage.maxRestarts
|
|
75537
|
-
};
|
|
75538
|
-
}
|
|
75539
|
-
completedRestarts = stageEnd;
|
|
75540
|
-
}
|
|
75541
|
-
return null;
|
|
75542
|
-
}
|
|
75948
|
+
// src/control/owner-alerts.ts
|
|
75543
75949
|
function parseOwnerPrincipal(principal) {
|
|
75544
75950
|
const trimmed = principal.trim();
|
|
75545
75951
|
if (!trimmed) {
|
|
@@ -75581,58 +75987,30 @@ function buildOwnerAlertCommand(params) {
|
|
|
75581
75987
|
renderMode: "native"
|
|
75582
75988
|
};
|
|
75583
75989
|
}
|
|
75990
|
+
function dedupe(values) {
|
|
75991
|
+
return [...new Set(values.filter(Boolean))];
|
|
75992
|
+
}
|
|
75584
75993
|
async function sendOwnerAlert(params) {
|
|
75585
|
-
const plugins = params.
|
|
75586
|
-
const loadedByPlatform = new Map;
|
|
75994
|
+
const plugins = params.listChannelPlugins();
|
|
75587
75995
|
const delivered = [];
|
|
75588
75996
|
const failed = [];
|
|
75589
|
-
async function loadPlatform(platform) {
|
|
75590
|
-
const existing = loadedByPlatform.get(platform);
|
|
75591
|
-
if (existing) {
|
|
75592
|
-
return existing;
|
|
75593
|
-
}
|
|
75594
|
-
const loaded = await params.dependencies.loadConfig(params.configPath, {
|
|
75595
|
-
materializeChannels: [platform]
|
|
75596
|
-
});
|
|
75597
|
-
loadedByPlatform.set(platform, loaded);
|
|
75598
|
-
return loaded;
|
|
75599
|
-
}
|
|
75600
|
-
const ownersByPlatform = new Map;
|
|
75601
|
-
for (const platform of ["slack", "telegram"]) {
|
|
75602
|
-
try {
|
|
75603
|
-
const loaded = await loadPlatform(platform);
|
|
75604
|
-
const principals = dedupe(loaded.raw.app.auth.roles.owner?.users ?? []);
|
|
75605
|
-
ownersByPlatform.set(platform, principals.map(parseOwnerPrincipal).filter((entry) => entry?.platform === platform).map((entry) => entry.userId));
|
|
75606
|
-
} catch {
|
|
75607
|
-
ownersByPlatform.set(platform, []);
|
|
75608
|
-
}
|
|
75609
|
-
}
|
|
75610
75997
|
for (const platform of ["slack", "telegram"]) {
|
|
75611
|
-
const
|
|
75998
|
+
const principals = dedupe(params.loadedConfig.raw.app.auth.roles.owner?.users ?? []);
|
|
75999
|
+
const ownerIds = principals.map(parseOwnerPrincipal).filter((entry) => entry?.platform === platform).map((entry) => entry.userId);
|
|
75612
76000
|
if (ownerIds.length === 0) {
|
|
75613
76001
|
continue;
|
|
75614
76002
|
}
|
|
75615
|
-
const loaded = await loadPlatform(platform).catch(() => null);
|
|
75616
|
-
if (!loaded) {
|
|
75617
|
-
for (const userId of ownerIds) {
|
|
75618
|
-
failed.push({
|
|
75619
|
-
principal: `${platform}:${userId}`,
|
|
75620
|
-
detail: "config could not be loaded with resolved credentials"
|
|
75621
|
-
});
|
|
75622
|
-
}
|
|
75623
|
-
continue;
|
|
75624
|
-
}
|
|
75625
76003
|
const plugin = plugins.find((entry) => entry.id === platform);
|
|
75626
|
-
if (!plugin || !plugin.isEnabled(
|
|
76004
|
+
if (!plugin || !plugin.isEnabled(params.loadedConfig)) {
|
|
75627
76005
|
continue;
|
|
75628
76006
|
}
|
|
75629
|
-
const botIds = dedupe(plugin.listBots(
|
|
76007
|
+
const botIds = dedupe(plugin.listBots(params.loadedConfig).map((entry) => entry.botId));
|
|
75630
76008
|
for (const userId of ownerIds) {
|
|
75631
76009
|
let deliveredToPrincipal = false;
|
|
75632
76010
|
const principal = `${platform}:${userId}`;
|
|
75633
76011
|
for (const botId of botIds) {
|
|
75634
76012
|
try {
|
|
75635
|
-
await plugin.runMessageCommand(
|
|
76013
|
+
await plugin.runMessageCommand(params.loadedConfig, buildOwnerAlertCommand({
|
|
75636
76014
|
platform,
|
|
75637
76015
|
botId,
|
|
75638
76016
|
userId,
|
|
@@ -75661,16 +76039,73 @@ async function sendOwnerAlert(params) {
|
|
|
75661
76039
|
failed
|
|
75662
76040
|
};
|
|
75663
76041
|
}
|
|
76042
|
+
|
|
76043
|
+
// src/control/runtime-monitor.ts
|
|
76044
|
+
var defaultRuntimeMonitorDependencies = {
|
|
76045
|
+
loadConfig,
|
|
76046
|
+
listChannelPlugins,
|
|
76047
|
+
writePid: async (pidPath, pid = process.pid) => {
|
|
76048
|
+
await ensureDir2(dirname12(pidPath));
|
|
76049
|
+
await writeTextFile(pidPath, `${pid}
|
|
76050
|
+
`);
|
|
76051
|
+
},
|
|
76052
|
+
readState: readRuntimeMonitorState,
|
|
76053
|
+
writeState: writeRuntimeMonitorState,
|
|
76054
|
+
removePid: (pidPath) => rmSync2(pidPath, { force: true }),
|
|
76055
|
+
removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync2(runtimeCredentialsPath, { force: true }),
|
|
76056
|
+
sleep,
|
|
76057
|
+
now: () => Date.now(),
|
|
76058
|
+
spawnChild: (command, args, options) => spawn2(command, args, {
|
|
76059
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
76060
|
+
env: options.env
|
|
76061
|
+
}),
|
|
76062
|
+
sendSignal: kill
|
|
76063
|
+
};
|
|
76064
|
+
function isProcessAlive(pid) {
|
|
76065
|
+
try {
|
|
76066
|
+
kill(pid, 0);
|
|
76067
|
+
return true;
|
|
76068
|
+
} catch {
|
|
76069
|
+
return false;
|
|
76070
|
+
}
|
|
76071
|
+
}
|
|
76072
|
+
async function readRuntimeMonitorState(statePath = getDefaultRuntimeMonitorStatePath()) {
|
|
76073
|
+
if (!await fileExists(statePath)) {
|
|
76074
|
+
return null;
|
|
76075
|
+
}
|
|
76076
|
+
try {
|
|
76077
|
+
const raw = await readTextFile(statePath);
|
|
76078
|
+
if (!raw.trim()) {
|
|
76079
|
+
return null;
|
|
76080
|
+
}
|
|
76081
|
+
return JSON.parse(raw);
|
|
76082
|
+
} catch {
|
|
76083
|
+
return null;
|
|
76084
|
+
}
|
|
76085
|
+
}
|
|
76086
|
+
async function writeRuntimeMonitorState(statePath, state) {
|
|
76087
|
+
await ensureDir2(dirname12(statePath));
|
|
76088
|
+
await writeTextFile(statePath, `${JSON.stringify(state, null, 2)}
|
|
76089
|
+
`);
|
|
76090
|
+
}
|
|
76091
|
+
function summarizeExit(params) {
|
|
76092
|
+
if (params.signal) {
|
|
76093
|
+
return `signal ${params.signal}`;
|
|
76094
|
+
}
|
|
76095
|
+
return `code ${params.code ?? 0}`;
|
|
76096
|
+
}
|
|
75664
76097
|
function renderBackoffAlertMessage(params) {
|
|
76098
|
+
const restartLine = params.repeatingFinalStage ? `restart: ${params.restartNumber} (steady-state at final stage; configured ladder ${params.totalConfiguredRestarts})` : `restart: ${params.restartNumber}/${params.totalConfiguredRestarts}`;
|
|
76099
|
+
const stageAttemptLine = params.repeatingFinalStage ? "stage attempt: steady-state retry on final stage" : `stage attempt: ${params.restartAttemptInStage}/${params.stageMaxRestarts}`;
|
|
75665
76100
|
return [
|
|
75666
76101
|
"clisbot runtime alert",
|
|
75667
76102
|
"",
|
|
75668
76103
|
"status: runtime exited unexpectedly and entered restart backoff",
|
|
75669
76104
|
`last exit: ${summarizeExit(params.exit)} at ${params.exit.at}`,
|
|
75670
76105
|
`next restart: ${params.nextRestartAt}`,
|
|
75671
|
-
|
|
76106
|
+
restartLine,
|
|
75672
76107
|
`stage: ${params.stageIndex + 1}/${params.config.restartBackoff.stages.length}`,
|
|
75673
|
-
|
|
76108
|
+
stageAttemptLine
|
|
75674
76109
|
].join(`
|
|
75675
76110
|
`);
|
|
75676
76111
|
}
|
|
@@ -75680,7 +76115,7 @@ function renderStoppedAlertMessage(params) {
|
|
|
75680
76115
|
"",
|
|
75681
76116
|
"status: runtime stopped after exhausting the configured restart budget",
|
|
75682
76117
|
`last exit: ${summarizeExit(params.exit)} at ${params.exit.at}`,
|
|
75683
|
-
`restart budget used: ${params.
|
|
76118
|
+
`restart budget used: ${params.totalConfiguredRestarts}`,
|
|
75684
76119
|
`action: inspect ${renderCliCommand("logs", { inline: true })}, fix the fault, then start the service again`
|
|
75685
76120
|
].join(`
|
|
75686
76121
|
`);
|
|
@@ -75697,6 +76132,7 @@ class RuntimeMonitor {
|
|
|
75697
76132
|
stopRequested = false;
|
|
75698
76133
|
activeChild = null;
|
|
75699
76134
|
latestState = null;
|
|
76135
|
+
loadedConfig;
|
|
75700
76136
|
constructor(scriptPath, configPath, pidPath, statePath, runtimeCredentialsPath, dependencies) {
|
|
75701
76137
|
this.scriptPath = scriptPath;
|
|
75702
76138
|
this.configPath = configPath;
|
|
@@ -75710,13 +76146,16 @@ class RuntimeMonitor {
|
|
|
75710
76146
|
this.registerProcessHandlers();
|
|
75711
76147
|
try {
|
|
75712
76148
|
const loadedConfig = await this.dependencies.loadConfig(this.configPath);
|
|
76149
|
+
this.loadedConfig = loadedConfig;
|
|
75713
76150
|
const monitorConfig = loadedConfig.raw.control.runtimeMonitor;
|
|
76151
|
+
monitorConfig.restartBackoff = normalizeRuntimeMonitorRestartBackoff(monitorConfig.restartBackoff);
|
|
75714
76152
|
let restartNumber = 0;
|
|
75715
|
-
let
|
|
76153
|
+
let totalConfiguredRestarts = getConfiguredRuntimeMonitorRestartBudget(monitorConfig.restartBackoff);
|
|
75716
76154
|
await this.writeState({
|
|
75717
76155
|
phase: "starting"
|
|
75718
76156
|
});
|
|
75719
76157
|
while (!this.stopRequested) {
|
|
76158
|
+
const runStartedAt = this.dependencies.now();
|
|
75720
76159
|
const child = this.dependencies.spawnChild(process.execPath, [this.scriptPath, "serve-foreground"], {
|
|
75721
76160
|
env: {
|
|
75722
76161
|
...process.env,
|
|
@@ -75738,11 +76177,14 @@ class RuntimeMonitor {
|
|
|
75738
76177
|
break;
|
|
75739
76178
|
}
|
|
75740
76179
|
const exitAt = new Date().toISOString();
|
|
76180
|
+
if (this.dependencies.now() - runStartedAt >= RUNTIME_MONITOR_RESTART_RESET_AFTER_MS) {
|
|
76181
|
+
restartNumber = 0;
|
|
76182
|
+
}
|
|
75741
76183
|
const nextRestartNumber = restartNumber + 1;
|
|
75742
|
-
const plan =
|
|
76184
|
+
const plan = getRuntimeMonitorRestartPlan(monitorConfig.restartBackoff, nextRestartNumber);
|
|
75743
76185
|
if (!plan) {
|
|
75744
76186
|
await this.maybeSendAlert("stopped", monitorConfig, renderStoppedAlertMessage({
|
|
75745
|
-
|
|
76187
|
+
totalConfiguredRestarts,
|
|
75746
76188
|
exit: {
|
|
75747
76189
|
code: exit.code,
|
|
75748
76190
|
signal: exit.signal,
|
|
@@ -75762,7 +76204,7 @@ class RuntimeMonitor {
|
|
|
75762
76204
|
return;
|
|
75763
76205
|
}
|
|
75764
76206
|
restartNumber = nextRestartNumber;
|
|
75765
|
-
|
|
76207
|
+
totalConfiguredRestarts = plan.totalConfiguredRestarts;
|
|
75766
76208
|
const nextRestartAt = new Date(this.dependencies.now() + plan.delayMs).toISOString();
|
|
75767
76209
|
if (plan.mode === "backoff") {
|
|
75768
76210
|
await this.maybeSendAlert("backoff", monitorConfig, renderBackoffAlertMessage({
|
|
@@ -75771,8 +76213,9 @@ class RuntimeMonitor {
|
|
|
75771
76213
|
stageIndex: plan.stageIndex,
|
|
75772
76214
|
restartAttemptInStage: plan.restartAttemptInStage,
|
|
75773
76215
|
stageMaxRestarts: plan.stageMaxRestarts,
|
|
75774
|
-
|
|
76216
|
+
totalConfiguredRestarts,
|
|
75775
76217
|
nextRestartAt,
|
|
76218
|
+
repeatingFinalStage: plan.repeatingFinalStage,
|
|
75776
76219
|
exit: {
|
|
75777
76220
|
code: exit.code,
|
|
75778
76221
|
signal: exit.signal,
|
|
@@ -75873,10 +76316,13 @@ class RuntimeMonitor {
|
|
|
75873
76316
|
}
|
|
75874
76317
|
}
|
|
75875
76318
|
try {
|
|
76319
|
+
if (!this.loadedConfig) {
|
|
76320
|
+
return;
|
|
76321
|
+
}
|
|
75876
76322
|
const result = await sendOwnerAlert({
|
|
75877
|
-
|
|
76323
|
+
loadedConfig: this.loadedConfig,
|
|
75878
76324
|
message,
|
|
75879
|
-
|
|
76325
|
+
listChannelPlugins: this.dependencies.listChannelPlugins
|
|
75880
76326
|
});
|
|
75881
76327
|
if (result.delivered.length === 0 && result.failed.length > 0) {
|
|
75882
76328
|
console.error("clisbot runtime alert delivery failed", result.failed.map((entry) => `${entry.principal}: ${entry.detail}`).join("; "));
|
|
@@ -76021,6 +76467,20 @@ function resolveMonitorStatePath(monitorStatePath, configPath, options = {}) {
|
|
|
76021
76467
|
}
|
|
76022
76468
|
return expandHomePath(getDefaultRuntimeMonitorStatePath());
|
|
76023
76469
|
}
|
|
76470
|
+
function resolveLiveMonitorPid(params) {
|
|
76471
|
+
const processLiveness = params.processLiveness ?? getProcessLiveness;
|
|
76472
|
+
if (params.pidFromFile && processLiveness(params.pidFromFile) === "running") {
|
|
76473
|
+
return params.pidFromFile;
|
|
76474
|
+
}
|
|
76475
|
+
const monitorPid = params.monitorState?.monitorPid;
|
|
76476
|
+
if (monitorPid && processLiveness(monitorPid) === "running") {
|
|
76477
|
+
return monitorPid;
|
|
76478
|
+
}
|
|
76479
|
+
return null;
|
|
76480
|
+
}
|
|
76481
|
+
function resolveKnownMonitorPid(params) {
|
|
76482
|
+
return params.pidFromFile ?? params.monitorState?.monitorPid ?? null;
|
|
76483
|
+
}
|
|
76024
76484
|
function resolveRuntimeCredentialsPath(runtimeCredentialsPath, configPath, options = {}) {
|
|
76025
76485
|
if (runtimeCredentialsPath) {
|
|
76026
76486
|
return expandHomePath(runtimeCredentialsPath);
|
|
@@ -76125,11 +76585,18 @@ async function startDetachedRuntime(params) {
|
|
|
76125
76585
|
const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath, configPath, { preferConfigSibling });
|
|
76126
76586
|
const existingPid = await readRuntimePid(pidPath);
|
|
76127
76587
|
const existingMonitorState = await readRuntimeMonitorState(monitorStatePath);
|
|
76128
|
-
|
|
76588
|
+
const liveMonitorPid = resolveLiveMonitorPid({
|
|
76589
|
+
pidFromFile: existingPid,
|
|
76590
|
+
monitorState: existingMonitorState
|
|
76591
|
+
});
|
|
76592
|
+
if (liveMonitorPid) {
|
|
76593
|
+
if (existingPid !== liveMonitorPid) {
|
|
76594
|
+
await writeRuntimePid(pidPath, liveMonitorPid);
|
|
76595
|
+
}
|
|
76129
76596
|
return {
|
|
76130
76597
|
alreadyRunning: true,
|
|
76131
76598
|
createdConfig: false,
|
|
76132
|
-
pid:
|
|
76599
|
+
pid: liveMonitorPid,
|
|
76133
76600
|
configPath,
|
|
76134
76601
|
logPath
|
|
76135
76602
|
};
|
|
@@ -76208,10 +76675,14 @@ async function stopDetachedRuntime(params, dependencies = {}) {
|
|
|
76208
76675
|
const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
|
|
76209
76676
|
const sendSignal = dependencies.sendSignal ?? kill2;
|
|
76210
76677
|
const sleepFn = dependencies.sleep ?? sleep;
|
|
76211
|
-
const
|
|
76212
|
-
|
|
76213
|
-
|
|
76214
|
-
|
|
76678
|
+
const monitorPid = resolveKnownMonitorPid({
|
|
76679
|
+
pidFromFile: existingPid,
|
|
76680
|
+
monitorState
|
|
76681
|
+
});
|
|
76682
|
+
const monitorLiveness = monitorPid ? processLiveness(monitorPid) : "missing";
|
|
76683
|
+
if (monitorPid && monitorLiveness === "running") {
|
|
76684
|
+
sendSignal(monitorPid, "SIGTERM");
|
|
76685
|
+
const exited = await waitForProcessExit(monitorPid, STOP_WAIT_TIMEOUT_MS, {
|
|
76215
76686
|
processLiveness,
|
|
76216
76687
|
sleep: sleepFn
|
|
76217
76688
|
});
|
|
@@ -76219,7 +76690,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
|
|
|
76219
76690
|
throw new Error(`clisbot did not stop within ${STOP_WAIT_TIMEOUT_MS}ms`);
|
|
76220
76691
|
}
|
|
76221
76692
|
stopped = true;
|
|
76222
|
-
} else if (
|
|
76693
|
+
} else if (monitorPid && monitorLiveness === "zombie") {
|
|
76223
76694
|
stopped = true;
|
|
76224
76695
|
}
|
|
76225
76696
|
const runtimePid = monitorState?.runtimePid;
|
|
@@ -76235,7 +76706,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
|
|
|
76235
76706
|
}
|
|
76236
76707
|
stopped = true;
|
|
76237
76708
|
} catch (error) {
|
|
76238
|
-
if (!(
|
|
76709
|
+
if (!(monitorPid && monitorLiveness === "running")) {
|
|
76239
76710
|
throw error;
|
|
76240
76711
|
}
|
|
76241
76712
|
}
|
|
@@ -76293,11 +76764,14 @@ async function getRuntimeStatus(params = {}) {
|
|
|
76293
76764
|
preferConfigSibling
|
|
76294
76765
|
});
|
|
76295
76766
|
const pid = await readRuntimePid(pidPath);
|
|
76296
|
-
const liveness = pid ? getProcessLiveness(pid) : "missing";
|
|
76297
76767
|
const monitorState = await readRuntimeMonitorState(monitorStatePath);
|
|
76768
|
+
const liveMonitorPid = resolveLiveMonitorPid({
|
|
76769
|
+
pidFromFile: pid,
|
|
76770
|
+
monitorState
|
|
76771
|
+
});
|
|
76298
76772
|
return {
|
|
76299
|
-
running:
|
|
76300
|
-
pid:
|
|
76773
|
+
running: liveMonitorPid != null,
|
|
76774
|
+
pid: liveMonitorPid ?? undefined,
|
|
76301
76775
|
configPath,
|
|
76302
76776
|
pidPath,
|
|
76303
76777
|
logPath,
|
|
@@ -76495,7 +76969,7 @@ function extractLinuxProcState(raw) {
|
|
|
76495
76969
|
}
|
|
76496
76970
|
|
|
76497
76971
|
// src/control/bots-cli.ts
|
|
76498
|
-
function
|
|
76972
|
+
function getEditableConfigPath7() {
|
|
76499
76973
|
return process.env.CLISBOT_CONFIG_PATH;
|
|
76500
76974
|
}
|
|
76501
76975
|
function parseOptionValue2(args, name) {
|
|
@@ -76685,7 +77159,7 @@ function summarizeBotConfig(provider, botId, bot) {
|
|
|
76685
77159
|
};
|
|
76686
77160
|
}
|
|
76687
77161
|
async function listBots(args) {
|
|
76688
|
-
const { config } = await readEditableConfig(
|
|
77162
|
+
const { config } = await readEditableConfig(getEditableConfigPath7());
|
|
76689
77163
|
const provider = parseOptionValue2(args, "--channel");
|
|
76690
77164
|
const printJson = hasFlag3(args, "--json");
|
|
76691
77165
|
const summaries = [
|
|
@@ -76710,7 +77184,7 @@ async function addOrSetBotCredentials(args, deps, action) {
|
|
|
76710
77184
|
const botId = getBotId(args);
|
|
76711
77185
|
const persist = hasFlag3(args, "--persist");
|
|
76712
77186
|
const runtimeStatus = await deps.getRuntimeStatus();
|
|
76713
|
-
const { config, configPath } = await readEditableConfig(
|
|
77187
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76714
77188
|
const exists = provider === "slack" ? botId in getSlackBots2(config) : (botId in getTelegramBots2(config));
|
|
76715
77189
|
if (action === "add" && exists) {
|
|
76716
77190
|
throw new Error(`Bot already exists: ${provider}/${botId}. Use ${renderCliCommand("bots set-agent ...", { inline: true })}, ${renderCliCommand("bots set-credentials ...", { inline: true })}, or another \`set-<key>\` command.`);
|
|
@@ -76838,7 +77312,7 @@ async function getBot(args) {
|
|
|
76838
77312
|
const provider = parseProvider(args);
|
|
76839
77313
|
const botId = getBotId(args);
|
|
76840
77314
|
const printJson = hasFlag3(args, "--json");
|
|
76841
|
-
const { config, configPath } = await readEditableConfig(
|
|
77315
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76842
77316
|
const bot = ensureProviderBot(config, provider, botId);
|
|
76843
77317
|
if (printJson) {
|
|
76844
77318
|
console.log(JSON.stringify(bot, null, 2));
|
|
@@ -76849,7 +77323,7 @@ async function getBot(args) {
|
|
|
76849
77323
|
async function setBotEnabled(args, enabled) {
|
|
76850
77324
|
const provider = parseProvider(args);
|
|
76851
77325
|
const botId = getBotId(args);
|
|
76852
|
-
const { config, configPath } = await readEditableConfig(
|
|
77326
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76853
77327
|
const bot = ensureProviderBot(config, provider, botId);
|
|
76854
77328
|
bot.enabled = enabled;
|
|
76855
77329
|
reconcileProviderDefaults(config, provider);
|
|
@@ -76860,7 +77334,7 @@ async function setBotEnabled(args, enabled) {
|
|
|
76860
77334
|
async function removeBot(args) {
|
|
76861
77335
|
const provider = parseProvider(args);
|
|
76862
77336
|
const botId = getBotId(args);
|
|
76863
|
-
const { config, configPath } = await readEditableConfig(
|
|
77337
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76864
77338
|
const bot = ensureProviderBot(config, provider, botId);
|
|
76865
77339
|
const directMessages = "directMessages" in bot ? Object.keys(bot.directMessages ?? {}) : [];
|
|
76866
77340
|
const groups = "groups" in bot ? Object.keys(bot.groups ?? {}) : [];
|
|
@@ -76879,7 +77353,7 @@ async function removeBot(args) {
|
|
|
76879
77353
|
}
|
|
76880
77354
|
async function getOrSetDefaultBot(args, action) {
|
|
76881
77355
|
const provider = parseProvider(args);
|
|
76882
|
-
const { config, configPath } = await readEditableConfig(
|
|
77356
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76883
77357
|
if (action === "get-default") {
|
|
76884
77358
|
const botId2 = provider === "slack" ? config.bots.slack.defaults.defaultBotId : config.bots.telegram.defaults.defaultBotId;
|
|
76885
77359
|
console.log(`${provider} default bot: ${botId2}`);
|
|
@@ -76904,7 +77378,7 @@ async function getOrSetDefaultBot(args, action) {
|
|
|
76904
77378
|
async function getOrSetBotAgent(args, action) {
|
|
76905
77379
|
const provider = parseProvider(args);
|
|
76906
77380
|
const botId = getBotId(args);
|
|
76907
|
-
const { config, configPath } = await readEditableConfig(
|
|
77381
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76908
77382
|
const bot = ensureProviderBot(config, provider, botId);
|
|
76909
77383
|
if (action === "get-agent") {
|
|
76910
77384
|
console.log(`${provider}/${botId} agent: ${bot.agentId ?? "(inherit)"}`);
|
|
@@ -76934,7 +77408,7 @@ function ensureDefaultDmRoute(config, provider, botId) {
|
|
|
76934
77408
|
async function getOrSetBotPolicy(args, action) {
|
|
76935
77409
|
const provider = parseProvider(args);
|
|
76936
77410
|
const botId = getBotId(args);
|
|
76937
|
-
const { config, configPath } = await readEditableConfig(
|
|
77411
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76938
77412
|
const bot = ensureProviderBot(config, provider, botId);
|
|
76939
77413
|
if (action === "get-dm-policy") {
|
|
76940
77414
|
console.log(`${provider}/${botId} dmPolicy: ${ensureDefaultDmRoute(config, provider, botId).policy ?? "pairing"}`);
|
|
@@ -76989,7 +77463,7 @@ async function getOrSetBotPolicy(args, action) {
|
|
|
76989
77463
|
async function getCredentialSource(args) {
|
|
76990
77464
|
const provider = parseProvider(args);
|
|
76991
77465
|
const botId = getBotId(args);
|
|
76992
|
-
const { config, configPath } = await readEditableConfig(
|
|
77466
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
|
|
76993
77467
|
ensureProviderBot(config, provider, botId);
|
|
76994
77468
|
const source = provider === "slack" ? describeSlackCredentialSource({ config: config.bots.slack, botId }) : describeTelegramCredentialSource({ config: config.bots.telegram, botId });
|
|
76995
77469
|
console.log(`${provider}/${botId} credentials: ${source.detail}`);
|
|
@@ -77580,14 +78054,14 @@ async function getScopedLoopCounts(params) {
|
|
|
77580
78054
|
|
|
77581
78055
|
// src/control/loops-cli.ts
|
|
77582
78056
|
var LOOP_BUSY_RETRY_MS = 250;
|
|
77583
|
-
function
|
|
78057
|
+
function getEditableConfigPath8() {
|
|
77584
78058
|
return process.env.CLISBOT_CONFIG_PATH;
|
|
77585
78059
|
}
|
|
77586
78060
|
function getSessionState(sessionStorePath) {
|
|
77587
78061
|
return new AgentSessionState(new SessionStore(sessionStorePath));
|
|
77588
78062
|
}
|
|
77589
78063
|
async function loadLoopControlState() {
|
|
77590
|
-
const configPath = await ensureEditableConfigFile(
|
|
78064
|
+
const configPath = await ensureEditableConfigFile(getEditableConfigPath8());
|
|
77591
78065
|
const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
|
|
77592
78066
|
const sessionStorePath = resolveSessionStorePath(loadedConfig);
|
|
77593
78067
|
return {
|
|
@@ -78102,6 +78576,14 @@ function parseOptionValue4(args, name) {
|
|
|
78102
78576
|
const values = parseRepeatedOption3(args, name);
|
|
78103
78577
|
return values.length > 0 ? values.at(-1) : undefined;
|
|
78104
78578
|
}
|
|
78579
|
+
function parseAliasedOptionValue2(args, preferredName, aliasName) {
|
|
78580
|
+
const preferredValues = parseRepeatedOption3(args, preferredName);
|
|
78581
|
+
const aliasValues = parseRepeatedOption3(args, aliasName);
|
|
78582
|
+
if (preferredValues.length > 0 && aliasValues.length > 0) {
|
|
78583
|
+
throw new Error(`${preferredName} and ${aliasName} are aliases; use only one`);
|
|
78584
|
+
}
|
|
78585
|
+
return preferredValues.at(-1) ?? aliasValues.at(-1);
|
|
78586
|
+
}
|
|
78105
78587
|
function parseThreadingOptions(args, channel) {
|
|
78106
78588
|
const threadId = parseOptionValue4(args, "--thread-id");
|
|
78107
78589
|
const topicId = parseOptionValue4(args, "--topic-id");
|
|
@@ -78124,6 +78606,9 @@ function parseMessageBodyFileOption(args) {
|
|
|
78124
78606
|
}
|
|
78125
78607
|
return bodyFileValues.at(-1) ?? messageFileValues.at(-1);
|
|
78126
78608
|
}
|
|
78609
|
+
function parseMessageAttachmentOption(args) {
|
|
78610
|
+
return parseAliasedOptionValue2(args, "--file", "--media");
|
|
78611
|
+
}
|
|
78127
78612
|
function parseIntegerOption(args, name) {
|
|
78128
78613
|
const raw = parseOptionValue4(args, name);
|
|
78129
78614
|
if (!raw) {
|
|
@@ -78165,7 +78650,7 @@ function parseMessageCommand(args) {
|
|
|
78165
78650
|
target: parseOptionValue4(rest, "--target"),
|
|
78166
78651
|
message: parseOptionValue4(rest, "--message") ?? parseOptionValue4(rest, "-m"),
|
|
78167
78652
|
messageFile: parseMessageBodyFileOption(rest),
|
|
78168
|
-
media:
|
|
78653
|
+
media: parseMessageAttachmentOption(rest),
|
|
78169
78654
|
messageId: parseOptionValue4(rest, "--message-id"),
|
|
78170
78655
|
emoji: parseOptionValue4(rest, "--emoji"),
|
|
78171
78656
|
remove: hasFlag5(rest, "--remove"),
|
|
@@ -78189,7 +78674,7 @@ function renderMessageHelp() {
|
|
|
78189
78674
|
renderCliCommand("message"),
|
|
78190
78675
|
"",
|
|
78191
78676
|
"Usage:",
|
|
78192
|
-
` ${renderCliCommand("message send --channel <slack|telegram> --target <dest> [--message <text> | --body-file <path>] [--input <plain|md|html|mrkdwn|blocks>] [--render <native|none|html|mrkdwn|blocks>] [--account <id>] [--
|
|
78677
|
+
` ${renderCliCommand("message send --channel <slack|telegram> --target <dest> [--message <text> | --body-file <path>] [--input <plain|md|html|mrkdwn|blocks>] [--render <native|none|html|mrkdwn|blocks>] [--account <id>] [--file <path-or-url>] [--reply-to <id>] [--thread-id <slack-thread-ts>] [--topic-id <telegram-topic-id>] [--force-document] [--silent] [--progress|--final]")}`,
|
|
78193
78678
|
` ${renderCliCommand("message poll --channel <slack|telegram> --target <dest> --poll-question <text> --poll-option <value> [--poll-option <value>] [--account <id>] [--thread-id <slack-thread-ts>] [--topic-id <telegram-topic-id>] [--silent]")}`,
|
|
78194
78679
|
` ${renderCliCommand("message react --channel <slack|telegram> --target <dest> --message-id <id> --emoji <emoji> [--account <id>] [--remove]")}`,
|
|
78195
78680
|
` ${renderCliCommand("message reactions --channel <slack|telegram> --target <dest> --message-id <id> [--account <id>]")}`,
|
|
@@ -78205,6 +78690,8 @@ function renderMessageHelp() {
|
|
|
78205
78690
|
" --message <text> Inline message body",
|
|
78206
78691
|
" --body-file <path> Read the message body from a file",
|
|
78207
78692
|
" Alias: --message-file (compat only)",
|
|
78693
|
+
" --file <path-or-url> Attach a file or remote URL",
|
|
78694
|
+
" Alias: --media (compat only)",
|
|
78208
78695
|
" --input <plain|md|html|mrkdwn|blocks>",
|
|
78209
78696
|
" Input content format. Default: md",
|
|
78210
78697
|
" --render <native|none|html|mrkdwn|blocks>",
|
|
@@ -78221,6 +78708,11 @@ function renderMessageHelp() {
|
|
|
78221
78708
|
" html Telegram only",
|
|
78222
78709
|
" mrkdwn Slack only",
|
|
78223
78710
|
"",
|
|
78711
|
+
"Length Guidance:",
|
|
78712
|
+
" Telegram native/html Final payload must stay under 4096 chars; leave headroom after HTML-safe rendering",
|
|
78713
|
+
" Slack text/mrkdwn Prefer text under 4000 chars; Slack truncates very long text after 40000",
|
|
78714
|
+
" Slack blocks Max 50 blocks; keep header text under 150 and section text under 3000",
|
|
78715
|
+
"",
|
|
78224
78716
|
"Threading:",
|
|
78225
78717
|
" --thread-id <id> Slack thread ts",
|
|
78226
78718
|
" --topic-id <id> Telegram topic id",
|
|
@@ -78291,7 +78783,7 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
|
|
|
78291
78783
|
}
|
|
78292
78784
|
|
|
78293
78785
|
// src/control/routes-cli.ts
|
|
78294
|
-
function
|
|
78786
|
+
function getEditableConfigPath9() {
|
|
78295
78787
|
return process.env.CLISBOT_CONFIG_PATH;
|
|
78296
78788
|
}
|
|
78297
78789
|
function getSlackBots3(config) {
|
|
@@ -78567,7 +79059,7 @@ function renderRoutesHelp() {
|
|
|
78567
79059
|
`);
|
|
78568
79060
|
}
|
|
78569
79061
|
async function listRoutes(args) {
|
|
78570
|
-
const { config } = await readEditableConfig(
|
|
79062
|
+
const { config } = await readEditableConfig(getEditableConfigPath9());
|
|
78571
79063
|
const provider = parseOptionalProvider(args);
|
|
78572
79064
|
const botIdFilter = parseOptionValue5(args, "--bot");
|
|
78573
79065
|
const printJson = hasFlag6(args, "--json");
|
|
@@ -78628,7 +79120,7 @@ async function addRoute(args) {
|
|
|
78628
79120
|
const policy = parseOptionValue5(args, "--policy");
|
|
78629
79121
|
const requireMention = parseOptionValue5(args, "--require-mention");
|
|
78630
79122
|
const allowBots = parseOptionValue5(args, "--allow-bots");
|
|
78631
|
-
const { config, configPath } = await readEditableConfig(
|
|
79123
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78632
79124
|
const existing = getOrCreateRoute(config, provider, botId, parsed);
|
|
78633
79125
|
if (existing) {
|
|
78634
79126
|
throw new Error(`Route already exists: ${provider}/${botId}/${parsed.routeId}. Use a matching \`set-<key>\` command instead.`);
|
|
@@ -78652,7 +79144,7 @@ async function getRoute(args) {
|
|
|
78652
79144
|
const botId = getBotId2(args);
|
|
78653
79145
|
const parsed = parseCommandRoute(args, provider);
|
|
78654
79146
|
const printJson = hasFlag6(args, "--json");
|
|
78655
|
-
const { config, configPath } = await readEditableConfig(
|
|
79147
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78656
79148
|
const route = ensureRoute(config, provider, botId, parsed);
|
|
78657
79149
|
if (printJson) {
|
|
78658
79150
|
console.log(JSON.stringify(route, null, 2));
|
|
@@ -78665,7 +79157,7 @@ async function setRouteEnabled(args, enabled) {
|
|
|
78665
79157
|
const botId = getBotId2(args);
|
|
78666
79158
|
const parsed = parseCommandRoute(args, provider);
|
|
78667
79159
|
rejectExactDirectMessageAdmissionChange(parsed, enabled ? "Enabling" : "Disabling");
|
|
78668
|
-
const { config, configPath } = await readEditableConfig(
|
|
79160
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78669
79161
|
const route = ensureRoute(config, provider, botId, parsed);
|
|
78670
79162
|
route.enabled = enabled;
|
|
78671
79163
|
await writeEditableConfig(configPath, config);
|
|
@@ -78676,7 +79168,7 @@ async function removeRoute(args) {
|
|
|
78676
79168
|
const provider = parseProvider2(args);
|
|
78677
79169
|
const botId = getBotId2(args);
|
|
78678
79170
|
const parsed = parseCommandRoute(args, provider);
|
|
78679
|
-
const { config, configPath } = await readEditableConfig(
|
|
79171
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78680
79172
|
ensureRoute(config, provider, botId, parsed);
|
|
78681
79173
|
if (provider === "slack") {
|
|
78682
79174
|
const bot = ensureSlackBot(config, botId);
|
|
@@ -78703,7 +79195,7 @@ async function getSetClearRouteField(args, action) {
|
|
|
78703
79195
|
const provider = parseProvider2(args);
|
|
78704
79196
|
const botId = getBotId2(args);
|
|
78705
79197
|
const parsed = parseCommandRoute(args, provider);
|
|
78706
|
-
const { config, configPath } = await readEditableConfig(
|
|
79198
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78707
79199
|
const route = ensureRoute(config, provider, botId, parsed);
|
|
78708
79200
|
if (action === "get-agent") {
|
|
78709
79201
|
console.log(`${provider}/${botId}/${parsed.routeId} agent: ${route.agentId ?? "(inherit)"}`);
|
|
@@ -78809,7 +79301,7 @@ async function mutateRouteUsers(args, action) {
|
|
|
78809
79301
|
if (!user) {
|
|
78810
79302
|
throw new Error(renderRoutesHelp());
|
|
78811
79303
|
}
|
|
78812
|
-
const { config, configPath } = await readEditableConfig(
|
|
79304
|
+
const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
|
|
78813
79305
|
const route = parsed.storage === "directMessages" && isDirectMessageWildcardRouteId(parsed.routeId) ? ensureBotDirectMessageWildcardRoute(config, provider, botId) : ensureRoute(config, provider, botId, parsed);
|
|
78814
79306
|
const field = action.includes("allow") ? "allowUsers" : "blockUsers";
|
|
78815
79307
|
const current = Array.from(new Set((route[field] ?? []).filter(Boolean)));
|
|
@@ -79055,7 +79547,7 @@ function renderRunnerHelp() {
|
|
|
79055
79547
|
` ${renderCliCommand("runner smoke --backend all --suite launch-trio [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]")}`,
|
|
79056
79548
|
"",
|
|
79057
79549
|
"Operator session debugging:",
|
|
79058
|
-
" - `list` shows current tmux runner sessions, newest admitted turn first when known",
|
|
79550
|
+
" - `list` shows current tmux runner sessions, newest admitted turn first when known, plus stored sessionId/state when available",
|
|
79059
79551
|
" - `inspect` captures one snapshot from a named tmux session",
|
|
79060
79552
|
" - `watch --latest` follows the session that most recently admitted a new prompt",
|
|
79061
79553
|
" - `watch --next` waits for the next newly admitted prompt, then follows that session",
|
|
@@ -79255,21 +79747,29 @@ async function runListCli() {
|
|
|
79255
79747
|
console.log([
|
|
79256
79748
|
renderCliCommand("runner list"),
|
|
79257
79749
|
"",
|
|
79258
|
-
...sessions.map(
|
|
79259
|
-
if (!session.entry) {
|
|
79260
|
-
return `- ${session.sessionName}`;
|
|
79261
|
-
}
|
|
79262
|
-
return [
|
|
79263
|
-
`- ${session.sessionName}`,
|
|
79264
|
-
` agent: ${session.entry.agentId}`,
|
|
79265
|
-
` sessionKey: ${session.entry.sessionKey}`,
|
|
79266
|
-
` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
|
|
79267
|
-
].join(`
|
|
79268
|
-
`);
|
|
79269
|
-
})
|
|
79750
|
+
...sessions.map(renderRunnerListSession)
|
|
79270
79751
|
].join(`
|
|
79271
79752
|
`));
|
|
79272
79753
|
}
|
|
79754
|
+
function renderRunnerListSession(session) {
|
|
79755
|
+
if (!session.entry) {
|
|
79756
|
+
return [
|
|
79757
|
+
`- sessionName: ${session.sessionName}`,
|
|
79758
|
+
" sessionId: none",
|
|
79759
|
+
" state: unmanaged"
|
|
79760
|
+
].join(`
|
|
79761
|
+
`);
|
|
79762
|
+
}
|
|
79763
|
+
return [
|
|
79764
|
+
`- sessionName: ${session.sessionName}`,
|
|
79765
|
+
` agent: ${session.entry.agentId}`,
|
|
79766
|
+
` sessionKey: ${session.entry.sessionKey}`,
|
|
79767
|
+
` sessionId: ${session.entry.sessionId?.trim() || "none"}`,
|
|
79768
|
+
` state: ${session.entry.runtime?.state ?? "no-runtime"}`,
|
|
79769
|
+
` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
|
|
79770
|
+
].join(`
|
|
79771
|
+
`);
|
|
79772
|
+
}
|
|
79273
79773
|
async function runInspectCli(args) {
|
|
79274
79774
|
const options = parseInspectCommand(args);
|
|
79275
79775
|
const { tmux } = await loadRunnerContext();
|
|
@@ -80377,6 +80877,40 @@ class ProcessedEventsStore {
|
|
|
80377
80877
|
|
|
80378
80878
|
// src/control/runtime-supervisor.ts
|
|
80379
80879
|
var SERVICE_START_TIMEOUT_MS = 8000;
|
|
80880
|
+
function buildChannelOwnerAlertKey(params) {
|
|
80881
|
+
return `${params.runtimeId}:${params.channel}:${params.botId}`;
|
|
80882
|
+
}
|
|
80883
|
+
function formatElapsedDuration(elapsedMs) {
|
|
80884
|
+
const totalMinutes = Math.max(1, Math.floor(elapsedMs / 60000));
|
|
80885
|
+
if (totalMinutes < 60) {
|
|
80886
|
+
return `${totalMinutes} minute${totalMinutes === 1 ? "" : "s"}`;
|
|
80887
|
+
}
|
|
80888
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
80889
|
+
const minutes = totalMinutes % 60;
|
|
80890
|
+
if (minutes === 0) {
|
|
80891
|
+
return `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
80892
|
+
}
|
|
80893
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
80894
|
+
}
|
|
80895
|
+
function renderChannelOwnerAlertMessage(params) {
|
|
80896
|
+
const elapsed = formatElapsedDuration(params.elapsedMs);
|
|
80897
|
+
const statusLine = params.incidentState === "failed" ? `status: ${params.channel} channel has remained failed for ${elapsed}` : params.incidentState === "still-failed" ? `status: ${params.channel} channel is still failing after ${elapsed}` : `status: ${params.channel} channel recovered after ${elapsed}`;
|
|
80898
|
+
return [
|
|
80899
|
+
"clisbot channel alert",
|
|
80900
|
+
"",
|
|
80901
|
+
statusLine,
|
|
80902
|
+
`channel: ${params.channel}/${params.botId}`,
|
|
80903
|
+
...params.summary ? [`summary: ${params.summary}`] : [],
|
|
80904
|
+
...params.detail ? [`detail: ${params.detail}`] : [],
|
|
80905
|
+
...params.incidentState === "resolved" ? [
|
|
80906
|
+
"note: the channel recovered without requiring a runtime restart"
|
|
80907
|
+
] : [
|
|
80908
|
+
"note: the runtime process is still alive; clisbot is continuing automatic channel-level recovery attempts",
|
|
80909
|
+
`action: inspect ${renderCliCommand("logs", { inline: true })} and fix the channel-level fault or conflicting poller`
|
|
80910
|
+
]
|
|
80911
|
+
].join(`
|
|
80912
|
+
`);
|
|
80913
|
+
}
|
|
80380
80914
|
|
|
80381
80915
|
class RuntimeSupervisor {
|
|
80382
80916
|
configPath;
|
|
@@ -80387,6 +80921,7 @@ class RuntimeSupervisor {
|
|
|
80387
80921
|
reloadRequested = false;
|
|
80388
80922
|
configWatchDebounceMs = 250;
|
|
80389
80923
|
nextRuntimeId = 1;
|
|
80924
|
+
channelOwnerAlertIncidents = new Map;
|
|
80390
80925
|
dependencies;
|
|
80391
80926
|
constructor(configPath, dependencies) {
|
|
80392
80927
|
this.configPath = configPath;
|
|
@@ -80479,6 +81014,7 @@ class RuntimeSupervisor {
|
|
|
80479
81014
|
});
|
|
80480
81015
|
this.activeRuntime = nextRuntime;
|
|
80481
81016
|
if (previousRuntime) {
|
|
81017
|
+
this.clearChannelOwnerAlertsForRuntime(previousRuntime.id);
|
|
80482
81018
|
for (const service of previousRuntime.channelServices) {
|
|
80483
81019
|
await service.service.stop();
|
|
80484
81020
|
}
|
|
@@ -80517,6 +81053,7 @@ class RuntimeSupervisor {
|
|
|
80517
81053
|
this.activeRuntime = previousRuntime;
|
|
80518
81054
|
}
|
|
80519
81055
|
if (nextRuntime && nextRuntime !== this.activeRuntime) {
|
|
81056
|
+
this.clearChannelOwnerAlertsForRuntime(nextRuntime.id);
|
|
80520
81057
|
for (const service of nextRuntime.channelServices) {
|
|
80521
81058
|
await service.service.stop();
|
|
80522
81059
|
}
|
|
@@ -80640,11 +81177,18 @@ class RuntimeSupervisor {
|
|
|
80640
81177
|
return channelServices.filter((entry) => entry.channel === channel).map((entry) => entry.service.getRuntimeIdentity?.()).filter((identity) => identity != null);
|
|
80641
81178
|
}
|
|
80642
81179
|
async reportChannelLifecycle(params) {
|
|
80643
|
-
|
|
81180
|
+
const activeRuntime = this.activeRuntime;
|
|
81181
|
+
if (activeRuntime?.id !== params.runtimeId) {
|
|
80644
81182
|
return;
|
|
80645
81183
|
}
|
|
80646
81184
|
const instances = this.getChannelInstances(params.channelServices, params.plugin.id);
|
|
81185
|
+
const incidentKey = buildChannelOwnerAlertKey({
|
|
81186
|
+
runtimeId: params.runtimeId,
|
|
81187
|
+
channel: params.plugin.id,
|
|
81188
|
+
botId: params.botId
|
|
81189
|
+
});
|
|
80647
81190
|
if (params.event.connection === "active") {
|
|
81191
|
+
await this.clearChannelOwnerAlert(incidentKey, params.event);
|
|
80648
81192
|
await this.dependencies.runtimeHealthStore.setChannel({
|
|
80649
81193
|
channel: params.plugin.id,
|
|
80650
81194
|
connection: "active",
|
|
@@ -80667,6 +81211,14 @@ class RuntimeSupervisor {
|
|
|
80667
81211
|
],
|
|
80668
81212
|
instances
|
|
80669
81213
|
});
|
|
81214
|
+
this.scheduleChannelOwnerAlert({
|
|
81215
|
+
key: incidentKey,
|
|
81216
|
+
runtimeId: params.runtimeId,
|
|
81217
|
+
loadedConfig: activeRuntime.loadedConfig,
|
|
81218
|
+
pluginId: params.plugin.id,
|
|
81219
|
+
botId: params.botId,
|
|
81220
|
+
event: params.event
|
|
81221
|
+
});
|
|
80670
81222
|
}
|
|
80671
81223
|
async reconcileConfigWatcher(loadedConfig) {
|
|
80672
81224
|
const configReload = loadedConfig.raw.control.configReload;
|
|
@@ -80712,12 +81264,151 @@ class RuntimeSupervisor {
|
|
|
80712
81264
|
if (!this.activeRuntime) {
|
|
80713
81265
|
return;
|
|
80714
81266
|
}
|
|
81267
|
+
this.clearChannelOwnerAlertsForRuntime(this.activeRuntime.id);
|
|
80715
81268
|
for (const service of this.activeRuntime.channelServices) {
|
|
80716
81269
|
await service.service.stop();
|
|
80717
81270
|
}
|
|
80718
81271
|
await this.activeRuntime.agentService.stop();
|
|
80719
81272
|
this.activeRuntime = undefined;
|
|
80720
81273
|
}
|
|
81274
|
+
scheduleChannelOwnerAlert(params) {
|
|
81275
|
+
const delayMs = params.event.ownerAlertAfterMs;
|
|
81276
|
+
if (!delayMs || delayMs <= 0 || !params.loadedConfig.raw.control.runtimeMonitor.ownerAlerts.enabled) {
|
|
81277
|
+
return;
|
|
81278
|
+
}
|
|
81279
|
+
const repeatAlertEveryMs = params.event.ownerAlertRepeatMs ?? params.loadedConfig.raw.control.runtimeMonitor.ownerAlerts.minIntervalMinutes * 60000;
|
|
81280
|
+
const existingIncident = this.channelOwnerAlertIncidents.get(params.key);
|
|
81281
|
+
const incident = existingIncident ?? {
|
|
81282
|
+
runtimeId: params.runtimeId,
|
|
81283
|
+
channel: params.pluginId,
|
|
81284
|
+
botId: params.botId,
|
|
81285
|
+
repeatAlertEveryMs: Math.max(1, repeatAlertEveryMs),
|
|
81286
|
+
startedAtMs: Date.now(),
|
|
81287
|
+
deliveredAlerts: 0
|
|
81288
|
+
};
|
|
81289
|
+
incident.channel = params.pluginId;
|
|
81290
|
+
incident.botId = params.botId;
|
|
81291
|
+
incident.summary = params.event.summary;
|
|
81292
|
+
incident.detail = params.event.detail;
|
|
81293
|
+
incident.repeatAlertEveryMs = Math.max(1, repeatAlertEveryMs);
|
|
81294
|
+
this.channelOwnerAlertIncidents.set(params.key, incident);
|
|
81295
|
+
if (incident.timer) {
|
|
81296
|
+
return;
|
|
81297
|
+
}
|
|
81298
|
+
this.scheduleNextChannelOwnerAlert({
|
|
81299
|
+
key: params.key,
|
|
81300
|
+
runtimeId: params.runtimeId,
|
|
81301
|
+
channel: params.pluginId,
|
|
81302
|
+
botId: params.botId,
|
|
81303
|
+
delayMs: existingIncident ? incident.repeatAlertEveryMs : delayMs
|
|
81304
|
+
});
|
|
81305
|
+
}
|
|
81306
|
+
scheduleNextChannelOwnerAlert(params) {
|
|
81307
|
+
const incident = this.channelOwnerAlertIncidents.get(params.key);
|
|
81308
|
+
if (!incident || incident.runtimeId !== params.runtimeId) {
|
|
81309
|
+
return;
|
|
81310
|
+
}
|
|
81311
|
+
incident.timer = setTimeout(() => {
|
|
81312
|
+
this.fireChannelOwnerAlert({
|
|
81313
|
+
key: params.key,
|
|
81314
|
+
runtimeId: params.runtimeId,
|
|
81315
|
+
channel: params.channel,
|
|
81316
|
+
botId: params.botId
|
|
81317
|
+
});
|
|
81318
|
+
}, params.delayMs);
|
|
81319
|
+
incident.timer.unref?.();
|
|
81320
|
+
this.channelOwnerAlertIncidents.set(params.key, incident);
|
|
81321
|
+
}
|
|
81322
|
+
async fireChannelOwnerAlert(params) {
|
|
81323
|
+
const incident = this.channelOwnerAlertIncidents.get(params.key);
|
|
81324
|
+
if (!incident || incident.runtimeId !== params.runtimeId) {
|
|
81325
|
+
return;
|
|
81326
|
+
}
|
|
81327
|
+
incident.timer = undefined;
|
|
81328
|
+
const activeRuntime = this.activeRuntime;
|
|
81329
|
+
if (activeRuntime?.id !== params.runtimeId) {
|
|
81330
|
+
return;
|
|
81331
|
+
}
|
|
81332
|
+
try {
|
|
81333
|
+
const message = renderChannelOwnerAlertMessage({
|
|
81334
|
+
channel: params.channel,
|
|
81335
|
+
botId: params.botId,
|
|
81336
|
+
incidentState: incident.deliveredAlerts === 0 ? "failed" : "still-failed",
|
|
81337
|
+
elapsedMs: Date.now() - incident.startedAtMs,
|
|
81338
|
+
summary: incident.summary,
|
|
81339
|
+
detail: incident.detail
|
|
81340
|
+
});
|
|
81341
|
+
const result = await sendOwnerAlert({
|
|
81342
|
+
loadedConfig: activeRuntime.loadedConfig,
|
|
81343
|
+
message,
|
|
81344
|
+
listChannelPlugins: this.dependencies.listChannelPlugins
|
|
81345
|
+
});
|
|
81346
|
+
if (result.delivered.length > 0) {
|
|
81347
|
+
incident.deliveredAlerts += 1;
|
|
81348
|
+
}
|
|
81349
|
+
this.channelOwnerAlertIncidents.set(params.key, incident);
|
|
81350
|
+
if (result.delivered.length === 0 && result.failed.length > 0) {
|
|
81351
|
+
console.error("clisbot channel alert delivery failed", result.failed.map((entry) => `${entry.principal}: ${entry.detail}`).join("; "));
|
|
81352
|
+
}
|
|
81353
|
+
} catch (error) {
|
|
81354
|
+
console.error("clisbot channel alert dispatch failed", error);
|
|
81355
|
+
}
|
|
81356
|
+
const nextIncident = this.channelOwnerAlertIncidents.get(params.key);
|
|
81357
|
+
if (!nextIncident || nextIncident.runtimeId !== params.runtimeId || nextIncident.timer || nextIncident.deliveredAlerts >= 2) {
|
|
81358
|
+
return;
|
|
81359
|
+
}
|
|
81360
|
+
this.scheduleNextChannelOwnerAlert({
|
|
81361
|
+
key: params.key,
|
|
81362
|
+
runtimeId: params.runtimeId,
|
|
81363
|
+
channel: params.channel,
|
|
81364
|
+
botId: params.botId,
|
|
81365
|
+
delayMs: nextIncident.repeatAlertEveryMs
|
|
81366
|
+
});
|
|
81367
|
+
}
|
|
81368
|
+
async clearChannelOwnerAlert(key, activeEvent) {
|
|
81369
|
+
const incident = this.channelOwnerAlertIncidents.get(key);
|
|
81370
|
+
if (!incident) {
|
|
81371
|
+
return;
|
|
81372
|
+
}
|
|
81373
|
+
if (incident.timer) {
|
|
81374
|
+
clearTimeout(incident.timer);
|
|
81375
|
+
}
|
|
81376
|
+
this.channelOwnerAlertIncidents.delete(key);
|
|
81377
|
+
if (incident.deliveredAlerts === 0) {
|
|
81378
|
+
return;
|
|
81379
|
+
}
|
|
81380
|
+
const activeRuntime = this.activeRuntime;
|
|
81381
|
+
if (!activeRuntime || activeRuntime.id !== incident.runtimeId) {
|
|
81382
|
+
return;
|
|
81383
|
+
}
|
|
81384
|
+
try {
|
|
81385
|
+
await sendOwnerAlert({
|
|
81386
|
+
loadedConfig: activeRuntime.loadedConfig,
|
|
81387
|
+
message: renderChannelOwnerAlertMessage({
|
|
81388
|
+
channel: incident.channel,
|
|
81389
|
+
botId: incident.botId,
|
|
81390
|
+
incidentState: "resolved",
|
|
81391
|
+
elapsedMs: Date.now() - incident.startedAtMs,
|
|
81392
|
+
summary: activeEvent?.summary ?? "Channel recovered.",
|
|
81393
|
+
detail: activeEvent?.detail
|
|
81394
|
+
}),
|
|
81395
|
+
listChannelPlugins: this.dependencies.listChannelPlugins
|
|
81396
|
+
});
|
|
81397
|
+
} catch (error) {
|
|
81398
|
+
console.error("clisbot channel recovery alert dispatch failed", error);
|
|
81399
|
+
}
|
|
81400
|
+
}
|
|
81401
|
+
clearChannelOwnerAlertsForRuntime(runtimeId) {
|
|
81402
|
+
for (const [key, incident] of this.channelOwnerAlertIncidents.entries()) {
|
|
81403
|
+
if (incident.runtimeId !== runtimeId) {
|
|
81404
|
+
continue;
|
|
81405
|
+
}
|
|
81406
|
+
if (incident.timer) {
|
|
81407
|
+
clearTimeout(incident.timer);
|
|
81408
|
+
}
|
|
81409
|
+
this.channelOwnerAlertIncidents.delete(key);
|
|
81410
|
+
}
|
|
81411
|
+
}
|
|
80721
81412
|
}
|
|
80722
81413
|
async function withStartupTimeout(name, start2) {
|
|
80723
81414
|
let timer;
|
|
@@ -80738,6 +81429,9 @@ async function withStartupTimeout(name, start2) {
|
|
|
80738
81429
|
}
|
|
80739
81430
|
|
|
80740
81431
|
// src/control/runtime-management-cli.ts
|
|
81432
|
+
function getOperatorConfigPath() {
|
|
81433
|
+
return expandHomePath(process.env.CLISBOT_CONFIG_PATH || DEFAULT_CONFIG_PATH);
|
|
81434
|
+
}
|
|
80741
81435
|
function getPrimaryWorkspacePath2(summary) {
|
|
80742
81436
|
const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
|
|
80743
81437
|
return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
|
|
@@ -80801,7 +81495,9 @@ function registerProcessHandlers(runtimeSupervisor, shutdown) {
|
|
|
80801
81495
|
});
|
|
80802
81496
|
}
|
|
80803
81497
|
async function printStatusSummary() {
|
|
80804
|
-
const runtimeStatus = await getRuntimeStatus(
|
|
81498
|
+
const runtimeStatus = await getRuntimeStatus({
|
|
81499
|
+
configPath: getOperatorConfigPath()
|
|
81500
|
+
});
|
|
80805
81501
|
console.log(`version: ${getClisbotVersion()}`);
|
|
80806
81502
|
console.log(`running: ${runtimeStatus.running ? "yes" : "no"}`);
|
|
80807
81503
|
if (runtimeStatus.pid) {
|
|
@@ -80860,7 +81556,9 @@ async function printStatusSummary() {
|
|
|
80860
81556
|
}
|
|
80861
81557
|
async function printDiagnosticsAfterLogTail() {
|
|
80862
81558
|
try {
|
|
80863
|
-
const runtimeStatus = await getRuntimeStatus(
|
|
81559
|
+
const runtimeStatus = await getRuntimeStatus({
|
|
81560
|
+
configPath: getOperatorConfigPath()
|
|
81561
|
+
});
|
|
80864
81562
|
const summary = await getRuntimeOperatorSummary({
|
|
80865
81563
|
configPath: runtimeStatus.configPath,
|
|
80866
81564
|
runtimeRunning: runtimeStatus.running
|
|
@@ -80948,6 +81646,7 @@ async function printCliError(error) {
|
|
|
80948
81646
|
}
|
|
80949
81647
|
async function stop(hard = false) {
|
|
80950
81648
|
const result = await stopDetachedRuntime({
|
|
81649
|
+
configPath: getOperatorConfigPath(),
|
|
80951
81650
|
hard
|
|
80952
81651
|
});
|
|
80953
81652
|
if (!result.stopped && !hard) {
|
|
@@ -80968,6 +81667,7 @@ async function stop(hard = false) {
|
|
|
80968
81667
|
}
|
|
80969
81668
|
async function restart() {
|
|
80970
81669
|
await stopDetachedRuntime({
|
|
81670
|
+
configPath: getOperatorConfigPath(),
|
|
80971
81671
|
hard: false
|
|
80972
81672
|
});
|
|
80973
81673
|
}
|
|
@@ -80975,7 +81675,11 @@ async function status() {
|
|
|
80975
81675
|
await printStatusSummary();
|
|
80976
81676
|
}
|
|
80977
81677
|
async function logs(lines) {
|
|
81678
|
+
const runtimeStatus = await getRuntimeStatus({
|
|
81679
|
+
configPath: getOperatorConfigPath()
|
|
81680
|
+
});
|
|
80978
81681
|
const result = await readRuntimeLog({
|
|
81682
|
+
logPath: runtimeStatus.logPath,
|
|
80979
81683
|
lines
|
|
80980
81684
|
});
|
|
80981
81685
|
if (!result.text) {
|