clisbot 0.1.41 → 0.1.42

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.42"),
60226
60336
  lastTouchedAt: exports_external.string().optional()
60227
60337
  }).default({
60228
- schemaVersion: "0.1.41"
60338
+ schemaVersion: "0.1.42"
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.42",
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
@@ -64076,9 +64129,11 @@ function parseAgentCommand(text, options = {}) {
64076
64129
  };
64077
64130
  }
64078
64131
  if (lowered === "transcript") {
64132
+ const transcriptMode = withoutSlash.slice(command.length).trim().toLowerCase() === "full" ? "full" : "default";
64079
64133
  return {
64080
64134
  type: "control",
64081
- name: "transcript"
64135
+ name: "transcript",
64136
+ mode: transcriptMode
64082
64137
  };
64083
64138
  }
64084
64139
  if (lowered === "attach") {
@@ -64123,6 +64178,10 @@ function parseAgentCommand(text, options = {}) {
64123
64178
  if (lowered === "followup") {
64124
64179
  return parseFollowUpSlashCommand(withoutSlash.slice(command.length).trim().toLowerCase());
64125
64180
  }
64181
+ if (lowered === "mention") {
64182
+ const scope = withoutSlash.slice(command.length).trim().toLowerCase();
64183
+ return parseFollowUpSlashCommand(scope ? `mention-only ${scope}` : "mention-only");
64184
+ }
64126
64185
  if (lowered === "pause") {
64127
64186
  return parseFollowUpSlashCommand("pause");
64128
64187
  }
@@ -64339,7 +64398,8 @@ function renderAgentControlSlashHelp() {
64339
64398
  "- `/status`: show the current route status and operator setup commands",
64340
64399
  "- `/help`: show available control slash commands",
64341
64400
  "- `/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",
64401
+ "- `/transcript`: show a short recent session snapshot when the route verbose policy allows it",
64402
+ "- `/transcript full`: show a longer session snapshot when you need the full pane context",
64343
64403
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
64344
64404
  "- `/detach`: stop live updates for this thread while still posting the final result here",
64345
64405
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
@@ -64347,11 +64407,11 @@ function renderAgentControlSlashHelp() {
64347
64407
  "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
64348
64408
  "- `/followup status`: show the current conversation follow-up policy",
64349
64409
  "- `/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`",
64410
+ "- `/followup mention-only` or `/mention`: require explicit mention for each later turn",
64411
+ "- `/followup mention-only channel` or `/mention channel`: persist mention-only as the default for the current channel or group",
64412
+ "- `/followup mention-only all` or `/mention all`: persist mention-only as the default for all routed conversations on this bot",
64413
+ "- `/followup pause` or `/pause`: stop passive follow-up until the next explicit mention",
64414
+ "- `/followup resume` or `/resume`: clear the runtime override and restore config defaults",
64355
64415
  "- `/streaming status|on|off|latest|all`: show or change streaming mode for this surface",
64356
64416
  "- `/responsemode status`: show the configured response mode for this surface",
64357
64417
  "- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
@@ -64401,31 +64461,44 @@ function parseWatchCommand(raw) {
64401
64461
  durationMs: parsedDurationMs ?? undefined
64402
64462
  };
64403
64463
  }
64464
+ function parseFollowUpScope(raw) {
64465
+ if (raw === "channel") {
64466
+ return "channel";
64467
+ }
64468
+ if (raw === "all") {
64469
+ return "all";
64470
+ }
64471
+ return "conversation";
64472
+ }
64404
64473
  function parseFollowUpSlashCommand(action) {
64405
- if (!action || action === "status") {
64474
+ const [rawAction = "", rawScope = ""] = action.split(/\s+/, 2).map((token) => token.trim().toLowerCase());
64475
+ const scope = parseFollowUpScope(rawScope);
64476
+ if (!rawAction || rawAction === "status") {
64406
64477
  return {
64407
64478
  type: "control",
64408
64479
  name: "followup",
64409
64480
  action: "status"
64410
64481
  };
64411
64482
  }
64412
- if (action === "auto") {
64483
+ if (rawAction === "auto") {
64413
64484
  return {
64414
64485
  type: "control",
64415
64486
  name: "followup",
64416
64487
  action: "auto",
64417
- mode: "auto"
64488
+ mode: "auto",
64489
+ scope
64418
64490
  };
64419
64491
  }
64420
- if (action === "mention-only") {
64492
+ if (rawAction === "mention-only") {
64421
64493
  return {
64422
64494
  type: "control",
64423
64495
  name: "followup",
64424
64496
  action: "mention-only",
64425
- mode: "mention-only"
64497
+ mode: "mention-only",
64498
+ scope
64426
64499
  };
64427
64500
  }
64428
- if (action === "pause") {
64501
+ if (rawAction === "pause") {
64429
64502
  return {
64430
64503
  type: "control",
64431
64504
  name: "followup",
@@ -64433,7 +64506,7 @@ function parseFollowUpSlashCommand(action) {
64433
64506
  mode: "paused"
64434
64507
  };
64435
64508
  }
64436
- if (action === "resume") {
64509
+ if (rawAction === "resume") {
64437
64510
  return {
64438
64511
  type: "control",
64439
64512
  name: "followup",
@@ -65126,7 +65199,7 @@ var REPLY_COMMAND = `{{reply_command_base}}
65126
65199
  <user-facing reply>
65127
65200
  __CLISBOT_MESSAGE__
65128
65201
  )" \\
65129
- [--media /absolute/path/to/file]`;
65202
+ [--file /absolute/path/to/file]`;
65130
65203
  var REPLY_RULES = `When replying to the user:
65131
65204
  - put the user-facing message inside the --message body of that command
65132
65205
  {{progress_rules_block}}- {{final_rule_line}}`;
@@ -65159,8 +65232,10 @@ var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
65159
65232
  {{thread_clause}} --input md \\
65160
65233
  --render native \\
65161
65234
  `;
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.";
65235
+ var SLACK_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
65236
+ Keep each paragraph, list, or code block under 2500 chars.`;
65237
+ var TELEGRAM_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
65238
+ Keep the Markdown body under 3000 chars.`;
65164
65239
  var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
65165
65240
  var EMPTY_ACCOUNT_CLAUSE = "";
65166
65241
  var SLACK_THREAD_CLAUSE = " --thread-id {{thread_ts}} \\\n";
@@ -66706,6 +66781,20 @@ function renderSlackTranscript(params) {
66706
66781
  ${body}
66707
66782
  \`\`\``;
66708
66783
  }
66784
+ function renderCompactChannelTranscript(params) {
66785
+ const body = escapeCodeFence(truncateTail(params.snapshot || "(no tmux output yet)", params.maxChars));
66786
+ const fullCommand = params.fullCommand ?? "/transcript full";
66787
+ return [
66788
+ "Transcript",
66789
+ "",
66790
+ "Recent session snapshot:",
66791
+ "```",
66792
+ body,
66793
+ "```",
66794
+ `Use \`${fullCommand}\` if you want the longer pane snapshot.`
66795
+ ].join(`
66796
+ `);
66797
+ }
66709
66798
  var renderSlackSnapshot = renderSlackTranscript;
66710
66799
  var renderChannelSnapshot = renderSlackSnapshot;
66711
66800
  function resolveDetachedInteractionNote(params) {
@@ -66742,12 +66831,16 @@ var PASTE_SETTLE_POLL_INTERVAL_MS = 40;
66742
66831
  var PASTE_SETTLE_QUIET_WINDOW_MS = 60;
66743
66832
  var PASTE_SETTLE_MULTILINE_MAX_WAIT_MS = 800;
66744
66833
  var PASTE_SETTLE_SINGLE_LINE_MAX_WAIT_MS = 80;
66834
+ var PASTE_CONFIRM_MAX_ATTEMPTS = 3;
66745
66835
  var PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS = 40;
66746
66836
  var PASTE_CAPTURE_REVALIDATE_MAX_WAIT_MS = 160;
66747
66837
  var SUBMIT_CONFIRM_POLL_INTERVAL_MS = 40;
66748
66838
  var SUBMIT_CONFIRM_MAX_WAIT_MS = 160;
66749
66839
  var SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS = 40;
66750
66840
  var SUBMIT_SNAPSHOT_CONFIRM_MAX_WAIT_MS = 320;
66841
+ var POST_STATUS_SETTLE_POLL_INTERVAL_MS = 40;
66842
+ var POST_STATUS_SETTLE_QUIET_WINDOW_MS = 80;
66843
+ var POST_STATUS_SETTLE_MAX_WAIT_MS = 240;
66751
66844
  var TMUX_MISSING_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
66752
66845
  var TMUX_MISSING_SESSION_PATTERN = /(?:can't find session:|no server running on )/i;
66753
66846
  var TMUX_SERVER_UNAVAILABLE_PATTERN = /(?:No such file or directory|error connecting to|failed to connect to server)/i;
@@ -66760,38 +66853,44 @@ class TmuxBootstrapSessionLostError extends Error {
66760
66853
  this.name = "TmuxBootstrapSessionLostError";
66761
66854
  }
66762
66855
  }
66856
+
66857
+ class TmuxPasteUnconfirmedError extends Error {
66858
+ attempts;
66859
+ constructor(attempts) {
66860
+ 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.`);
66861
+ this.attempts = attempts;
66862
+ this.name = "TmuxPasteUnconfirmedError";
66863
+ }
66864
+ }
66865
+
66866
+ class TmuxSubmitUnconfirmedError extends Error {
66867
+ constructor() {
66868
+ super("tmux submit was not confirmed after Enter. The pane state did not change, so clisbot did not treat the prompt as truthfully submitted.");
66869
+ this.name = "TmuxSubmitUnconfirmedError";
66870
+ }
66871
+ }
66763
66872
  async function submitTmuxSessionInput(params) {
66764
66873
  const prePasteState = await params.tmux.getPaneState(params.sessionName);
66765
66874
  const captureLines = estimatePasteCaptureLines(params.text);
66766
66875
  const prePasteSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
66767
- await params.tmux.sendLiteral(params.sessionName, params.text);
66768
- const pasteSettlement = await waitForPanePasteSettlement({
66876
+ const pasteDelivery = await deliverTmuxPasteWithConfirmation({
66769
66877
  tmux: params.tmux,
66770
66878
  sessionName: params.sessionName,
66771
- baseline: prePasteState,
66772
66879
  text: params.text,
66773
- minDelayMs: params.promptSubmitDelayMs
66880
+ baselineState: prePasteState,
66881
+ baselineSnapshot: prePasteSnapshot,
66882
+ captureLines,
66883
+ promptSubmitDelayMs: params.promptSubmitDelayMs,
66884
+ timingContext: params.timingContext
66774
66885
  });
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,
66886
+ if (!pasteDelivery.confirmed) {
66887
+ logLatencyDebug("tmux-paste-unconfirmed", params.timingContext, {
66782
66888
  sessionName: params.sessionName,
66783
- baselineSnapshot: prePasteSnapshot,
66784
- captureLines
66889
+ attempts: pasteDelivery.attempts
66785
66890
  });
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
- }
66891
+ throw new TmuxPasteUnconfirmedError(pasteDelivery.attempts);
66794
66892
  }
66893
+ const preSubmitState = pasteDelivery.state;
66795
66894
  await params.tmux.sendKey(params.sessionName, "Enter");
66796
66895
  if (await waitForPaneSubmitConfirmation({
66797
66896
  tmux: params.tmux,
@@ -66815,13 +66914,10 @@ async function submitTmuxSessionInput(params) {
66815
66914
  })) {
66816
66915
  return;
66817
66916
  }
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
66917
  logLatencyDebug("tmux-submit-unconfirmed", params.timingContext, {
66822
66918
  sessionName: params.sessionName
66823
66919
  });
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.");
66920
+ throw new TmuxSubmitUnconfirmedError;
66825
66921
  }
66826
66922
  async function captureTmuxSessionIdentity(params) {
66827
66923
  await submitTmuxSessionInput({
@@ -66864,6 +66960,14 @@ async function captureTmuxSessionIdentity(params) {
66864
66960
  }
66865
66961
  const sessionId = extractSessionId(snapshot, params.pattern);
66866
66962
  if (sessionId) {
66963
+ await waitForTmuxPaneSettle({
66964
+ tmux: params.tmux,
66965
+ sessionName: params.sessionName,
66966
+ captureLines: params.captureLines,
66967
+ pollIntervalMs: POST_STATUS_SETTLE_POLL_INTERVAL_MS,
66968
+ quietWindowMs: POST_STATUS_SETTLE_QUIET_WINDOW_MS,
66969
+ maxWaitMs: POST_STATUS_SETTLE_MAX_WAIT_MS
66970
+ });
66867
66971
  return sessionId;
66868
66972
  }
66869
66973
  }
@@ -67018,6 +67122,49 @@ async function waitForPaneSubmitSnapshotConfirmation(params) {
67018
67122
  await sleep(Math.min(SUBMIT_SNAPSHOT_CONFIRM_POLL_INTERVAL_MS, remainingMs));
67019
67123
  }
67020
67124
  }
67125
+ async function deliverTmuxPasteWithConfirmation(params) {
67126
+ for (let attempt = 1;attempt <= PASTE_CONFIRM_MAX_ATTEMPTS; attempt += 1) {
67127
+ if (attempt > 1) {
67128
+ logLatencyDebug("tmux-paste-retry", params.timingContext, {
67129
+ sessionName: params.sessionName,
67130
+ attempt
67131
+ });
67132
+ }
67133
+ await params.tmux.sendLiteral(params.sessionName, params.text);
67134
+ const pasteSettlement = await waitForPanePasteSettlement({
67135
+ tmux: params.tmux,
67136
+ sessionName: params.sessionName,
67137
+ baseline: params.baselineState,
67138
+ text: params.text,
67139
+ minDelayMs: params.promptSubmitDelayMs
67140
+ });
67141
+ if (pasteSettlement.visible) {
67142
+ return {
67143
+ confirmed: true,
67144
+ state: pasteSettlement.state,
67145
+ attempts: attempt
67146
+ };
67147
+ }
67148
+ const snapshotConfirmed = await waitForPanePasteSnapshotConfirmation({
67149
+ tmux: params.tmux,
67150
+ sessionName: params.sessionName,
67151
+ baselineSnapshot: params.baselineSnapshot,
67152
+ captureLines: params.captureLines
67153
+ });
67154
+ if (snapshotConfirmed) {
67155
+ return {
67156
+ confirmed: true,
67157
+ state: await params.tmux.getPaneState(params.sessionName),
67158
+ attempts: attempt
67159
+ };
67160
+ }
67161
+ }
67162
+ return {
67163
+ confirmed: false,
67164
+ state: params.baselineState,
67165
+ attempts: PASTE_CONFIRM_MAX_ATTEMPTS
67166
+ };
67167
+ }
67021
67168
  async function waitForPanePasteSettlement(params) {
67022
67169
  await sleep(params.minDelayMs);
67023
67170
  let currentState = await params.tmux.getPaneState(params.sessionName);
@@ -67063,6 +67210,41 @@ async function waitForPanePasteSnapshotConfirmation(params) {
67063
67210
  await sleep(Math.min(PASTE_CAPTURE_REVALIDATE_POLL_INTERVAL_MS, remainingMs));
67064
67211
  }
67065
67212
  }
67213
+ async function waitForTmuxPaneSettle(params) {
67214
+ let previousSnapshot = "";
67215
+ let previousState = null;
67216
+ let lastChangeAt = Date.now();
67217
+ const deadline = Date.now() + params.maxWaitMs;
67218
+ while (true) {
67219
+ let snapshot = "";
67220
+ let state;
67221
+ try {
67222
+ snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
67223
+ state = await params.tmux.getPaneState(params.sessionName);
67224
+ } catch (error) {
67225
+ if (isRetryableBootstrapTargetError(error)) {
67226
+ if (Date.now() >= deadline) {
67227
+ return;
67228
+ }
67229
+ await sleep(params.pollIntervalMs);
67230
+ continue;
67231
+ }
67232
+ if (isBootstrapSessionGoneError(error)) {
67233
+ throw buildBootstrapSessionLostError(params.sessionName, error);
67234
+ }
67235
+ throw error;
67236
+ }
67237
+ if (snapshot !== previousSnapshot || !previousState || !arePaneStatesEqual(previousState, state)) {
67238
+ previousSnapshot = snapshot;
67239
+ previousState = state;
67240
+ lastChangeAt = Date.now();
67241
+ }
67242
+ if (Date.now() - lastChangeAt >= params.quietWindowMs || Date.now() >= deadline) {
67243
+ return;
67244
+ }
67245
+ await sleep(params.pollIntervalMs);
67246
+ }
67247
+ }
67066
67248
  function estimatePasteCaptureLines(text) {
67067
67249
  return Math.max(40, Math.min(160, text.split(`
67068
67250
  `).length + 24));
@@ -67284,6 +67466,9 @@ function isBootstrapSessionLostError(error) {
67284
67466
  function isRecoverableStartupSessionLoss(error) {
67285
67467
  return isMissingTmuxSessionError(error) || isTmuxServerUnavailableError(error) || isBootstrapSessionLostError(error);
67286
67468
  }
67469
+ function isFreshStartRetryablePromptDeliveryError(error) {
67470
+ return error instanceof TmuxPasteUnconfirmedError;
67471
+ }
67287
67472
 
67288
67473
  class RunnerService {
67289
67474
  loadedConfig;
@@ -67398,7 +67583,7 @@ class RunnerService {
67398
67583
  });
67399
67584
  }
67400
67585
  async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
67401
- if (!isRecoverableStartupSessionLoss(error)) {
67586
+ if (!isRecoverableStartupSessionLoss(error) && !isFreshStartRetryablePromptDeliveryError(error)) {
67402
67587
  return null;
67403
67588
  }
67404
67589
  return this.retryFreshStartWithClearedSessionId(target, resolved, remainingFreshRetries);
@@ -67640,6 +67825,9 @@ class RunnerService {
67640
67825
  canRecoverMidRun(error) {
67641
67826
  return isRecoverableStartupSessionLoss(error);
67642
67827
  }
67828
+ canRetryPromptAfterFreshStart(error) {
67829
+ return isFreshStartRetryablePromptDeliveryError(error);
67830
+ }
67643
67831
  async reopenRunContext(target, timingContext) {
67644
67832
  const resolved = this.resolveTarget(target);
67645
67833
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
@@ -67654,7 +67842,10 @@ class RunnerService {
67654
67842
  return;
67655
67843
  });
67656
67844
  await this.sessionState.clearSessionIdEntry(resolved, { runnerCommand: resolved.runner.command });
67657
- return this.ensureSessionReady(target, { allowFreshRetry: false, timingContext });
67845
+ return this.ensureRunnerReady(target, {
67846
+ allowFreshRetryBeforePrompt: false,
67847
+ timingContext
67848
+ });
67658
67849
  }
67659
67850
  async captureTranscript(target) {
67660
67851
  const resolved = this.resolveTarget(target);
@@ -68295,6 +68486,55 @@ class SessionService {
68295
68486
  }
68296
68487
  this.activeRuns.delete(run.resolved.sessionKey);
68297
68488
  }
68489
+ async recoverPromptDeliveryFailure(sessionKey, params, error) {
68490
+ if (!params.prompt || params.promptRetryAttempt || !this.runnerSessions.canRetryPromptAfterFreshStart(error)) {
68491
+ return false;
68492
+ }
68493
+ const run = this.getRun(sessionKey, params.runId);
68494
+ if (!run) {
68495
+ return true;
68496
+ }
68497
+ const target = {
68498
+ agentId: run.resolved.agentId,
68499
+ sessionKey: run.resolved.sessionKey
68500
+ };
68501
+ 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.");
68502
+ try {
68503
+ const fresh = await this.runnerSessions.startFreshSession(target, params.timingContext);
68504
+ const currentRun = this.getRun(sessionKey, params.runId);
68505
+ if (!currentRun) {
68506
+ return true;
68507
+ }
68508
+ const restartedAt = Date.now();
68509
+ currentRun.resolved = fresh.resolved;
68510
+ currentRun.steeringReady = false;
68511
+ currentRun.startedAt = restartedAt;
68512
+ currentRun.latestUpdate = this.createRunUpdate({
68513
+ resolved: currentRun.resolved,
68514
+ status: currentRun.latestUpdate.status === "detached" ? "detached" : "running",
68515
+ snapshot: "",
68516
+ fullSnapshot: fresh.initialSnapshot,
68517
+ initialSnapshot: fresh.initialSnapshot,
68518
+ note: "Retrying the prompt in one fresh runner session.",
68519
+ forceVisible: true
68520
+ });
68521
+ await this.sessionState.setSessionRuntime(currentRun.resolved, {
68522
+ state: "running",
68523
+ startedAt: restartedAt
68524
+ });
68525
+ await this.notifyRunObservers(currentRun, currentRun.latestUpdate);
68526
+ this.startRunMonitor(sessionKey, {
68527
+ ...params,
68528
+ promptRetryAttempt: 1,
68529
+ initialSnapshot: fresh.initialSnapshot,
68530
+ startedAt: restartedAt
68531
+ });
68532
+ return true;
68533
+ } catch (freshError) {
68534
+ await this.failActiveRun(sessionKey, run.runId, await this.runnerSessions.mapRunError(freshError, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
68535
+ return true;
68536
+ }
68537
+ }
68298
68538
  async recoverLostMidRun(sessionKey, params, error) {
68299
68539
  if (!this.runnerSessions.canRecoverMidRun(error)) {
68300
68540
  return false;
@@ -68450,6 +68690,16 @@ class SessionService {
68450
68690
  }
68451
68691
  });
68452
68692
  } catch (error) {
68693
+ if (await this.recoverPromptDeliveryFailure(sessionKey, {
68694
+ runId: params.runId,
68695
+ prompt: params.prompt,
68696
+ startedAt: params.startedAt,
68697
+ detachedAlready: params.detachedAlready,
68698
+ timingContext: params.timingContext,
68699
+ promptRetryAttempt: params.promptRetryAttempt
68700
+ }, error)) {
68701
+ return;
68702
+ }
68453
68703
  if (await this.recoverLostMidRun(sessionKey, {
68454
68704
  runId: params.runId,
68455
68705
  timingContext: params.timingContext,
@@ -69331,43 +69581,188 @@ async function setConversationStreaming(params) {
69331
69581
  };
69332
69582
  }
69333
69583
 
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
- `);
69351
- }
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
- `);
69584
+ // src/channels/follow-up-mode-config.ts
69585
+ function getEditableConfigPath6() {
69586
+ return process.env.CLISBOT_CONFIG_PATH;
69358
69587
  }
69359
- function renderPrincipalFormat(identity) {
69360
- if (identity.platform === "slack") {
69361
- return "slack:<nativeUserId>";
69588
+ function getOrCreateFollowUp(source) {
69589
+ const existing = source.followUp;
69590
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
69591
+ return existing;
69362
69592
  }
69363
- return "telegram:<nativeUserId>";
69593
+ const created = {};
69594
+ source.followUp = created;
69595
+ return created;
69364
69596
  }
69365
- function renderPrincipalExample(identity) {
69366
- if (identity.senderId) {
69367
- return `${identity.platform}:${identity.senderId}`;
69368
- }
69369
- if (identity.platform === "slack") {
69370
- return "slack:U123ABC456";
69597
+ function createTelegramRouteOverride2() {
69598
+ return {
69599
+ enabled: true,
69600
+ allowUsers: [],
69601
+ blockUsers: [],
69602
+ topics: {}
69603
+ };
69604
+ }
69605
+ function getOrCreateTelegramGroupRoute2(bot, chatId) {
69606
+ const existingGroup = bot.groups[chatId];
69607
+ if (existingGroup) {
69608
+ return existingGroup;
69609
+ }
69610
+ const createdGroup = createTelegramRouteOverride2();
69611
+ bot.groups[chatId] = createdGroup;
69612
+ return createdGroup;
69613
+ }
69614
+ function resolveSlackFollowUpModeTarget(config, params) {
69615
+ const botId = resolveSlackBotId(config.bots.slack, params.botId);
69616
+ const bot = getSlackBotRecord(config.bots.slack, botId);
69617
+ if (!bot) {
69618
+ throw new Error(`Unknown Slack bot: ${botId}`);
69619
+ }
69620
+ if (params.scope === "all") {
69621
+ return {
69622
+ get: () => bot.followUp?.mode,
69623
+ set: (value) => {
69624
+ getOrCreateFollowUp(bot).mode = value;
69625
+ },
69626
+ label: `slack bot ${botId}`
69627
+ };
69628
+ }
69629
+ if (params.identity.conversationKind === "dm") {
69630
+ const targetId = params.identity.senderId?.trim() || params.identity.channelId?.trim();
69631
+ if (!targetId) {
69632
+ throw new Error("Slack follow-up channel scope requires a senderId or channelId.");
69633
+ }
69634
+ const routeKey2 = `dm:${targetId}`;
69635
+ const existingRoute = resolveDirectMessageExactRoute(bot.directMessages, targetId) ?? (bot.directMessages[routeKey2] = createDirectMessageBehaviorOverride());
69636
+ return {
69637
+ get: () => existingRoute.followUp?.mode ?? bot.followUp?.mode,
69638
+ set: (value) => {
69639
+ getOrCreateFollowUp(existingRoute).mode = value;
69640
+ },
69641
+ label: `slack ${routeKey2}`
69642
+ };
69643
+ }
69644
+ const routeKind = params.identity.conversationKind === "group" ? "group" : "channel";
69645
+ const channelId = params.identity.channelId?.trim();
69646
+ if (!channelId) {
69647
+ throw new Error("Slack follow-up channel scope requires a channelId.");
69648
+ }
69649
+ const routeKey = `${routeKind}:${channelId}`;
69650
+ const route = bot.groups[routeKey];
69651
+ if (!route) {
69652
+ throw new Error(`Route not configured yet: slack ${routeKey}. Add the route first.`);
69653
+ }
69654
+ return {
69655
+ get: () => route.followUp?.mode ?? bot.followUp?.mode,
69656
+ set: (value) => {
69657
+ getOrCreateFollowUp(route).mode = value;
69658
+ },
69659
+ label: `slack ${routeKey}`
69660
+ };
69661
+ }
69662
+ function resolveTelegramFollowUpModeTarget(config, params) {
69663
+ const botId = resolveTelegramBotId(config.bots.telegram, params.botId);
69664
+ const bot = getTelegramBotRecord(config.bots.telegram, botId);
69665
+ if (!bot) {
69666
+ throw new Error(`Unknown Telegram bot: ${botId}`);
69667
+ }
69668
+ if (params.scope === "all") {
69669
+ return {
69670
+ get: () => bot.followUp?.mode,
69671
+ set: (value) => {
69672
+ getOrCreateFollowUp(bot).mode = value;
69673
+ },
69674
+ label: `telegram bot ${botId}`
69675
+ };
69676
+ }
69677
+ if (params.identity.conversationKind === "dm") {
69678
+ const targetId = params.identity.senderId?.trim() || params.identity.chatId?.trim();
69679
+ if (!targetId) {
69680
+ throw new Error("Telegram follow-up channel scope requires a senderId or chatId.");
69681
+ }
69682
+ const routeKey = `dm:${targetId}`;
69683
+ const existingRoute = resolveDirectMessageExactRoute(bot.directMessages, targetId) ?? (bot.directMessages[routeKey] = createDirectMessageBehaviorOverride());
69684
+ return {
69685
+ get: () => existingRoute.followUp?.mode ?? bot.followUp?.mode,
69686
+ set: (value) => {
69687
+ getOrCreateFollowUp(existingRoute).mode = value;
69688
+ },
69689
+ label: `telegram ${routeKey}`
69690
+ };
69691
+ }
69692
+ const chatId = params.identity.chatId?.trim();
69693
+ if (!chatId) {
69694
+ throw new Error("Telegram follow-up channel scope requires a chatId.");
69695
+ }
69696
+ const group = getOrCreateTelegramGroupRoute2(bot, chatId);
69697
+ return {
69698
+ get: () => group.followUp?.mode ?? bot.followUp?.mode,
69699
+ set: (value) => {
69700
+ getOrCreateFollowUp(group).mode = value;
69701
+ },
69702
+ label: `telegram group:${chatId}`
69703
+ };
69704
+ }
69705
+ function resolveConfiguredFollowUpModeTarget(config, params) {
69706
+ if (params.channel === "slack") {
69707
+ return resolveSlackFollowUpModeTarget(config, params);
69708
+ }
69709
+ return resolveTelegramFollowUpModeTarget(config, params);
69710
+ }
69711
+ async function setScopedConversationFollowUpMode(params) {
69712
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
69713
+ const target = resolveConfiguredFollowUpModeTarget(config, {
69714
+ channel: params.identity.platform,
69715
+ botId: resolveChannelIdentityBotId(params.identity),
69716
+ scope: params.scope,
69717
+ identity: params.identity
69718
+ });
69719
+ target.set(params.mode);
69720
+ await writeEditableConfig(configPath, config);
69721
+ return {
69722
+ configPath,
69723
+ label: target.label,
69724
+ followUpMode: params.mode
69725
+ };
69726
+ }
69727
+
69728
+ // src/channels/interaction-processing.ts
69729
+ var MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS = 3000;
69730
+ var MESSAGE_TOOL_FINAL_GRACE_POLL_MS = 100;
69731
+ var MESSAGE_TOOL_PREVIEW_SIGNAL_POLL_MS = 100;
69732
+ var TRANSCRIPT_PREVIEW_MAX_CHARS = 1200;
69733
+ function renderSensitiveCommandDisabledMessage() {
69734
+ return [
69735
+ "Shell execution is not allowed for your current role on this agent.",
69736
+ "Ask an app or agent admin to grant `shellExecute` if this surface should allow `/bash`."
69737
+ ].join(`
69738
+ `);
69739
+ }
69740
+ function renderTranscriptDisabledMessage() {
69741
+ return [
69742
+ "Transcript inspection is disabled for this route.",
69743
+ 'Set `verbose: "minimal"` on the route or channel to allow `/transcript`.'
69744
+ ].join(`
69745
+ `);
69746
+ }
69747
+ function renderStartupSteeringUnavailableMessage() {
69748
+ return [
69749
+ "The active run is still starting and cannot accept steering input yet.",
69750
+ "Send a normal follow-up message to keep it ordered behind the first prompt, or wait until startup finishes before using `/steer`."
69751
+ ].join(`
69752
+ `);
69753
+ }
69754
+ function renderPrincipalFormat(identity) {
69755
+ if (identity.platform === "slack") {
69756
+ return "slack:<nativeUserId>";
69757
+ }
69758
+ return "telegram:<nativeUserId>";
69759
+ }
69760
+ function renderPrincipalExample(identity) {
69761
+ if (identity.senderId) {
69762
+ return `${identity.platform}:${identity.senderId}`;
69763
+ }
69764
+ if (identity.platform === "slack") {
69765
+ return "slack:U123ABC456";
69371
69766
  }
69372
69767
  return "telegram:1276408333";
69373
69768
  }
@@ -69439,7 +69834,7 @@ function renderRouteStatusMessage(params) {
69439
69834
  lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
69440
69835
  }
69441
69836
  }
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`");
69837
+ 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
69838
  return lines.join(`
69444
69839
  `);
69445
69840
  }
@@ -69486,6 +69881,29 @@ function renderAdditionalMessageModeStatusMessage(params) {
69486
69881
  return lines.join(`
69487
69882
  `);
69488
69883
  }
69884
+ function renderFollowUpModeUpdateMessage(params) {
69885
+ if (!params.persisted) {
69886
+ if (params.mode === "paused") {
69887
+ return "Follow-up paused for this conversation until the next explicit mention.";
69888
+ }
69889
+ return `Follow-up mode set to \`${params.mode}\` for this conversation.`;
69890
+ }
69891
+ const lines = [
69892
+ `Updated follow-up mode for \`${params.persisted.label}\`.`,
69893
+ `config.followUp.mode: \`${params.persisted.followUpMode}\``,
69894
+ `config: \`${params.persisted.configPath}\``,
69895
+ `currentConversation.overrideMode: \`${params.mode}\``,
69896
+ "The current conversation changes immediately.",
69897
+ "If config reload is enabled, the broader default should apply automatically shortly."
69898
+ ];
69899
+ if (params.scope === "all") {
69900
+ lines.splice(4, 0, "This persists the bot-wide default for later routed conversations on this bot.");
69901
+ } else if (params.scope === "channel") {
69902
+ lines.splice(4, 0, "This persists the default for the current channel, group, or DM container.");
69903
+ }
69904
+ return lines.join(`
69905
+ `);
69906
+ }
69489
69907
  function buildChannelObserverId(identity) {
69490
69908
  return [
69491
69909
  identity.platform,
@@ -70132,14 +70550,18 @@ async function processChannelInteraction(params) {
70132
70550
  return interactionResult;
70133
70551
  }
70134
70552
  const transcript = await params.agentService.captureTranscript(params.sessionTarget);
70135
- await params.postText(renderChannelSnapshot({
70553
+ await params.postText(slashCommand.mode === "full" ? renderChannelSnapshot({
70136
70554
  agentId: transcript.agentId,
70137
70555
  sessionName: transcript.sessionName,
70138
70556
  workspacePath: transcript.workspacePath,
70139
70557
  status: "completed",
70140
70558
  snapshot: transcript.snapshot || "(no tmux output yet)",
70141
70559
  maxChars: params.maxChars,
70142
- note: "transcript command"
70560
+ note: "transcript command (full)"
70561
+ }) : renderCompactChannelTranscript({
70562
+ snapshot: transcript.snapshot || "(no tmux output yet)",
70563
+ maxChars: Math.min(params.maxChars, TRANSCRIPT_PREVIEW_MAX_CHARS),
70564
+ fullCommand: "/transcript full"
70143
70565
  }));
70144
70566
  return interactionResult;
70145
70567
  }
@@ -70202,8 +70624,25 @@ async function processChannelInteraction(params) {
70202
70624
  await params.agentService.resetConversationFollowUpMode(params.sessionTarget);
70203
70625
  await params.postText("Follow-up policy reset to route defaults for this conversation.");
70204
70626
  } 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.`);
70627
+ if (slashCommand.scope === "channel" || slashCommand.scope === "all") {
70628
+ await params.agentService.setConversationFollowUpMode(params.sessionTarget, slashCommand.mode);
70629
+ const persisted = await setScopedConversationFollowUpMode({
70630
+ identity: params.identity,
70631
+ scope: slashCommand.scope,
70632
+ mode: slashCommand.mode
70633
+ });
70634
+ await params.postText(renderFollowUpModeUpdateMessage({
70635
+ scope: slashCommand.scope,
70636
+ mode: slashCommand.mode,
70637
+ persisted
70638
+ }));
70639
+ } else {
70640
+ await params.agentService.setConversationFollowUpMode(params.sessionTarget, slashCommand.mode);
70641
+ await params.postText(renderFollowUpModeUpdateMessage({
70642
+ scope: "conversation",
70643
+ mode: slashCommand.mode
70644
+ }));
70645
+ }
70207
70646
  }
70208
70647
  await params.agentService.recordConversationReply(params.sessionTarget);
70209
70648
  return interactionResult;
@@ -71518,7 +71957,7 @@ function renderSlackRouteChoiceMessage(params) {
71518
71957
  `- ${renderCliCommand(`routes add --channel slack channel:${params.channelId} --bot default`, { inline: true })}`,
71519
71958
  `- ${renderCliCommand(`routes set-agent --channel slack channel:${params.channelId} --bot default --agent <id>`, { inline: true })}`,
71520
71959
  "",
71521
- `After that, ${botReference} and send \`\\start\` or \`\\status\` here.`
71960
+ `After that, ${botReference} and send \`\\start\`, \`\\status\`, or \`\\mention\` here.`
71522
71961
  ].join(`
71523
71962
  `);
71524
71963
  }
@@ -71526,7 +71965,7 @@ function renderSlackMentionRequiredMessage(botLabel) {
71526
71965
  const botReference = botLabel?.trim() ? `mention this bot (${botLabel.trim()})` : "mention this bot";
71527
71966
  return [
71528
71967
  "clisbot: this Slack channel requires a bot mention for new commands.",
71529
- `Try ${botReference} and send \`\\start\` or \`\\status\` here.`,
71968
+ `Try ${botReference} and send \`\\status\` or \`\\mention\` here.`,
71530
71969
  "After the bot replies in a thread, normal follow-up messages there can continue according to the follow-up policy."
71531
71970
  ].join(`
71532
71971
  `);
@@ -74302,7 +74741,7 @@ function renderTelegramRouteChoiceMessage(params) {
74302
74741
  if (params.includeConfigPath) {
74303
74742
  lines.push("", topicId != null ? `Config path: \`bots.telegram.default.groups."${chatId}".topics."${topicId}"\`` : `Config path: \`bots.telegram.default.groups."${chatId}"\``);
74304
74743
  } else {
74305
- lines.push("", "After that, routed commands such as `/status`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
74744
+ lines.push("", "After that, routed commands such as `/status`, `/mention`, `/stop`, `/nudge`, `/followup`, and `/bash` will work here.");
74306
74745
  }
74307
74746
  return lines.join(`
74308
74747
  `);
@@ -74368,6 +74807,7 @@ var TELEGRAM_FULL_COMMANDS = [
74368
74807
  { command: "stop", description: "Interrupt current run" },
74369
74808
  { command: "nudge", description: "Send one extra Enter to the session" },
74370
74809
  { command: "followup", description: "Show or change follow-up mode" },
74810
+ { command: "mention", description: "Require explicit mention for later turns" },
74371
74811
  { command: "pause", description: "Pause passive follow-up for this conversation" },
74372
74812
  { command: "resume", description: "Restore route follow-up defaults for this conversation" },
74373
74813
  { command: "streaming", description: "Show or change streaming mode" },
@@ -74379,6 +74819,15 @@ var TELEGRAM_FULL_COMMANDS = [
74379
74819
  { command: "bash", description: "Run bash in the agent workspace" }
74380
74820
  ];
74381
74821
  var TELEGRAM_STARTUP_CONFLICT_MAX_WAIT_MS = 6000;
74822
+ var TELEGRAM_POLLING_CONFLICT_BACKOFF_MAX_DELAY_MS = 30000;
74823
+ var TELEGRAM_POLLING_CONFLICT_SLEEP_SLICE_MS = 250;
74824
+ var TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_DELAY_MS = 60000;
74825
+ var TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_REPEAT_MS = 15 * 60000;
74826
+ function computeTelegramPollingConflictBackoffDelayMs(baseDelayMs, attempt) {
74827
+ const safeBaseDelayMs = Math.max(1, baseDelayMs);
74828
+ const boundedAttempt = Math.max(1, attempt);
74829
+ return Math.min(safeBaseDelayMs * 2 ** (boundedAttempt - 1), TELEGRAM_POLLING_CONFLICT_BACKOFF_MAX_DELAY_MS);
74830
+ }
74382
74831
  function renderTelegramUnroutedRouteMessage(params) {
74383
74832
  const lines = params.mode === "whoami" ? [
74384
74833
  "Who am I",
@@ -74460,6 +74909,8 @@ class TelegramPollingService {
74460
74909
  nextUpdateId;
74461
74910
  loopPromise;
74462
74911
  activePollController;
74912
+ pollingConflictActive = false;
74913
+ pollingConflictAttempt = 0;
74463
74914
  inFlightUpdates = new Set;
74464
74915
  processingIndicators = new ConversationProcessingIndicatorCoordinator;
74465
74916
  constructor(loadedConfig, agentService, processedEventsStore, activityStore, botId = "default", botCredentials, reportLifecycle) {
@@ -74509,7 +74960,13 @@ class TelegramPollingService {
74509
74960
  this.botUserId = me.id;
74510
74961
  this.botUsername = me.username ?? "";
74511
74962
  console.log(`telegram bot @${this.botUsername || this.botUserId} (${this.botId})`);
74512
- await this.initializeOffset();
74963
+ try {
74964
+ await this.initializeOffset();
74965
+ } catch (error) {
74966
+ if (!isTelegramPollingConflict(error)) {
74967
+ throw error;
74968
+ }
74969
+ }
74513
74970
  await this.registerCommands();
74514
74971
  this.running = true;
74515
74972
  this.loopPromise = this.pollLoop();
@@ -74549,6 +75006,7 @@ class TelegramPollingService {
74549
75006
  timeoutMs: (telegramConfig.polling.timeoutSeconds + 5) * 1000
74550
75007
  });
74551
75008
  this.activePollController = undefined;
75009
+ await this.recoverFromPollingConflictIfNeeded();
74552
75010
  const dispatched = dispatchTelegramUpdates({
74553
75011
  updates,
74554
75012
  handleUpdate: (update) => this.handleUpdate(update),
@@ -74568,24 +75026,54 @@ class TelegramPollingService {
74568
75026
  return;
74569
75027
  }
74570
75028
  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;
75029
+ await this.handlePollingConflict(error, telegramConfig.polling.retryDelayMs);
75030
+ continue;
74583
75031
  }
74584
75032
  console.error("telegram polling error", error);
74585
75033
  await sleep(telegramConfig.polling.retryDelayMs);
74586
75034
  }
74587
75035
  }
74588
75036
  }
75037
+ async handlePollingConflict(error, retryDelayMs) {
75038
+ this.pollingConflictAttempt += 1;
75039
+ const nextDelayMs = computeTelegramPollingConflictBackoffDelayMs(retryDelayMs, this.pollingConflictAttempt);
75040
+ if (!this.pollingConflictActive) {
75041
+ this.pollingConflictActive = true;
75042
+ await this.reportLifecycle?.({
75043
+ connection: "failed",
75044
+ summary: "Telegram polling is temporarily blocked because another poller is already using this bot token.",
75045
+ detail: error instanceof Error ? error.message : String(error),
75046
+ actions: [
75047
+ "stop the other Telegram poller that is using the same bot token if it is unintended",
75048
+ "clisbot will keep retrying automatically with backoff until Telegram polling can recover"
75049
+ ],
75050
+ ownerAlertAfterMs: TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_DELAY_MS,
75051
+ ownerAlertRepeatMs: TELEGRAM_POLLING_CONFLICT_OWNER_ALERT_REPEAT_MS
75052
+ });
75053
+ console.error("telegram polling blocked: another bot instance is already calling getUpdates for this token; retrying with backoff");
75054
+ }
75055
+ await this.waitForPollingConflictRetryDelay(nextDelayMs);
75056
+ }
75057
+ async recoverFromPollingConflictIfNeeded() {
75058
+ if (!this.pollingConflictActive) {
75059
+ return;
75060
+ }
75061
+ this.pollingConflictActive = false;
75062
+ this.pollingConflictAttempt = 0;
75063
+ await this.reportLifecycle?.({
75064
+ connection: "active",
75065
+ detail: "Telegram polling recovered after a polling-conflict retry."
75066
+ });
75067
+ console.log("telegram polling recovered after polling conflict");
75068
+ }
75069
+ async waitForPollingConflictRetryDelay(delayMs) {
75070
+ let remainingMs = Math.max(0, delayMs);
75071
+ while (this.running && remainingMs > 0) {
75072
+ const sliceMs = Math.min(remainingMs, TELEGRAM_POLLING_CONFLICT_SLEEP_SLICE_MS);
75073
+ await sleep(sliceMs);
75074
+ remainingMs -= sliceMs;
75075
+ }
75076
+ }
74589
75077
  trackInFlightUpdate(task) {
74590
75078
  this.inFlightUpdates.add(task);
74591
75079
  task.finally(() => {
@@ -75449,97 +75937,7 @@ function installRuntimeConsoleTimestamps() {
75449
75937
  };
75450
75938
  }
75451
75939
 
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
- }
75940
+ // src/control/owner-alerts.ts
75543
75941
  function parseOwnerPrincipal(principal) {
75544
75942
  const trimmed = principal.trim();
75545
75943
  if (!trimmed) {
@@ -75581,58 +75979,30 @@ function buildOwnerAlertCommand(params) {
75581
75979
  renderMode: "native"
75582
75980
  };
75583
75981
  }
75982
+ function dedupe(values) {
75983
+ return [...new Set(values.filter(Boolean))];
75984
+ }
75584
75985
  async function sendOwnerAlert(params) {
75585
- const plugins = params.dependencies.listChannelPlugins();
75586
- const loadedByPlatform = new Map;
75986
+ const plugins = params.listChannelPlugins();
75587
75987
  const delivered = [];
75588
75988
  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
75989
  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
- for (const platform of ["slack", "telegram"]) {
75611
- const ownerIds = ownersByPlatform.get(platform) ?? [];
75990
+ const principals = dedupe(params.loadedConfig.raw.app.auth.roles.owner?.users ?? []);
75991
+ const ownerIds = principals.map(parseOwnerPrincipal).filter((entry) => entry?.platform === platform).map((entry) => entry.userId);
75612
75992
  if (ownerIds.length === 0) {
75613
75993
  continue;
75614
75994
  }
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
75995
  const plugin = plugins.find((entry) => entry.id === platform);
75626
- if (!plugin || !plugin.isEnabled(loaded)) {
75996
+ if (!plugin || !plugin.isEnabled(params.loadedConfig)) {
75627
75997
  continue;
75628
75998
  }
75629
- const botIds = dedupe(plugin.listBots(loaded).map((entry) => entry.botId));
75999
+ const botIds = dedupe(plugin.listBots(params.loadedConfig).map((entry) => entry.botId));
75630
76000
  for (const userId of ownerIds) {
75631
76001
  let deliveredToPrincipal = false;
75632
76002
  const principal = `${platform}:${userId}`;
75633
76003
  for (const botId of botIds) {
75634
76004
  try {
75635
- await plugin.runMessageCommand(loaded, buildOwnerAlertCommand({
76005
+ await plugin.runMessageCommand(params.loadedConfig, buildOwnerAlertCommand({
75636
76006
  platform,
75637
76007
  botId,
75638
76008
  userId,
@@ -75661,16 +76031,73 @@ async function sendOwnerAlert(params) {
75661
76031
  failed
75662
76032
  };
75663
76033
  }
76034
+
76035
+ // src/control/runtime-monitor.ts
76036
+ var defaultRuntimeMonitorDependencies = {
76037
+ loadConfig,
76038
+ listChannelPlugins,
76039
+ writePid: async (pidPath, pid = process.pid) => {
76040
+ await ensureDir2(dirname12(pidPath));
76041
+ await writeTextFile(pidPath, `${pid}
76042
+ `);
76043
+ },
76044
+ readState: readRuntimeMonitorState,
76045
+ writeState: writeRuntimeMonitorState,
76046
+ removePid: (pidPath) => rmSync2(pidPath, { force: true }),
76047
+ removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync2(runtimeCredentialsPath, { force: true }),
76048
+ sleep,
76049
+ now: () => Date.now(),
76050
+ spawnChild: (command, args, options) => spawn2(command, args, {
76051
+ stdio: ["ignore", "inherit", "inherit"],
76052
+ env: options.env
76053
+ }),
76054
+ sendSignal: kill
76055
+ };
76056
+ function isProcessAlive(pid) {
76057
+ try {
76058
+ kill(pid, 0);
76059
+ return true;
76060
+ } catch {
76061
+ return false;
76062
+ }
76063
+ }
76064
+ async function readRuntimeMonitorState(statePath = getDefaultRuntimeMonitorStatePath()) {
76065
+ if (!await fileExists(statePath)) {
76066
+ return null;
76067
+ }
76068
+ try {
76069
+ const raw = await readTextFile(statePath);
76070
+ if (!raw.trim()) {
76071
+ return null;
76072
+ }
76073
+ return JSON.parse(raw);
76074
+ } catch {
76075
+ return null;
76076
+ }
76077
+ }
76078
+ async function writeRuntimeMonitorState(statePath, state) {
76079
+ await ensureDir2(dirname12(statePath));
76080
+ await writeTextFile(statePath, `${JSON.stringify(state, null, 2)}
76081
+ `);
76082
+ }
76083
+ function summarizeExit(params) {
76084
+ if (params.signal) {
76085
+ return `signal ${params.signal}`;
76086
+ }
76087
+ return `code ${params.code ?? 0}`;
76088
+ }
75664
76089
  function renderBackoffAlertMessage(params) {
76090
+ const restartLine = params.repeatingFinalStage ? `restart: ${params.restartNumber} (steady-state at final stage; configured ladder ${params.totalConfiguredRestarts})` : `restart: ${params.restartNumber}/${params.totalConfiguredRestarts}`;
76091
+ const stageAttemptLine = params.repeatingFinalStage ? "stage attempt: steady-state retry on final stage" : `stage attempt: ${params.restartAttemptInStage}/${params.stageMaxRestarts}`;
75665
76092
  return [
75666
76093
  "clisbot runtime alert",
75667
76094
  "",
75668
76095
  "status: runtime exited unexpectedly and entered restart backoff",
75669
76096
  `last exit: ${summarizeExit(params.exit)} at ${params.exit.at}`,
75670
76097
  `next restart: ${params.nextRestartAt}`,
75671
- `restart: ${params.restartNumber}/${params.totalRestarts}`,
76098
+ restartLine,
75672
76099
  `stage: ${params.stageIndex + 1}/${params.config.restartBackoff.stages.length}`,
75673
- `stage attempt: ${params.restartAttemptInStage}/${params.stageMaxRestarts}`
76100
+ stageAttemptLine
75674
76101
  ].join(`
75675
76102
  `);
75676
76103
  }
@@ -75680,7 +76107,7 @@ function renderStoppedAlertMessage(params) {
75680
76107
  "",
75681
76108
  "status: runtime stopped after exhausting the configured restart budget",
75682
76109
  `last exit: ${summarizeExit(params.exit)} at ${params.exit.at}`,
75683
- `restart budget used: ${params.totalRestarts}`,
76110
+ `restart budget used: ${params.totalConfiguredRestarts}`,
75684
76111
  `action: inspect ${renderCliCommand("logs", { inline: true })}, fix the fault, then start the service again`
75685
76112
  ].join(`
75686
76113
  `);
@@ -75697,6 +76124,7 @@ class RuntimeMonitor {
75697
76124
  stopRequested = false;
75698
76125
  activeChild = null;
75699
76126
  latestState = null;
76127
+ loadedConfig;
75700
76128
  constructor(scriptPath, configPath, pidPath, statePath, runtimeCredentialsPath, dependencies) {
75701
76129
  this.scriptPath = scriptPath;
75702
76130
  this.configPath = configPath;
@@ -75710,13 +76138,16 @@ class RuntimeMonitor {
75710
76138
  this.registerProcessHandlers();
75711
76139
  try {
75712
76140
  const loadedConfig = await this.dependencies.loadConfig(this.configPath);
76141
+ this.loadedConfig = loadedConfig;
75713
76142
  const monitorConfig = loadedConfig.raw.control.runtimeMonitor;
76143
+ monitorConfig.restartBackoff = normalizeRuntimeMonitorRestartBackoff(monitorConfig.restartBackoff);
75714
76144
  let restartNumber = 0;
75715
- let totalRestarts = monitorConfig.restartBackoff.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
76145
+ let totalConfiguredRestarts = getConfiguredRuntimeMonitorRestartBudget(monitorConfig.restartBackoff);
75716
76146
  await this.writeState({
75717
76147
  phase: "starting"
75718
76148
  });
75719
76149
  while (!this.stopRequested) {
76150
+ const runStartedAt = this.dependencies.now();
75720
76151
  const child = this.dependencies.spawnChild(process.execPath, [this.scriptPath, "serve-foreground"], {
75721
76152
  env: {
75722
76153
  ...process.env,
@@ -75738,11 +76169,14 @@ class RuntimeMonitor {
75738
76169
  break;
75739
76170
  }
75740
76171
  const exitAt = new Date().toISOString();
76172
+ if (this.dependencies.now() - runStartedAt >= RUNTIME_MONITOR_RESTART_RESET_AFTER_MS) {
76173
+ restartNumber = 0;
76174
+ }
75741
76175
  const nextRestartNumber = restartNumber + 1;
75742
- const plan = getRestartPlan(monitorConfig.restartBackoff, nextRestartNumber);
76176
+ const plan = getRuntimeMonitorRestartPlan(monitorConfig.restartBackoff, nextRestartNumber);
75743
76177
  if (!plan) {
75744
76178
  await this.maybeSendAlert("stopped", monitorConfig, renderStoppedAlertMessage({
75745
- totalRestarts,
76179
+ totalConfiguredRestarts,
75746
76180
  exit: {
75747
76181
  code: exit.code,
75748
76182
  signal: exit.signal,
@@ -75762,7 +76196,7 @@ class RuntimeMonitor {
75762
76196
  return;
75763
76197
  }
75764
76198
  restartNumber = nextRestartNumber;
75765
- totalRestarts = plan.totalRestarts;
76199
+ totalConfiguredRestarts = plan.totalConfiguredRestarts;
75766
76200
  const nextRestartAt = new Date(this.dependencies.now() + plan.delayMs).toISOString();
75767
76201
  if (plan.mode === "backoff") {
75768
76202
  await this.maybeSendAlert("backoff", monitorConfig, renderBackoffAlertMessage({
@@ -75771,8 +76205,9 @@ class RuntimeMonitor {
75771
76205
  stageIndex: plan.stageIndex,
75772
76206
  restartAttemptInStage: plan.restartAttemptInStage,
75773
76207
  stageMaxRestarts: plan.stageMaxRestarts,
75774
- totalRestarts,
76208
+ totalConfiguredRestarts,
75775
76209
  nextRestartAt,
76210
+ repeatingFinalStage: plan.repeatingFinalStage,
75776
76211
  exit: {
75777
76212
  code: exit.code,
75778
76213
  signal: exit.signal,
@@ -75873,10 +76308,13 @@ class RuntimeMonitor {
75873
76308
  }
75874
76309
  }
75875
76310
  try {
76311
+ if (!this.loadedConfig) {
76312
+ return;
76313
+ }
75876
76314
  const result = await sendOwnerAlert({
75877
- configPath: this.configPath,
76315
+ loadedConfig: this.loadedConfig,
75878
76316
  message,
75879
- dependencies: this.dependencies
76317
+ listChannelPlugins: this.dependencies.listChannelPlugins
75880
76318
  });
75881
76319
  if (result.delivered.length === 0 && result.failed.length > 0) {
75882
76320
  console.error("clisbot runtime alert delivery failed", result.failed.map((entry) => `${entry.principal}: ${entry.detail}`).join("; "));
@@ -76021,6 +76459,20 @@ function resolveMonitorStatePath(monitorStatePath, configPath, options = {}) {
76021
76459
  }
76022
76460
  return expandHomePath(getDefaultRuntimeMonitorStatePath());
76023
76461
  }
76462
+ function resolveLiveMonitorPid(params) {
76463
+ const processLiveness = params.processLiveness ?? getProcessLiveness;
76464
+ if (params.pidFromFile && processLiveness(params.pidFromFile) === "running") {
76465
+ return params.pidFromFile;
76466
+ }
76467
+ const monitorPid = params.monitorState?.monitorPid;
76468
+ if (monitorPid && processLiveness(monitorPid) === "running") {
76469
+ return monitorPid;
76470
+ }
76471
+ return null;
76472
+ }
76473
+ function resolveKnownMonitorPid(params) {
76474
+ return params.pidFromFile ?? params.monitorState?.monitorPid ?? null;
76475
+ }
76024
76476
  function resolveRuntimeCredentialsPath(runtimeCredentialsPath, configPath, options = {}) {
76025
76477
  if (runtimeCredentialsPath) {
76026
76478
  return expandHomePath(runtimeCredentialsPath);
@@ -76125,11 +76577,18 @@ async function startDetachedRuntime(params) {
76125
76577
  const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath, configPath, { preferConfigSibling });
76126
76578
  const existingPid = await readRuntimePid(pidPath);
76127
76579
  const existingMonitorState = await readRuntimeMonitorState(monitorStatePath);
76128
- if (existingPid && isProcessRunning(existingPid)) {
76580
+ const liveMonitorPid = resolveLiveMonitorPid({
76581
+ pidFromFile: existingPid,
76582
+ monitorState: existingMonitorState
76583
+ });
76584
+ if (liveMonitorPid) {
76585
+ if (existingPid !== liveMonitorPid) {
76586
+ await writeRuntimePid(pidPath, liveMonitorPid);
76587
+ }
76129
76588
  return {
76130
76589
  alreadyRunning: true,
76131
76590
  createdConfig: false,
76132
- pid: existingPid,
76591
+ pid: liveMonitorPid,
76133
76592
  configPath,
76134
76593
  logPath
76135
76594
  };
@@ -76208,10 +76667,14 @@ async function stopDetachedRuntime(params, dependencies = {}) {
76208
76667
  const processLiveness = dependencies.processLiveness ?? getProcessLiveness;
76209
76668
  const sendSignal = dependencies.sendSignal ?? kill2;
76210
76669
  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, {
76670
+ const monitorPid = resolveKnownMonitorPid({
76671
+ pidFromFile: existingPid,
76672
+ monitorState
76673
+ });
76674
+ const monitorLiveness = monitorPid ? processLiveness(monitorPid) : "missing";
76675
+ if (monitorPid && monitorLiveness === "running") {
76676
+ sendSignal(monitorPid, "SIGTERM");
76677
+ const exited = await waitForProcessExit(monitorPid, STOP_WAIT_TIMEOUT_MS, {
76215
76678
  processLiveness,
76216
76679
  sleep: sleepFn
76217
76680
  });
@@ -76219,7 +76682,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
76219
76682
  throw new Error(`clisbot did not stop within ${STOP_WAIT_TIMEOUT_MS}ms`);
76220
76683
  }
76221
76684
  stopped = true;
76222
- } else if (existingPid && existingLiveness === "zombie") {
76685
+ } else if (monitorPid && monitorLiveness === "zombie") {
76223
76686
  stopped = true;
76224
76687
  }
76225
76688
  const runtimePid = monitorState?.runtimePid;
@@ -76235,7 +76698,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
76235
76698
  }
76236
76699
  stopped = true;
76237
76700
  } catch (error) {
76238
- if (!(existingPid && existingLiveness === "running")) {
76701
+ if (!(monitorPid && monitorLiveness === "running")) {
76239
76702
  throw error;
76240
76703
  }
76241
76704
  }
@@ -76293,11 +76756,14 @@ async function getRuntimeStatus(params = {}) {
76293
76756
  preferConfigSibling
76294
76757
  });
76295
76758
  const pid = await readRuntimePid(pidPath);
76296
- const liveness = pid ? getProcessLiveness(pid) : "missing";
76297
76759
  const monitorState = await readRuntimeMonitorState(monitorStatePath);
76760
+ const liveMonitorPid = resolveLiveMonitorPid({
76761
+ pidFromFile: pid,
76762
+ monitorState
76763
+ });
76298
76764
  return {
76299
- running: liveness === "running",
76300
- pid: liveness === "running" && pid ? pid : undefined,
76765
+ running: liveMonitorPid != null,
76766
+ pid: liveMonitorPid ?? undefined,
76301
76767
  configPath,
76302
76768
  pidPath,
76303
76769
  logPath,
@@ -76495,7 +76961,7 @@ function extractLinuxProcState(raw) {
76495
76961
  }
76496
76962
 
76497
76963
  // src/control/bots-cli.ts
76498
- function getEditableConfigPath6() {
76964
+ function getEditableConfigPath7() {
76499
76965
  return process.env.CLISBOT_CONFIG_PATH;
76500
76966
  }
76501
76967
  function parseOptionValue2(args, name) {
@@ -76685,7 +77151,7 @@ function summarizeBotConfig(provider, botId, bot) {
76685
77151
  };
76686
77152
  }
76687
77153
  async function listBots(args) {
76688
- const { config } = await readEditableConfig(getEditableConfigPath6());
77154
+ const { config } = await readEditableConfig(getEditableConfigPath7());
76689
77155
  const provider = parseOptionValue2(args, "--channel");
76690
77156
  const printJson = hasFlag3(args, "--json");
76691
77157
  const summaries = [
@@ -76710,7 +77176,7 @@ async function addOrSetBotCredentials(args, deps, action) {
76710
77176
  const botId = getBotId(args);
76711
77177
  const persist = hasFlag3(args, "--persist");
76712
77178
  const runtimeStatus = await deps.getRuntimeStatus();
76713
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77179
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76714
77180
  const exists = provider === "slack" ? botId in getSlackBots2(config) : (botId in getTelegramBots2(config));
76715
77181
  if (action === "add" && exists) {
76716
77182
  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 +77304,7 @@ async function getBot(args) {
76838
77304
  const provider = parseProvider(args);
76839
77305
  const botId = getBotId(args);
76840
77306
  const printJson = hasFlag3(args, "--json");
76841
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77307
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76842
77308
  const bot = ensureProviderBot(config, provider, botId);
76843
77309
  if (printJson) {
76844
77310
  console.log(JSON.stringify(bot, null, 2));
@@ -76849,7 +77315,7 @@ async function getBot(args) {
76849
77315
  async function setBotEnabled(args, enabled) {
76850
77316
  const provider = parseProvider(args);
76851
77317
  const botId = getBotId(args);
76852
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77318
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76853
77319
  const bot = ensureProviderBot(config, provider, botId);
76854
77320
  bot.enabled = enabled;
76855
77321
  reconcileProviderDefaults(config, provider);
@@ -76860,7 +77326,7 @@ async function setBotEnabled(args, enabled) {
76860
77326
  async function removeBot(args) {
76861
77327
  const provider = parseProvider(args);
76862
77328
  const botId = getBotId(args);
76863
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77329
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76864
77330
  const bot = ensureProviderBot(config, provider, botId);
76865
77331
  const directMessages = "directMessages" in bot ? Object.keys(bot.directMessages ?? {}) : [];
76866
77332
  const groups = "groups" in bot ? Object.keys(bot.groups ?? {}) : [];
@@ -76879,7 +77345,7 @@ async function removeBot(args) {
76879
77345
  }
76880
77346
  async function getOrSetDefaultBot(args, action) {
76881
77347
  const provider = parseProvider(args);
76882
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77348
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76883
77349
  if (action === "get-default") {
76884
77350
  const botId2 = provider === "slack" ? config.bots.slack.defaults.defaultBotId : config.bots.telegram.defaults.defaultBotId;
76885
77351
  console.log(`${provider} default bot: ${botId2}`);
@@ -76904,7 +77370,7 @@ async function getOrSetDefaultBot(args, action) {
76904
77370
  async function getOrSetBotAgent(args, action) {
76905
77371
  const provider = parseProvider(args);
76906
77372
  const botId = getBotId(args);
76907
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77373
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76908
77374
  const bot = ensureProviderBot(config, provider, botId);
76909
77375
  if (action === "get-agent") {
76910
77376
  console.log(`${provider}/${botId} agent: ${bot.agentId ?? "(inherit)"}`);
@@ -76934,7 +77400,7 @@ function ensureDefaultDmRoute(config, provider, botId) {
76934
77400
  async function getOrSetBotPolicy(args, action) {
76935
77401
  const provider = parseProvider(args);
76936
77402
  const botId = getBotId(args);
76937
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77403
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76938
77404
  const bot = ensureProviderBot(config, provider, botId);
76939
77405
  if (action === "get-dm-policy") {
76940
77406
  console.log(`${provider}/${botId} dmPolicy: ${ensureDefaultDmRoute(config, provider, botId).policy ?? "pairing"}`);
@@ -76989,7 +77455,7 @@ async function getOrSetBotPolicy(args, action) {
76989
77455
  async function getCredentialSource(args) {
76990
77456
  const provider = parseProvider(args);
76991
77457
  const botId = getBotId(args);
76992
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
77458
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
76993
77459
  ensureProviderBot(config, provider, botId);
76994
77460
  const source = provider === "slack" ? describeSlackCredentialSource({ config: config.bots.slack, botId }) : describeTelegramCredentialSource({ config: config.bots.telegram, botId });
76995
77461
  console.log(`${provider}/${botId} credentials: ${source.detail}`);
@@ -77580,14 +78046,14 @@ async function getScopedLoopCounts(params) {
77580
78046
 
77581
78047
  // src/control/loops-cli.ts
77582
78048
  var LOOP_BUSY_RETRY_MS = 250;
77583
- function getEditableConfigPath7() {
78049
+ function getEditableConfigPath8() {
77584
78050
  return process.env.CLISBOT_CONFIG_PATH;
77585
78051
  }
77586
78052
  function getSessionState(sessionStorePath) {
77587
78053
  return new AgentSessionState(new SessionStore(sessionStorePath));
77588
78054
  }
77589
78055
  async function loadLoopControlState() {
77590
- const configPath = await ensureEditableConfigFile(getEditableConfigPath7());
78056
+ const configPath = await ensureEditableConfigFile(getEditableConfigPath8());
77591
78057
  const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
77592
78058
  const sessionStorePath = resolveSessionStorePath(loadedConfig);
77593
78059
  return {
@@ -78102,6 +78568,14 @@ function parseOptionValue4(args, name) {
78102
78568
  const values = parseRepeatedOption3(args, name);
78103
78569
  return values.length > 0 ? values.at(-1) : undefined;
78104
78570
  }
78571
+ function parseAliasedOptionValue2(args, preferredName, aliasName) {
78572
+ const preferredValues = parseRepeatedOption3(args, preferredName);
78573
+ const aliasValues = parseRepeatedOption3(args, aliasName);
78574
+ if (preferredValues.length > 0 && aliasValues.length > 0) {
78575
+ throw new Error(`${preferredName} and ${aliasName} are aliases; use only one`);
78576
+ }
78577
+ return preferredValues.at(-1) ?? aliasValues.at(-1);
78578
+ }
78105
78579
  function parseThreadingOptions(args, channel) {
78106
78580
  const threadId = parseOptionValue4(args, "--thread-id");
78107
78581
  const topicId = parseOptionValue4(args, "--topic-id");
@@ -78124,6 +78598,9 @@ function parseMessageBodyFileOption(args) {
78124
78598
  }
78125
78599
  return bodyFileValues.at(-1) ?? messageFileValues.at(-1);
78126
78600
  }
78601
+ function parseMessageAttachmentOption(args) {
78602
+ return parseAliasedOptionValue2(args, "--file", "--media");
78603
+ }
78127
78604
  function parseIntegerOption(args, name) {
78128
78605
  const raw = parseOptionValue4(args, name);
78129
78606
  if (!raw) {
@@ -78165,7 +78642,7 @@ function parseMessageCommand(args) {
78165
78642
  target: parseOptionValue4(rest, "--target"),
78166
78643
  message: parseOptionValue4(rest, "--message") ?? parseOptionValue4(rest, "-m"),
78167
78644
  messageFile: parseMessageBodyFileOption(rest),
78168
- media: parseOptionValue4(rest, "--media"),
78645
+ media: parseMessageAttachmentOption(rest),
78169
78646
  messageId: parseOptionValue4(rest, "--message-id"),
78170
78647
  emoji: parseOptionValue4(rest, "--emoji"),
78171
78648
  remove: hasFlag5(rest, "--remove"),
@@ -78189,7 +78666,7 @@ function renderMessageHelp() {
78189
78666
  renderCliCommand("message"),
78190
78667
  "",
78191
78668
  "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]")}`,
78669
+ ` ${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
78670
  ` ${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
78671
  ` ${renderCliCommand("message react --channel <slack|telegram> --target <dest> --message-id <id> --emoji <emoji> [--account <id>] [--remove]")}`,
78195
78672
  ` ${renderCliCommand("message reactions --channel <slack|telegram> --target <dest> --message-id <id> [--account <id>]")}`,
@@ -78205,6 +78682,8 @@ function renderMessageHelp() {
78205
78682
  " --message <text> Inline message body",
78206
78683
  " --body-file <path> Read the message body from a file",
78207
78684
  " Alias: --message-file (compat only)",
78685
+ " --file <path-or-url> Attach a file or remote URL",
78686
+ " Alias: --media (compat only)",
78208
78687
  " --input <plain|md|html|mrkdwn|blocks>",
78209
78688
  " Input content format. Default: md",
78210
78689
  " --render <native|none|html|mrkdwn|blocks>",
@@ -78221,6 +78700,11 @@ function renderMessageHelp() {
78221
78700
  " html Telegram only",
78222
78701
  " mrkdwn Slack only",
78223
78702
  "",
78703
+ "Length Guidance:",
78704
+ " Telegram native/html Final payload must stay under 4096 chars; leave headroom after HTML-safe rendering",
78705
+ " Slack text/mrkdwn Prefer text under 4000 chars; Slack truncates very long text after 40000",
78706
+ " Slack blocks Max 50 blocks; keep header text under 150 and section text under 3000",
78707
+ "",
78224
78708
  "Threading:",
78225
78709
  " --thread-id <id> Slack thread ts",
78226
78710
  " --topic-id <id> Telegram topic id",
@@ -78291,7 +78775,7 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
78291
78775
  }
78292
78776
 
78293
78777
  // src/control/routes-cli.ts
78294
- function getEditableConfigPath8() {
78778
+ function getEditableConfigPath9() {
78295
78779
  return process.env.CLISBOT_CONFIG_PATH;
78296
78780
  }
78297
78781
  function getSlackBots3(config) {
@@ -78567,7 +79051,7 @@ function renderRoutesHelp() {
78567
79051
  `);
78568
79052
  }
78569
79053
  async function listRoutes(args) {
78570
- const { config } = await readEditableConfig(getEditableConfigPath8());
79054
+ const { config } = await readEditableConfig(getEditableConfigPath9());
78571
79055
  const provider = parseOptionalProvider(args);
78572
79056
  const botIdFilter = parseOptionValue5(args, "--bot");
78573
79057
  const printJson = hasFlag6(args, "--json");
@@ -78628,7 +79112,7 @@ async function addRoute(args) {
78628
79112
  const policy = parseOptionValue5(args, "--policy");
78629
79113
  const requireMention = parseOptionValue5(args, "--require-mention");
78630
79114
  const allowBots = parseOptionValue5(args, "--allow-bots");
78631
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79115
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78632
79116
  const existing = getOrCreateRoute(config, provider, botId, parsed);
78633
79117
  if (existing) {
78634
79118
  throw new Error(`Route already exists: ${provider}/${botId}/${parsed.routeId}. Use a matching \`set-<key>\` command instead.`);
@@ -78652,7 +79136,7 @@ async function getRoute(args) {
78652
79136
  const botId = getBotId2(args);
78653
79137
  const parsed = parseCommandRoute(args, provider);
78654
79138
  const printJson = hasFlag6(args, "--json");
78655
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79139
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78656
79140
  const route = ensureRoute(config, provider, botId, parsed);
78657
79141
  if (printJson) {
78658
79142
  console.log(JSON.stringify(route, null, 2));
@@ -78665,7 +79149,7 @@ async function setRouteEnabled(args, enabled) {
78665
79149
  const botId = getBotId2(args);
78666
79150
  const parsed = parseCommandRoute(args, provider);
78667
79151
  rejectExactDirectMessageAdmissionChange(parsed, enabled ? "Enabling" : "Disabling");
78668
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79152
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78669
79153
  const route = ensureRoute(config, provider, botId, parsed);
78670
79154
  route.enabled = enabled;
78671
79155
  await writeEditableConfig(configPath, config);
@@ -78676,7 +79160,7 @@ async function removeRoute(args) {
78676
79160
  const provider = parseProvider2(args);
78677
79161
  const botId = getBotId2(args);
78678
79162
  const parsed = parseCommandRoute(args, provider);
78679
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79163
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78680
79164
  ensureRoute(config, provider, botId, parsed);
78681
79165
  if (provider === "slack") {
78682
79166
  const bot = ensureSlackBot(config, botId);
@@ -78703,7 +79187,7 @@ async function getSetClearRouteField(args, action) {
78703
79187
  const provider = parseProvider2(args);
78704
79188
  const botId = getBotId2(args);
78705
79189
  const parsed = parseCommandRoute(args, provider);
78706
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79190
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78707
79191
  const route = ensureRoute(config, provider, botId, parsed);
78708
79192
  if (action === "get-agent") {
78709
79193
  console.log(`${provider}/${botId}/${parsed.routeId} agent: ${route.agentId ?? "(inherit)"}`);
@@ -78809,7 +79293,7 @@ async function mutateRouteUsers(args, action) {
78809
79293
  if (!user) {
78810
79294
  throw new Error(renderRoutesHelp());
78811
79295
  }
78812
- const { config, configPath } = await readEditableConfig(getEditableConfigPath8());
79296
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath9());
78813
79297
  const route = parsed.storage === "directMessages" && isDirectMessageWildcardRouteId(parsed.routeId) ? ensureBotDirectMessageWildcardRoute(config, provider, botId) : ensureRoute(config, provider, botId, parsed);
78814
79298
  const field = action.includes("allow") ? "allowUsers" : "blockUsers";
78815
79299
  const current = Array.from(new Set((route[field] ?? []).filter(Boolean)));
@@ -79055,7 +79539,7 @@ function renderRunnerHelp() {
79055
79539
  ` ${renderCliCommand("runner smoke --backend all --suite launch-trio [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]")}`,
79056
79540
  "",
79057
79541
  "Operator session debugging:",
79058
- " - `list` shows current tmux runner sessions, newest admitted turn first when known",
79542
+ " - `list` shows current tmux runner sessions, newest admitted turn first when known, plus stored sessionId/state when available",
79059
79543
  " - `inspect` captures one snapshot from a named tmux session",
79060
79544
  " - `watch --latest` follows the session that most recently admitted a new prompt",
79061
79545
  " - `watch --next` waits for the next newly admitted prompt, then follows that session",
@@ -79255,21 +79739,29 @@ async function runListCli() {
79255
79739
  console.log([
79256
79740
  renderCliCommand("runner list"),
79257
79741
  "",
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
- })
79742
+ ...sessions.map(renderRunnerListSession)
79270
79743
  ].join(`
79271
79744
  `));
79272
79745
  }
79746
+ function renderRunnerListSession(session) {
79747
+ if (!session.entry) {
79748
+ return [
79749
+ `- sessionName: ${session.sessionName}`,
79750
+ " sessionId: none",
79751
+ " state: unmanaged"
79752
+ ].join(`
79753
+ `);
79754
+ }
79755
+ return [
79756
+ `- sessionName: ${session.sessionName}`,
79757
+ ` agent: ${session.entry.agentId}`,
79758
+ ` sessionKey: ${session.entry.sessionKey}`,
79759
+ ` sessionId: ${session.entry.sessionId?.trim() || "none"}`,
79760
+ ` state: ${session.entry.runtime?.state ?? "no-runtime"}`,
79761
+ ` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
79762
+ ].join(`
79763
+ `);
79764
+ }
79273
79765
  async function runInspectCli(args) {
79274
79766
  const options = parseInspectCommand(args);
79275
79767
  const { tmux } = await loadRunnerContext();
@@ -80377,6 +80869,40 @@ class ProcessedEventsStore {
80377
80869
 
80378
80870
  // src/control/runtime-supervisor.ts
80379
80871
  var SERVICE_START_TIMEOUT_MS = 8000;
80872
+ function buildChannelOwnerAlertKey(params) {
80873
+ return `${params.runtimeId}:${params.channel}:${params.botId}`;
80874
+ }
80875
+ function formatElapsedDuration(elapsedMs) {
80876
+ const totalMinutes = Math.max(1, Math.floor(elapsedMs / 60000));
80877
+ if (totalMinutes < 60) {
80878
+ return `${totalMinutes} minute${totalMinutes === 1 ? "" : "s"}`;
80879
+ }
80880
+ const hours = Math.floor(totalMinutes / 60);
80881
+ const minutes = totalMinutes % 60;
80882
+ if (minutes === 0) {
80883
+ return `${hours} hour${hours === 1 ? "" : "s"}`;
80884
+ }
80885
+ return `${hours} hour${hours === 1 ? "" : "s"} ${minutes} minute${minutes === 1 ? "" : "s"}`;
80886
+ }
80887
+ function renderChannelOwnerAlertMessage(params) {
80888
+ const elapsed = formatElapsedDuration(params.elapsedMs);
80889
+ 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}`;
80890
+ return [
80891
+ "clisbot channel alert",
80892
+ "",
80893
+ statusLine,
80894
+ `channel: ${params.channel}/${params.botId}`,
80895
+ ...params.summary ? [`summary: ${params.summary}`] : [],
80896
+ ...params.detail ? [`detail: ${params.detail}`] : [],
80897
+ ...params.incidentState === "resolved" ? [
80898
+ "note: the channel recovered without requiring a runtime restart"
80899
+ ] : [
80900
+ "note: the runtime process is still alive; clisbot is continuing automatic channel-level recovery attempts",
80901
+ `action: inspect ${renderCliCommand("logs", { inline: true })} and fix the channel-level fault or conflicting poller`
80902
+ ]
80903
+ ].join(`
80904
+ `);
80905
+ }
80380
80906
 
80381
80907
  class RuntimeSupervisor {
80382
80908
  configPath;
@@ -80387,6 +80913,7 @@ class RuntimeSupervisor {
80387
80913
  reloadRequested = false;
80388
80914
  configWatchDebounceMs = 250;
80389
80915
  nextRuntimeId = 1;
80916
+ channelOwnerAlertIncidents = new Map;
80390
80917
  dependencies;
80391
80918
  constructor(configPath, dependencies) {
80392
80919
  this.configPath = configPath;
@@ -80479,6 +81006,7 @@ class RuntimeSupervisor {
80479
81006
  });
80480
81007
  this.activeRuntime = nextRuntime;
80481
81008
  if (previousRuntime) {
81009
+ this.clearChannelOwnerAlertsForRuntime(previousRuntime.id);
80482
81010
  for (const service of previousRuntime.channelServices) {
80483
81011
  await service.service.stop();
80484
81012
  }
@@ -80517,6 +81045,7 @@ class RuntimeSupervisor {
80517
81045
  this.activeRuntime = previousRuntime;
80518
81046
  }
80519
81047
  if (nextRuntime && nextRuntime !== this.activeRuntime) {
81048
+ this.clearChannelOwnerAlertsForRuntime(nextRuntime.id);
80520
81049
  for (const service of nextRuntime.channelServices) {
80521
81050
  await service.service.stop();
80522
81051
  }
@@ -80640,11 +81169,18 @@ class RuntimeSupervisor {
80640
81169
  return channelServices.filter((entry) => entry.channel === channel).map((entry) => entry.service.getRuntimeIdentity?.()).filter((identity) => identity != null);
80641
81170
  }
80642
81171
  async reportChannelLifecycle(params) {
80643
- if (this.activeRuntime?.id !== params.runtimeId) {
81172
+ const activeRuntime = this.activeRuntime;
81173
+ if (activeRuntime?.id !== params.runtimeId) {
80644
81174
  return;
80645
81175
  }
80646
81176
  const instances = this.getChannelInstances(params.channelServices, params.plugin.id);
81177
+ const incidentKey = buildChannelOwnerAlertKey({
81178
+ runtimeId: params.runtimeId,
81179
+ channel: params.plugin.id,
81180
+ botId: params.botId
81181
+ });
80647
81182
  if (params.event.connection === "active") {
81183
+ await this.clearChannelOwnerAlert(incidentKey, params.event);
80648
81184
  await this.dependencies.runtimeHealthStore.setChannel({
80649
81185
  channel: params.plugin.id,
80650
81186
  connection: "active",
@@ -80667,6 +81203,14 @@ class RuntimeSupervisor {
80667
81203
  ],
80668
81204
  instances
80669
81205
  });
81206
+ this.scheduleChannelOwnerAlert({
81207
+ key: incidentKey,
81208
+ runtimeId: params.runtimeId,
81209
+ loadedConfig: activeRuntime.loadedConfig,
81210
+ pluginId: params.plugin.id,
81211
+ botId: params.botId,
81212
+ event: params.event
81213
+ });
80670
81214
  }
80671
81215
  async reconcileConfigWatcher(loadedConfig) {
80672
81216
  const configReload = loadedConfig.raw.control.configReload;
@@ -80712,12 +81256,151 @@ class RuntimeSupervisor {
80712
81256
  if (!this.activeRuntime) {
80713
81257
  return;
80714
81258
  }
81259
+ this.clearChannelOwnerAlertsForRuntime(this.activeRuntime.id);
80715
81260
  for (const service of this.activeRuntime.channelServices) {
80716
81261
  await service.service.stop();
80717
81262
  }
80718
81263
  await this.activeRuntime.agentService.stop();
80719
81264
  this.activeRuntime = undefined;
80720
81265
  }
81266
+ scheduleChannelOwnerAlert(params) {
81267
+ const delayMs = params.event.ownerAlertAfterMs;
81268
+ if (!delayMs || delayMs <= 0 || !params.loadedConfig.raw.control.runtimeMonitor.ownerAlerts.enabled) {
81269
+ return;
81270
+ }
81271
+ const repeatAlertEveryMs = params.event.ownerAlertRepeatMs ?? params.loadedConfig.raw.control.runtimeMonitor.ownerAlerts.minIntervalMinutes * 60000;
81272
+ const existingIncident = this.channelOwnerAlertIncidents.get(params.key);
81273
+ const incident = existingIncident ?? {
81274
+ runtimeId: params.runtimeId,
81275
+ channel: params.pluginId,
81276
+ botId: params.botId,
81277
+ repeatAlertEveryMs: Math.max(1, repeatAlertEveryMs),
81278
+ startedAtMs: Date.now(),
81279
+ deliveredAlerts: 0
81280
+ };
81281
+ incident.channel = params.pluginId;
81282
+ incident.botId = params.botId;
81283
+ incident.summary = params.event.summary;
81284
+ incident.detail = params.event.detail;
81285
+ incident.repeatAlertEveryMs = Math.max(1, repeatAlertEveryMs);
81286
+ this.channelOwnerAlertIncidents.set(params.key, incident);
81287
+ if (incident.timer) {
81288
+ return;
81289
+ }
81290
+ this.scheduleNextChannelOwnerAlert({
81291
+ key: params.key,
81292
+ runtimeId: params.runtimeId,
81293
+ channel: params.pluginId,
81294
+ botId: params.botId,
81295
+ delayMs: existingIncident ? incident.repeatAlertEveryMs : delayMs
81296
+ });
81297
+ }
81298
+ scheduleNextChannelOwnerAlert(params) {
81299
+ const incident = this.channelOwnerAlertIncidents.get(params.key);
81300
+ if (!incident || incident.runtimeId !== params.runtimeId) {
81301
+ return;
81302
+ }
81303
+ incident.timer = setTimeout(() => {
81304
+ this.fireChannelOwnerAlert({
81305
+ key: params.key,
81306
+ runtimeId: params.runtimeId,
81307
+ channel: params.channel,
81308
+ botId: params.botId
81309
+ });
81310
+ }, params.delayMs);
81311
+ incident.timer.unref?.();
81312
+ this.channelOwnerAlertIncidents.set(params.key, incident);
81313
+ }
81314
+ async fireChannelOwnerAlert(params) {
81315
+ const incident = this.channelOwnerAlertIncidents.get(params.key);
81316
+ if (!incident || incident.runtimeId !== params.runtimeId) {
81317
+ return;
81318
+ }
81319
+ incident.timer = undefined;
81320
+ const activeRuntime = this.activeRuntime;
81321
+ if (activeRuntime?.id !== params.runtimeId) {
81322
+ return;
81323
+ }
81324
+ try {
81325
+ const message = renderChannelOwnerAlertMessage({
81326
+ channel: params.channel,
81327
+ botId: params.botId,
81328
+ incidentState: incident.deliveredAlerts === 0 ? "failed" : "still-failed",
81329
+ elapsedMs: Date.now() - incident.startedAtMs,
81330
+ summary: incident.summary,
81331
+ detail: incident.detail
81332
+ });
81333
+ const result = await sendOwnerAlert({
81334
+ loadedConfig: activeRuntime.loadedConfig,
81335
+ message,
81336
+ listChannelPlugins: this.dependencies.listChannelPlugins
81337
+ });
81338
+ if (result.delivered.length > 0) {
81339
+ incident.deliveredAlerts += 1;
81340
+ }
81341
+ this.channelOwnerAlertIncidents.set(params.key, incident);
81342
+ if (result.delivered.length === 0 && result.failed.length > 0) {
81343
+ console.error("clisbot channel alert delivery failed", result.failed.map((entry) => `${entry.principal}: ${entry.detail}`).join("; "));
81344
+ }
81345
+ } catch (error) {
81346
+ console.error("clisbot channel alert dispatch failed", error);
81347
+ }
81348
+ const nextIncident = this.channelOwnerAlertIncidents.get(params.key);
81349
+ if (!nextIncident || nextIncident.runtimeId !== params.runtimeId || nextIncident.timer || nextIncident.deliveredAlerts >= 2) {
81350
+ return;
81351
+ }
81352
+ this.scheduleNextChannelOwnerAlert({
81353
+ key: params.key,
81354
+ runtimeId: params.runtimeId,
81355
+ channel: params.channel,
81356
+ botId: params.botId,
81357
+ delayMs: nextIncident.repeatAlertEveryMs
81358
+ });
81359
+ }
81360
+ async clearChannelOwnerAlert(key, activeEvent) {
81361
+ const incident = this.channelOwnerAlertIncidents.get(key);
81362
+ if (!incident) {
81363
+ return;
81364
+ }
81365
+ if (incident.timer) {
81366
+ clearTimeout(incident.timer);
81367
+ }
81368
+ this.channelOwnerAlertIncidents.delete(key);
81369
+ if (incident.deliveredAlerts === 0) {
81370
+ return;
81371
+ }
81372
+ const activeRuntime = this.activeRuntime;
81373
+ if (!activeRuntime || activeRuntime.id !== incident.runtimeId) {
81374
+ return;
81375
+ }
81376
+ try {
81377
+ await sendOwnerAlert({
81378
+ loadedConfig: activeRuntime.loadedConfig,
81379
+ message: renderChannelOwnerAlertMessage({
81380
+ channel: incident.channel,
81381
+ botId: incident.botId,
81382
+ incidentState: "resolved",
81383
+ elapsedMs: Date.now() - incident.startedAtMs,
81384
+ summary: activeEvent?.summary ?? "Channel recovered.",
81385
+ detail: activeEvent?.detail
81386
+ }),
81387
+ listChannelPlugins: this.dependencies.listChannelPlugins
81388
+ });
81389
+ } catch (error) {
81390
+ console.error("clisbot channel recovery alert dispatch failed", error);
81391
+ }
81392
+ }
81393
+ clearChannelOwnerAlertsForRuntime(runtimeId) {
81394
+ for (const [key, incident] of this.channelOwnerAlertIncidents.entries()) {
81395
+ if (incident.runtimeId !== runtimeId) {
81396
+ continue;
81397
+ }
81398
+ if (incident.timer) {
81399
+ clearTimeout(incident.timer);
81400
+ }
81401
+ this.channelOwnerAlertIncidents.delete(key);
81402
+ }
81403
+ }
80721
81404
  }
80722
81405
  async function withStartupTimeout(name, start2) {
80723
81406
  let timer;
@@ -80738,6 +81421,9 @@ async function withStartupTimeout(name, start2) {
80738
81421
  }
80739
81422
 
80740
81423
  // src/control/runtime-management-cli.ts
81424
+ function getOperatorConfigPath() {
81425
+ return expandHomePath(process.env.CLISBOT_CONFIG_PATH || DEFAULT_CONFIG_PATH);
81426
+ }
80741
81427
  function getPrimaryWorkspacePath2(summary) {
80742
81428
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
80743
81429
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -80801,7 +81487,9 @@ function registerProcessHandlers(runtimeSupervisor, shutdown) {
80801
81487
  });
80802
81488
  }
80803
81489
  async function printStatusSummary() {
80804
- const runtimeStatus = await getRuntimeStatus();
81490
+ const runtimeStatus = await getRuntimeStatus({
81491
+ configPath: getOperatorConfigPath()
81492
+ });
80805
81493
  console.log(`version: ${getClisbotVersion()}`);
80806
81494
  console.log(`running: ${runtimeStatus.running ? "yes" : "no"}`);
80807
81495
  if (runtimeStatus.pid) {
@@ -80860,7 +81548,9 @@ async function printStatusSummary() {
80860
81548
  }
80861
81549
  async function printDiagnosticsAfterLogTail() {
80862
81550
  try {
80863
- const runtimeStatus = await getRuntimeStatus();
81551
+ const runtimeStatus = await getRuntimeStatus({
81552
+ configPath: getOperatorConfigPath()
81553
+ });
80864
81554
  const summary = await getRuntimeOperatorSummary({
80865
81555
  configPath: runtimeStatus.configPath,
80866
81556
  runtimeRunning: runtimeStatus.running
@@ -80948,6 +81638,7 @@ async function printCliError(error) {
80948
81638
  }
80949
81639
  async function stop(hard = false) {
80950
81640
  const result = await stopDetachedRuntime({
81641
+ configPath: getOperatorConfigPath(),
80951
81642
  hard
80952
81643
  });
80953
81644
  if (!result.stopped && !hard) {
@@ -80968,6 +81659,7 @@ async function stop(hard = false) {
80968
81659
  }
80969
81660
  async function restart() {
80970
81661
  await stopDetachedRuntime({
81662
+ configPath: getOperatorConfigPath(),
80971
81663
  hard: false
80972
81664
  });
80973
81665
  }
@@ -80975,7 +81667,11 @@ async function status() {
80975
81667
  await printStatusSummary();
80976
81668
  }
80977
81669
  async function logs(lines) {
81670
+ const runtimeStatus = await getRuntimeStatus({
81671
+ configPath: getOperatorConfigPath()
81672
+ });
80978
81673
  const result = await readRuntimeLog({
81674
+ logPath: runtimeStatus.logPath,
80979
81675
  lines
80980
81676
  });
80981
81677
  if (!result.text) {