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/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.41"),
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.41"
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.41",
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
- return fileURLToPath3(new URL(isPackagedRuntime() ? "../main.js" : "../main.ts", import.meta.url));
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(import.meta.url);
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 the current conversation session transcript when the route verbose policy allows it",
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 pause`: stop passive follow-up until the next explicit mention",
64352
- "- `/followup resume`: clear the runtime override and restore config defaults",
64353
- "- `/pause`: shortcut for `/followup pause`",
64354
- "- `/resume`: shortcut for `/followup 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
- if (!action || action === "status") {
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 (action === "auto") {
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 (action === "mention-only") {
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 (action === "pause") {
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 (action === "resume") {
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
- [--media /absolute/path/to/file]`;
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 = "Put readable hierarchical Markdown in the --message body.";
65163
- var TELEGRAM_REPLY_STYLE_HINT = "Put readable hierarchical Markdown in the --message body.";
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 params.tmux.sendLiteral(params.sessionName, params.text);
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
- minDelayMs: params.promptSubmitDelayMs
66883
+ baselineState: prePasteState,
66884
+ baselineSnapshot: prePasteSnapshot,
66885
+ captureLines,
66886
+ promptSubmitDelayMs: params.promptSubmitDelayMs,
66887
+ timingContext: params.timingContext
66774
66888
  });
66775
- let preSubmitState = pasteSettlement.state;
66776
- if (!pasteSettlement.visible) {
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
- baselineSnapshot: prePasteSnapshot,
66784
- captureLines
66892
+ attempts: pasteDelivery.attempts
66785
66893
  });
66786
- if (!snapshotConfirmed) {
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: prePasteSnapshot,
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: prePasteSnapshot,
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 Error("tmux submit was not confirmed after Enter. The pane state did not change, so clisbot did not treat the prompt as truthfully submitted.");
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
- const sessionId = await this.captureSessionIdentity(resolved);
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.ensureSessionReady(target, { allowFreshRetry: false, timingContext });
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/interaction-processing.ts
69335
- var MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS = 3000;
69336
- var MESSAGE_TOOL_FINAL_GRACE_POLL_MS = 100;
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 renderStartupSteeringUnavailableMessage() {
69353
- return [
69354
- "The active run is still starting and cannot accept steering input yet.",
69355
- "Send a normal follow-up message to keep it ordered behind the first prompt, or wait until startup finishes before using `/steer`."
69356
- ].join(`
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 renderPrincipalFormat(identity) {
69360
- if (identity.platform === "slack") {
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
- await params.agentService.setConversationFollowUpMode(params.sessionTarget, slashCommand.mode);
70206
- await params.postText(slashCommand.mode === "paused" ? "Follow-up paused for this conversation until the next explicit mention." : `Follow-up mode set to \`${slashCommand.mode}\` for this conversation.`);
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\` or \`\\status\` here.`
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 \`\\start\` or \`\\status\` here.`,
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
- await this.initializeOffset();
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.running = false;
74572
- await this.reportLifecycle?.({
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/runtime-monitor.ts
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.dependencies.listChannelPlugins();
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 ownerIds = ownersByPlatform.get(platform) ?? [];
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(loaded)) {
76004
+ if (!plugin || !plugin.isEnabled(params.loadedConfig)) {
75627
76005
  continue;
75628
76006
  }
75629
- const botIds = dedupe(plugin.listBots(loaded).map((entry) => entry.botId));
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(loaded, buildOwnerAlertCommand({
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
- `restart: ${params.restartNumber}/${params.totalRestarts}`,
76106
+ restartLine,
75672
76107
  `stage: ${params.stageIndex + 1}/${params.config.restartBackoff.stages.length}`,
75673
- `stage attempt: ${params.restartAttemptInStage}/${params.stageMaxRestarts}`
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.totalRestarts}`,
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 totalRestarts = monitorConfig.restartBackoff.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
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 = getRestartPlan(monitorConfig.restartBackoff, nextRestartNumber);
76184
+ const plan = getRuntimeMonitorRestartPlan(monitorConfig.restartBackoff, nextRestartNumber);
75743
76185
  if (!plan) {
75744
76186
  await this.maybeSendAlert("stopped", monitorConfig, renderStoppedAlertMessage({
75745
- totalRestarts,
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
- totalRestarts = plan.totalRestarts;
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
- totalRestarts,
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
- configPath: this.configPath,
76323
+ loadedConfig: this.loadedConfig,
75878
76324
  message,
75879
- dependencies: this.dependencies
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
- if (existingPid && isProcessRunning(existingPid)) {
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: existingPid,
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 existingLiveness = existingPid ? processLiveness(existingPid) : "missing";
76212
- if (existingPid && existingLiveness === "running") {
76213
- sendSignal(existingPid, "SIGTERM");
76214
- const exited = await waitForProcessExit(existingPid, STOP_WAIT_TIMEOUT_MS, {
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 (existingPid && existingLiveness === "zombie") {
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 (!(existingPid && existingLiveness === "running")) {
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: liveness === "running",
76300
- pid: liveness === "running" && pid ? pid : undefined,
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 getEditableConfigPath6() {
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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(getEditableConfigPath6());
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 getEditableConfigPath7() {
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(getEditableConfigPath7());
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: parseOptionValue4(rest, "--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>] [--media <path-or-url>] [--reply-to <id>] [--thread-id <slack-thread-ts>] [--topic-id <telegram-topic-id>] [--force-document] [--silent] [--progress|--final]")}`,
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 getEditableConfigPath8() {
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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(getEditableConfigPath8());
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((session) => {
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
- if (this.activeRuntime?.id !== params.runtimeId) {
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) {