clisbot 0.1.22 → 0.1.28

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
@@ -54694,6 +54694,7 @@ function renderCliHelp() {
54694
54694
  "",
54695
54695
  "Commands:",
54696
54696
  ` start Seed ${configPath} if missing, apply explicit channel-account bootstrap intent, and start clisbot in the background.`,
54697
+ " See `clisbot start --help` for bootstrap-focused flags and examples.",
54697
54698
  " restart Stop the running clisbot process, then start it again.",
54698
54699
  " stop Stop the running clisbot process.",
54699
54700
  " stop --hard Stop clisbot and kill all tmux sessions on the configured clisbot socket.",
@@ -54710,20 +54711,26 @@ function renderCliHelp() {
54710
54711
  " remove slack-group <groupId>",
54711
54712
  " set-token <slack-app|slack-bot|telegram-bot> <value>",
54712
54713
  " clear-token <slack-app|slack-bot|telegram-bot>",
54714
+ " See `clisbot channels --help` for route policy notes and defaults such as `requireMention`.",
54713
54715
  " accounts Manage Slack and Telegram provider accounts plus persistence state.",
54714
54716
  " add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54715
54717
  " add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
54716
54718
  " persist --channel <slack|telegram> --account <id>",
54717
54719
  " persist --all",
54720
+ " See `clisbot accounts --help` for env-vs-mem-vs-persist behavior.",
54718
54721
  " loops Inspect or cancel managed recurring loops persisted by `/loop`.",
54719
54722
  " list|status",
54720
54723
  " cancel <id>",
54721
54724
  " cancel --all",
54725
+ " See `clisbot loops --help` for behavior notes.",
54722
54726
  " message Run provider message actions such as send, react, read, edit, delete, and pins.",
54727
+ " See `clisbot message --help` for channel-specific syntax.",
54723
54728
  " agents Manage configured agents and top-level bindings.",
54729
+ " See `clisbot agents --help` for focused add/bootstrap/binding help.",
54724
54730
  " auth Manage app and agent auth roles, principals, and permissions in config. See `clisbot auth --help`.",
54725
- " pairing Run the pairing control CLI.",
54731
+ " pairing Run the pairing control CLI. See `clisbot pairing --help`.",
54726
54732
  ` init Seed ${configPath} and optionally create the first agent without starting clisbot.`,
54733
+ " See `clisbot init --help` for bootstrap-focused flags and examples.",
54727
54734
  " --version, -v Show the installed clisbot version.",
54728
54735
  " --help Show this help text.",
54729
54736
  "",
@@ -54766,6 +54773,35 @@ function buildPairingReply(params) {
54766
54773
  ].join(`
54767
54774
  `);
54768
54775
  }
54776
+ function buildPairingQueueFullReply(params) {
54777
+ return [
54778
+ "clisbot: access not configured.",
54779
+ "",
54780
+ params.idLine,
54781
+ "",
54782
+ "Pairing queue is full right now.",
54783
+ "",
54784
+ "Ask the bot owner to inspect or clear pending requests with:",
54785
+ `clisbot pairing list ${params.channel}`,
54786
+ `clisbot pairing reject ${params.channel} <code>`,
54787
+ `clisbot pairing clear ${params.channel}`
54788
+ ].join(`
54789
+ `);
54790
+ }
54791
+ function buildPairingReplyFromRequest(params) {
54792
+ const code = params.pairingRequest.code.trim();
54793
+ if (!code) {
54794
+ return buildPairingQueueFullReply({
54795
+ channel: params.channel,
54796
+ idLine: params.idLine
54797
+ });
54798
+ }
54799
+ return buildPairingReply({
54800
+ channel: params.channel,
54801
+ idLine: params.idLine,
54802
+ code
54803
+ });
54804
+ }
54769
54805
  function renderPairingRequests(params) {
54770
54806
  if (!params.requests.length) {
54771
54807
  return `No pending ${params.channel} pairing requests.`;
@@ -54788,7 +54824,7 @@ import path from "node:path";
54788
54824
  var PAIRING_CODE_LENGTH = 8;
54789
54825
  var PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
54790
54826
  var PAIRING_PENDING_TTL_MS = 60 * 60 * 1000;
54791
- var PAIRING_PENDING_MAX = 3;
54827
+ var PAIRING_PENDING_MAX = 20;
54792
54828
  var PAIRING_STORE_LOCK_OPTIONS = {
54793
54829
  retries: {
54794
54830
  retries: 10,
@@ -55067,8 +55103,63 @@ async function approveChannelPairingCode(params) {
55067
55103
  return { id: entry.id, entry };
55068
55104
  });
55069
55105
  }
55106
+ async function rejectChannelPairingCode(params) {
55107
+ const code = params.code.trim().toUpperCase();
55108
+ if (!code) {
55109
+ return null;
55110
+ }
55111
+ const filePath = resolvePairingPath(params.channel, params.baseDir);
55112
+ return withFileLock(filePath, { version: 1, requests: [] }, async () => {
55113
+ const { value } = await readJsonFile(filePath, {
55114
+ version: 1,
55115
+ requests: []
55116
+ });
55117
+ const requests = Array.isArray(value.requests) ? value.requests : [];
55118
+ const { requests: pruned, removed } = pruneExpiredRequests(requests, Date.now());
55119
+ const matchIndex = pruned.findIndex((request) => String(request.code ?? "").trim().toUpperCase() === code);
55120
+ if (matchIndex < 0) {
55121
+ if (removed) {
55122
+ await writeJsonFile(filePath, {
55123
+ version: 1,
55124
+ requests: pruned
55125
+ });
55126
+ }
55127
+ return null;
55128
+ }
55129
+ const [rejected] = pruned.splice(matchIndex, 1);
55130
+ await writeJsonFile(filePath, {
55131
+ version: 1,
55132
+ requests: pruned
55133
+ });
55134
+ return rejected ?? null;
55135
+ });
55136
+ }
55137
+ async function clearChannelPairingRequests(params) {
55138
+ const filePath = resolvePairingPath(params.channel, params.baseDir);
55139
+ return withFileLock(filePath, { version: 1, requests: [] }, async () => {
55140
+ const { value } = await readJsonFile(filePath, {
55141
+ version: 1,
55142
+ requests: []
55143
+ });
55144
+ const requests = Array.isArray(value.requests) ? value.requests : [];
55145
+ const { requests: pruned } = pruneExpiredRequests(requests, Date.now());
55146
+ await writeJsonFile(filePath, {
55147
+ version: 1,
55148
+ requests: []
55149
+ });
55150
+ return { cleared: pruned.length };
55151
+ });
55152
+ }
55070
55153
 
55071
55154
  // src/channels/pairing/cli.ts
55155
+ function resolvePairingBaseDir(env = process.env) {
55156
+ const configured = env.CLISBOT_PAIRING_DIR?.trim();
55157
+ if (configured) {
55158
+ return configured;
55159
+ }
55160
+ const legacy = env.TMUX_TALK_PAIRING_DIR?.trim();
55161
+ return legacy || undefined;
55162
+ }
55072
55163
  function parseChannel(raw) {
55073
55164
  const value = raw?.trim().toLowerCase();
55074
55165
  if (value === "slack" || value === "telegram") {
@@ -55076,9 +55167,33 @@ function parseChannel(raw) {
55076
55167
  }
55077
55168
  throw new Error("Channel required: slack | telegram");
55078
55169
  }
55170
+ function renderPairingCliHelp() {
55171
+ return [
55172
+ "clisbot pairing",
55173
+ "",
55174
+ "Usage:",
55175
+ " clisbot pairing --help",
55176
+ " clisbot pairing help",
55177
+ " clisbot pairing list <slack|telegram> [--json]",
55178
+ " clisbot pairing approve <slack|telegram> <code>",
55179
+ " clisbot pairing reject <slack|telegram> <code>",
55180
+ " clisbot pairing clear <slack|telegram>",
55181
+ "",
55182
+ "Notes:",
55183
+ " - `list` shows pending pairing requests for one channel only",
55184
+ " - `approve` moves that sender into the channel allowlist",
55185
+ " - `reject` removes one pending request without allowlisting the sender",
55186
+ " - `clear` drops every pending request for that channel when the queue needs a reset"
55187
+ ].join(`
55188
+ `);
55189
+ }
55079
55190
  async function runPairingCli(args, writer = console) {
55080
55191
  const [command, ...rest] = args;
55081
- const baseDir = process.env.TMUX_TALK_PAIRING_DIR;
55192
+ const baseDir = resolvePairingBaseDir();
55193
+ if (!command || command === "--help" || command === "-h" || command === "help") {
55194
+ writer.log(renderPairingCliHelp());
55195
+ return;
55196
+ }
55082
55197
  if (command === "list") {
55083
55198
  const wantsJson = rest.includes("--json");
55084
55199
  const channel = parseChannel(rest.find((value) => !value.startsWith("--")));
@@ -55103,7 +55218,34 @@ async function runPairingCli(args, writer = console) {
55103
55218
  writer.log(`Approved ${channel} sender ${approved.id}.`);
55104
55219
  return;
55105
55220
  }
55106
- throw new Error("Usage: pairing list <channel> [--json] | pairing approve <channel> <code>");
55221
+ if (command === "reject") {
55222
+ const [channelArg, code] = rest;
55223
+ const channel = parseChannel(channelArg);
55224
+ if (!code?.trim()) {
55225
+ throw new Error("Usage: pairing reject <channel> <code>");
55226
+ }
55227
+ const rejected = await rejectChannelPairingCode({
55228
+ channel,
55229
+ code,
55230
+ baseDir
55231
+ });
55232
+ if (!rejected) {
55233
+ throw new Error(`No pending pairing request found for code: ${code}`);
55234
+ }
55235
+ writer.log(`Rejected ${channel} sender ${rejected.id}.`);
55236
+ return;
55237
+ }
55238
+ if (command === "clear") {
55239
+ const [channelArg] = rest;
55240
+ const channel = parseChannel(channelArg);
55241
+ const result = await clearChannelPairingRequests({
55242
+ channel,
55243
+ baseDir
55244
+ });
55245
+ writer.log(`Cleared ${result.cleared} pending ${channel} pairing request(s).`);
55246
+ return;
55247
+ }
55248
+ throw new Error(renderPairingCliHelp());
55107
55249
  }
55108
55250
 
55109
55251
  // src/config/agent-tool-presets.ts
@@ -59283,7 +59425,7 @@ function formatConfiguredRuntimeLimit(params) {
59283
59425
  if (typeof params.maxRuntimeMin === "number" && Number.isFinite(params.maxRuntimeMin)) {
59284
59426
  return `${params.maxRuntimeMin} minute${params.maxRuntimeMin === 1 ? "" : "s"}`;
59285
59427
  }
59286
- return "15 minutes";
59428
+ return "30 minutes";
59287
59429
  }
59288
59430
  function parseCommandDurationMs(raw) {
59289
59431
  const match = raw.trim().match(/^(\d+)(ms|s|m|h)$/i);
@@ -59932,7 +60074,7 @@ var sessionDmScopeSchema = exports_external.enum([
59932
60074
  ]);
59933
60075
  var sessionConfigSchema = exports_external.object({
59934
60076
  mainKey: exports_external.string().min(1).default("main"),
59935
- dmScope: sessionDmScopeSchema.default("main"),
60077
+ dmScope: sessionDmScopeSchema.default("per-channel-peer"),
59936
60078
  identityLinks: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())).default({}),
59937
60079
  storePath: exports_external.string().default("~/.clisbot/state/sessions.json")
59938
60080
  });
@@ -59991,7 +60133,7 @@ var agentDefaultsSchema = exports_external.object({
59991
60133
  updateIntervalMs: 2000,
59992
60134
  idleTimeoutMs: 6000,
59993
60135
  noOutputTimeoutMs: 20000,
59994
- maxRuntimeMin: 15,
60136
+ maxRuntimeMin: 30,
59995
60137
  maxMessageChars: 3500
59996
60138
  }),
59997
60139
  session: sessionSchema.default({
@@ -60250,7 +60392,15 @@ var controlRuntimeMonitorRestartStageSchema = exports_external.object({
60250
60392
  delayMinutes: exports_external.number().int().positive().default(15),
60251
60393
  maxRestarts: exports_external.number().int().positive().default(4)
60252
60394
  });
60395
+ var controlRuntimeMonitorFastRetrySchema = exports_external.object({
60396
+ delaySeconds: exports_external.number().int().positive().default(10),
60397
+ maxRestarts: exports_external.number().int().min(0).default(3)
60398
+ });
60253
60399
  var controlRuntimeMonitorRestartBackoffSchema = exports_external.object({
60400
+ fastRetry: controlRuntimeMonitorFastRetrySchema.default({
60401
+ delaySeconds: 10,
60402
+ maxRestarts: 3
60403
+ }),
60254
60404
  stages: exports_external.array(controlRuntimeMonitorRestartStageSchema).min(1).default([
60255
60405
  {
60256
60406
  delayMinutes: 15,
@@ -60268,6 +60418,10 @@ var controlRuntimeMonitorOwnerAlertsSchema = exports_external.object({
60268
60418
  });
60269
60419
  var controlRuntimeMonitorSchema = exports_external.object({
60270
60420
  restartBackoff: controlRuntimeMonitorRestartBackoffSchema.default({
60421
+ fastRetry: {
60422
+ delaySeconds: 10,
60423
+ maxRestarts: 3
60424
+ },
60271
60425
  stages: [
60272
60426
  {
60273
60427
  delayMinutes: 15,
@@ -60328,7 +60482,7 @@ var clisbotConfigSchema = exports_external.object({
60328
60482
  }),
60329
60483
  session: sessionConfigSchema.default({
60330
60484
  mainKey: "main",
60331
- dmScope: "main",
60485
+ dmScope: "per-channel-peer",
60332
60486
  identityLinks: {},
60333
60487
  storePath: "~/.clisbot/state/sessions.json"
60334
60488
  }),
@@ -60367,6 +60521,10 @@ var clisbotConfigSchema = exports_external.object({
60367
60521
  },
60368
60522
  runtimeMonitor: {
60369
60523
  restartBackoff: {
60524
+ fastRetry: {
60525
+ delaySeconds: 10,
60526
+ maxRestarts: 3
60527
+ },
60370
60528
  stages: [
60371
60529
  {
60372
60530
  delayMinutes: 15,
@@ -60581,7 +60739,7 @@ function renderDefaultConfigTemplate(options = {}) {
60581
60739
  },
60582
60740
  session: {
60583
60741
  mainKey: "main",
60584
- dmScope: "main",
60742
+ dmScope: "per-channel-peer",
60585
60743
  identityLinks: {},
60586
60744
  storePath: sessionStorePath
60587
60745
  },
@@ -60663,7 +60821,7 @@ function renderDefaultConfigTemplate(options = {}) {
60663
60821
  updateIntervalMs: 2000,
60664
60822
  idleTimeoutMs: 6000,
60665
60823
  noOutputTimeoutMs: 20000,
60666
- maxRuntimeMin: 15,
60824
+ maxRuntimeMin: 30,
60667
60825
  maxMessageChars: 3500
60668
60826
  },
60669
60827
  session: {
@@ -60691,6 +60849,10 @@ function renderDefaultConfigTemplate(options = {}) {
60691
60849
  },
60692
60850
  runtimeMonitor: {
60693
60851
  restartBackoff: {
60852
+ fastRetry: {
60853
+ delaySeconds: 10,
60854
+ maxRestarts: 3
60855
+ },
60694
60856
  stages: [
60695
60857
  {
60696
60858
  delayMinutes: 15,
@@ -61024,6 +61186,32 @@ function parseSingleOption(args, name) {
61024
61186
  function hasFlag(args, name) {
61025
61187
  return args.includes(name);
61026
61188
  }
61189
+ function renderAgentsHelp() {
61190
+ return [
61191
+ "clisbot agents",
61192
+ "",
61193
+ "Usage:",
61194
+ " clisbot agents --help",
61195
+ " clisbot agents help",
61196
+ " clisbot agents list [--bindings] [--json]",
61197
+ " clisbot agents add <id> --cli <codex|claude|gemini> [--workspace <path>] [--startup-option <arg>]... [--bootstrap <personal-assistant|team-assistant>] [--bind <channel[:accountId]>]...",
61198
+ " clisbot agents bootstrap <id> --mode <personal-assistant|team-assistant> [--force]",
61199
+ " clisbot agents bindings [--agent <id>] [--json]",
61200
+ " clisbot agents bind --agent <id> --bind <channel[:accountId]>",
61201
+ " clisbot agents unbind --agent <id> [--bind <channel[:accountId]> | --all]",
61202
+ " clisbot agents response-mode <status|set|clear> --agent <id> [capture-pane|message-tool]",
61203
+ " clisbot agents additional-message-mode <status|set|clear> --agent <id> [queue|steer]",
61204
+ "",
61205
+ "Notes:",
61206
+ " - `agents add` is the lower-level manual surface; first-run `clisbot start` and `clisbot init` can bootstrap the first `default` agent for you",
61207
+ " - `--cli` is required on `agents add`; supported tools are `codex`, `claude`, and `gemini`",
61208
+ " - omit `--startup-option` to inherit the built-in startup args for the selected CLI tool",
61209
+ " - `--bind slack`, `--bind telegram`, or `--bind <channel>:<accountId>` creates top-level fallback bindings",
61210
+ " - explicit route `agentId` on Slack or Telegram still wins before these fallback bindings",
61211
+ " - `response-mode` and `additional-message-mode` mutate per-agent overrides under `agents.list[]`"
61212
+ ].join(`
61213
+ `);
61214
+ }
61027
61215
  function parseResponseMode(raw) {
61028
61216
  if (raw === "capture-pane" || raw === "message-tool") {
61029
61217
  return raw;
@@ -61353,7 +61541,11 @@ async function runAgentAdditionalMessageModeCli(args) {
61353
61541
  async function runAgentsCli(args) {
61354
61542
  const subcommand = args[0];
61355
61543
  const rest = args.slice(1);
61356
- if (!subcommand || subcommand === "list") {
61544
+ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
61545
+ console.log(renderAgentsHelp());
61546
+ return;
61547
+ }
61548
+ if (subcommand === "list") {
61357
61549
  await listAgents(rest);
61358
61550
  return;
61359
61551
  }
@@ -61385,7 +61577,7 @@ async function runAgentsCli(args) {
61385
61577
  await unbindAgent(rest);
61386
61578
  return;
61387
61579
  }
61388
- throw new Error(`Unknown agents subcommand: ${subcommand}`);
61580
+ throw new Error(renderAgentsHelp());
61389
61581
  }
61390
61582
 
61391
61583
  // src/control/accounts-cli.ts
@@ -61731,8 +61923,8 @@ class RuntimeHealthStore {
61731
61923
 
61732
61924
  // src/control/runtime-process.ts
61733
61925
  import { execFileSync, spawn as spawn3 } from "node:child_process";
61734
- import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
61735
- import { dirname as dirname13 } from "node:path";
61926
+ import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
61927
+ import { dirname as dirname13, join as join11 } from "node:path";
61736
61928
  import { kill as kill2 } from "node:process";
61737
61929
 
61738
61930
  // src/control/clisbot-wrapper.ts
@@ -62719,43 +62911,151 @@ function hasActiveRuntime(entry) {
62719
62911
  }
62720
62912
 
62721
62913
  // src/channels/agent-prompt.ts
62722
- function buildAgentPromptText(params) {
62723
- if (!params.config.enabled) {
62724
- return params.text;
62725
- }
62726
- const systemBlock = renderAgentPromptInstruction(params);
62727
- return `<system>
62728
- ${systemBlock}
62914
+ var CONFIGURATION_GUIDANCE = "When the user asks to change clisbot configuration, use clisbot CLI commands; see `clisbot --help`, `clisbot channels --help`, or `clisbot auth --help` for details.";
62915
+ var BASE_TEMPLATE = `<system>
62916
+ [{{timestamp}}] {{identity_summary}}
62917
+
62918
+ You are operating inside clisbot.
62919
+ {{delivery_intro}}
62920
+ {{reply_command}}
62921
+ {{reply_rules}}
62922
+ ${CONFIGURATION_GUIDANCE}{{protected_control_suffix}}
62923
+ </system>
62924
+
62925
+ <user>
62926
+ {{message_body}}
62927
+ </user>`;
62928
+ var STEERING_TEMPLATE = `<system>
62929
+ A new user message arrived while you were still working.
62930
+ Adjust your current work if needed and continue.{{protected_control_suffix}}
62729
62931
  </system>
62730
62932
 
62731
62933
  <user>
62732
- ${params.text}
62934
+ {{message_body}}
62733
62935
  </user>`;
62936
+ var DELIVERY_INTRO = "To send a user-visible {{progress_phrase}}final reply, use the following CLI command:";
62937
+ var DELIVERY_INTRO_CAPTURE_PANE = "channel auto-delivery remains enabled for this conversation; do not send user-facing progress updates or the final response with clisbot message send";
62938
+ var REPLY_COMMAND = `{{reply_command_base}}
62939
+ --final{{progress_flag_suffix}} \\
62940
+ --message "$(cat <<\\__CLISBOT_MESSAGE__
62941
+ <user-facing reply>
62942
+ __CLISBOT_MESSAGE__
62943
+ )" \\
62944
+ [--media /absolute/path/to/file]`;
62945
+ var REPLY_RULES = `When replying to the user:
62946
+ - put the user-facing message inside the --message body of that command
62947
+ {{progress_rules_block}}- {{final_rule_line}}`;
62948
+ var PROGRESS_PHRASE = "progress update or ";
62949
+ var EMPTY_PROGRESS_PHRASE = "";
62950
+ var PROGRESS_FLAG_SUFFIX = "|progress";
62951
+ var EMPTY_PROGRESS_FLAG_SUFFIX = "";
62952
+ var PROGRESS_RULES_BLOCK = `- use that command to send progress updates and the final reply back to the conversation
62953
+ - send at most {{max_progress_messages}} progress updates
62954
+ - keep progress updates short and meaningful
62955
+ - do not send progress updates for trivial internal steps
62956
+ `;
62957
+ var FINAL_ONLY_RULES_BLOCK = `- use that command only for the final user-facing reply
62958
+ - do not send user-facing progress updates for this conversation
62959
+ `;
62960
+ var FINAL_RULE_REQUIRED = "send exactly 1 final user-facing response";
62961
+ var FINAL_RULE_OPTIONAL = "final response is optional";
62962
+ var EMPTY_REPLY_COMMAND = "";
62963
+ var EMPTY_REPLY_RULES = "";
62964
+ var SLACK_REPLY_COMMAND_BASE = `{{command}} message send \\
62965
+ --channel slack \\
62966
+ {{account_clause}} --target channel:{{channel_id}} \\
62967
+ {{thread_clause}}`;
62968
+ var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
62969
+ --channel telegram \\
62970
+ {{account_clause}} --target {{chat_id}} \\
62971
+ {{thread_clause}}`;
62972
+ var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
62973
+ var EMPTY_ACCOUNT_CLAUSE = "";
62974
+ var SLACK_THREAD_CLAUSE = " --thread-id {{thread_ts}} \\\n";
62975
+ var TELEGRAM_THREAD_CLAUSE = " --thread-id {{topic_id}} \\\n";
62976
+ var EMPTY_THREAD_CLAUSE = "";
62977
+ function buildAgentPromptText(params) {
62978
+ return buildChannelPromptText({
62979
+ ...params,
62980
+ mode: "message"
62981
+ });
62734
62982
  }
62735
- function renderAgentPromptInstruction(params) {
62736
- const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
62737
- const progressAllowed = messageToolMode && (params.streaming ?? "off") === "off";
62738
- const lines = [
62739
- `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
62740
- "",
62741
- "You are operating inside clisbot.",
62742
- messageToolMode ? progressAllowed ? "To send a user-visible progress update or final reply, use the following CLI command:" : "To send the final user-visible reply, use the following CLI command:" : "channel auto-delivery remains enabled for this conversation; do not send user-facing progress updates or the final response with clisbot message send"
62743
- ];
62744
- if (messageToolMode) {
62745
- const replyCommand = buildReplyCommand({
62746
- command: getClisbotPromptCommand(),
62747
- identity: params.identity
62983
+ function buildSteeringPromptText(params) {
62984
+ return buildChannelPromptText({
62985
+ text: params.text,
62986
+ mode: "steer",
62987
+ protectedControlMutationRule: params.protectedControlMutationRule
62988
+ });
62989
+ }
62990
+ function buildChannelPromptText(params) {
62991
+ if (params.mode === "message" && !params.config?.enabled) {
62992
+ return params.text;
62993
+ }
62994
+ if (params.mode === "steer") {
62995
+ return renderTemplate(STEERING_TEMPLATE, {
62996
+ message_body: params.text,
62997
+ protected_control_suffix: renderProtectedControlSuffix(params.protectedControlMutationRule)
62748
62998
  });
62749
- lines.push(replyCommand, "When replying to the user:", "- put the user-facing message inside the --message body of that command", progressAllowed ? "- use that command to send progress updates and the final reply back to the conversation" : "- use that command only for the final user-facing reply", ...progressAllowed ? [`- send at most ${params.config.maxProgressMessages} progress updates`] : ["- do not send user-facing progress updates for this conversation"], params.config.requireFinalResponse ? "- send exactly 1 final user-facing response" : "- final response is optional", ...progressAllowed ? [
62750
- "- keep progress updates short and meaningful",
62751
- "- do not send progress updates for trivial internal steps"
62752
- ] : []);
62753
62999
  }
62754
- if (params.protectedControlMutationRule) {
62755
- lines.push("", params.protectedControlMutationRule);
63000
+ const promptParts = renderMessagePromptParts({
63001
+ identity: params.identity,
63002
+ config: params.config,
63003
+ responseMode: params.responseMode,
63004
+ streaming: params.streaming
63005
+ });
63006
+ return renderTemplate(BASE_TEMPLATE, {
63007
+ timestamp: renderPromptTimestamp(),
63008
+ identity_summary: renderIdentitySummary(params.identity),
63009
+ delivery_intro: promptParts.deliveryIntro,
63010
+ reply_command: promptParts.replyCommand,
63011
+ reply_rules: promptParts.replyRules,
63012
+ protected_control_suffix: renderProtectedControlSuffix(params.protectedControlMutationRule),
63013
+ message_body: params.text
63014
+ });
63015
+ }
63016
+ function renderMessagePromptParts(params) {
63017
+ const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
63018
+ if (!messageToolMode) {
63019
+ return {
63020
+ deliveryIntro: DELIVERY_INTRO_CAPTURE_PANE,
63021
+ replyCommand: EMPTY_REPLY_COMMAND,
63022
+ replyRules: EMPTY_REPLY_RULES
63023
+ };
62756
63024
  }
62757
- return lines.join(`
62758
- `);
63025
+ const allowProgress = (params.streaming ?? "off") === "off";
63026
+ const progressPhrase = allowProgress ? PROGRESS_PHRASE : EMPTY_PROGRESS_PHRASE;
63027
+ const progressFlagSuffix = allowProgress ? PROGRESS_FLAG_SUFFIX : EMPTY_PROGRESS_FLAG_SUFFIX;
63028
+ const progressRulesBlock = allowProgress ? PROGRESS_RULES_BLOCK : FINAL_ONLY_RULES_BLOCK;
63029
+ const finalRuleLine = params.config.requireFinalResponse ? FINAL_RULE_REQUIRED : FINAL_RULE_OPTIONAL;
63030
+ return {
63031
+ deliveryIntro: renderTemplate(DELIVERY_INTRO, {
63032
+ progress_phrase: progressPhrase
63033
+ }),
63034
+ replyCommand: renderTemplate(REPLY_COMMAND, {
63035
+ reply_command_base: buildReplyCommandBase({
63036
+ command: getClisbotPromptCommand(),
63037
+ identity: params.identity
63038
+ }).trimEnd(),
63039
+ progress_flag_suffix: progressFlagSuffix
63040
+ }),
63041
+ replyRules: renderTemplate(REPLY_RULES, {
63042
+ progress_rules_block: renderTemplate(progressRulesBlock, {
63043
+ max_progress_messages: String(params.config.maxProgressMessages)
63044
+ }),
63045
+ final_rule_line: finalRuleLine
63046
+ })
63047
+ };
63048
+ }
63049
+ function renderProtectedControlSuffix(rule) {
63050
+ if (!rule) {
63051
+ return "";
63052
+ }
63053
+ return `
63054
+
63055
+ ${rule}`;
63056
+ }
63057
+ function renderTemplate(template, values) {
63058
+ return template.replaceAll(/\{\{([a-zA-Z0-9_]+)\}\}/g, (_, key) => values[key] ?? "");
62759
63059
  }
62760
63060
  function renderPromptTimestamp() {
62761
63061
  const date = new Date;
@@ -62821,42 +63121,29 @@ function renderNamedValue(label, name, id) {
62821
63121
  const value = renderLabeledTarget(name, id);
62822
63122
  return value ? `${label} ${value}` : "";
62823
63123
  }
62824
- function buildReplyCommand(params) {
62825
- const lines = [`${params.command} message send \\`];
63124
+ function buildReplyCommandBase(params) {
62826
63125
  if (params.identity.platform === "slack") {
62827
- lines.push(" --channel slack \\");
62828
- if (params.identity.accountId) {
62829
- lines.push(` --account ${params.identity.accountId} \\`);
62830
- }
62831
- lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
62832
- if (params.identity.threadTs) {
62833
- lines.push(` --thread-id ${params.identity.threadTs} \\`);
62834
- }
62835
- lines.push(" --final \\");
62836
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
62837
- lines.push("<user-facing reply>");
62838
- lines.push("__CLISBOT_MESSAGE__");
62839
- lines.push(')" \\');
62840
- lines.push(" [--media /absolute/path/to/file]");
62841
- return lines.join(`
62842
- `);
62843
- }
62844
- lines.push(" --channel telegram \\");
62845
- if (params.identity.accountId) {
62846
- lines.push(` --account ${params.identity.accountId} \\`);
63126
+ return renderTemplate(SLACK_REPLY_COMMAND_BASE, {
63127
+ command: params.command,
63128
+ account_clause: params.identity.accountId ? renderTemplate(ACCOUNT_CLAUSE, {
63129
+ account_id: params.identity.accountId
63130
+ }) : EMPTY_ACCOUNT_CLAUSE,
63131
+ channel_id: params.identity.channelId ?? "",
63132
+ thread_clause: params.identity.threadTs ? renderTemplate(SLACK_THREAD_CLAUSE, {
63133
+ thread_ts: params.identity.threadTs
63134
+ }) : EMPTY_THREAD_CLAUSE
63135
+ });
62847
63136
  }
62848
- lines.push(` --target ${params.identity.chatId ?? ""} \\`);
62849
- if (params.identity.topicId) {
62850
- lines.push(` --thread-id ${params.identity.topicId} \\`);
62851
- }
62852
- lines.push(" --final \\");
62853
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
62854
- lines.push("<user-facing reply>");
62855
- lines.push("__CLISBOT_MESSAGE__");
62856
- lines.push(')" \\');
62857
- lines.push(" [--media /absolute/path/to/file]");
62858
- return lines.join(`
62859
- `);
63137
+ return renderTemplate(TELEGRAM_REPLY_COMMAND_BASE, {
63138
+ command: params.command,
63139
+ account_clause: params.identity.accountId ? renderTemplate(ACCOUNT_CLAUSE, {
63140
+ account_id: params.identity.accountId
63141
+ }) : EMPTY_ACCOUNT_CLAUSE,
63142
+ chat_id: params.identity.chatId ?? "",
63143
+ thread_clause: params.identity.topicId ? renderTemplate(TELEGRAM_THREAD_CLAUSE, {
63144
+ topic_id: params.identity.topicId
63145
+ }) : EMPTY_THREAD_CLAUSE
63146
+ });
62860
63147
  }
62861
63148
 
62862
63149
  // src/channels/surface-notifications.ts
@@ -63390,6 +63677,12 @@ function collapseBlankLines(lines) {
63390
63677
  }
63391
63678
  return collapsed;
63392
63679
  }
63680
+ var DURATION_STATUS_PATTERN = String.raw`(?:\d+(?:h|m|s))(?:\s+\d+(?:h|m|s)){0,2}`;
63681
+ var CODEX_WORKING_STATUS_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?Working(?:\s*\()?${DURATION_STATUS_PATTERN}\b.*(?:esc to interrupt|interrupt)\)?$`, "i");
63682
+ var CODEX_INTERRUPT_FOOTER_PATTERN = new RegExp(String.raw`^(?:[•◦·]\s*)?${DURATION_STATUS_PATTERN}\s*[•◦·]?\s*esc to interrupt\)?$`, "i");
63683
+ var GEMINI_THINKING_STATUS_PATTERN = new RegExp(String.raw`^Thinking\.\.\. \(esc to cancel,\s*${DURATION_STATUS_PATTERN}\)$`, "i");
63684
+ var CLAUDE_WORKED_STATUS_PATTERN = new RegExp(String.raw`^(?:[✻✽*]\s*)?(?:Worked|Cooked) for ${DURATION_STATUS_PATTERN}$`, "i");
63685
+ var CLAUDE_TIMER_FOOTER_PATTERN = new RegExp(String.raw`\|\s*claude\s*\|.*\|\s*${DURATION_STATUS_PATTERN}\s*$`, "i");
63393
63686
  function looksLikeUrlContinuation(line) {
63394
63687
  const trimmed = line.trim();
63395
63688
  return trimmed.startsWith("(") || trimmed.startsWith("http://") || trimmed.startsWith("https://");
@@ -63587,7 +63880,14 @@ function isInterruptStatusLine(line) {
63587
63880
  if (!trimmed) {
63588
63881
  return false;
63589
63882
  }
63590
- return /^(?:[•◦·]\s*)?Working(?:\s*\()?\d+s\b.*(?:esc to interrupt|interrupt)\)?$/i.test(trimmed) || /^(?:[•◦·]\s*)?\d+s\s*[•◦·]?\s*esc to interrupt\)?$/i.test(trimmed);
63883
+ return CODEX_WORKING_STATUS_PATTERN.test(trimmed) || CODEX_INTERRUPT_FOOTER_PATTERN.test(trimmed);
63884
+ }
63885
+ function isTimerDrivenStatusLine(line) {
63886
+ const trimmed = line.trim();
63887
+ if (!trimmed) {
63888
+ return false;
63889
+ }
63890
+ return isInterruptStatusLine(trimmed) || GEMINI_THINKING_STATUS_PATTERN.test(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed) || CLAUDE_TIMER_FOOTER_PATTERN.test(trimmed);
63591
63891
  }
63592
63892
  function shouldDropCodexChromeLine(line) {
63593
63893
  const trimmed = line.trim();
@@ -63601,14 +63901,14 @@ function shouldDropClaudeChromeLine(line) {
63601
63901
  if (!trimmed) {
63602
63902
  return false;
63603
63903
  }
63604
- return trimmed.includes("Claude Code v") || trimmed.includes("Welcome back!") || trimmed.includes("Tips for getting started") || trimmed.includes("Ask Claude to create a new app or clone a repository") || trimmed.includes("Recent activity") || trimmed.includes("No recent activity") || trimmed.includes("API Usage Billing") || trimmed.includes("shift+tab to cycle") || trimmed.includes("ctrl+o to expand") || trimmed.includes("ctrl+b ctrl+b") || trimmed.includes("run in background") || /^~\/\.clisbot\/(?:workspace\/)?[a-z0-9._/-]+$/i.test(trimmed) || trimmed.includes("| claude |") || /^(?:[✻*]\s*)?(?:Worked|Cooked) for \d+s$/i.test(trimmed) || trimmed.startsWith("⏵⏵") || trimmed.startsWith("❯") || isProgressLine(trimmed) || /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/.test(trimmed) || /^[╭╰│]/.test(trimmed) || /^─+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+.+$/.test(trimmed);
63904
+ return trimmed.includes("Claude Code v") || trimmed.includes("Welcome back!") || trimmed.includes("Tips for getting started") || trimmed.includes("Ask Claude to create a new app or clone a repository") || trimmed.includes("Recent activity") || trimmed.includes("No recent activity") || trimmed.includes("API Usage Billing") || trimmed.includes("shift+tab to cycle") || trimmed.includes("ctrl+o to expand") || trimmed.includes("ctrl+b ctrl+b") || trimmed.includes("run in background") || /^~\/\.clisbot\/(?:workspace\/)?[a-z0-9._/-]+$/i.test(trimmed) || trimmed.includes("| claude |") || isTimerDrivenStatusLine(trimmed) || trimmed.startsWith("⏵⏵") || trimmed.startsWith("❯") || isProgressLine(trimmed) || /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/.test(trimmed) || /^[╭╰│]/.test(trimmed) || /^─+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+$/.test(trimmed) || /^[▐▛▜▌▝▘ ]+.+$/.test(trimmed);
63605
63905
  }
63606
63906
  function shouldDropGeminiChromeLine(line) {
63607
63907
  const trimmed = line.trim();
63608
63908
  if (!trimmed) {
63609
63909
  return false;
63610
63910
  }
63611
- return trimmed.includes("Gemini CLI v") || trimmed.includes("Signed in with Google") || trimmed.includes("Plan:") || /^[▝▜▄▗▟▀ ]+$/.test(trimmed) || trimmed.includes("We're making changes to Gemini CLI") || trimmed.includes("What's Changing:") || trimmed.includes("How it affects you:") || trimmed.includes("Read more: https://goo.gle/geminicli-updates") || trimmed.includes("Skipping project agents due to untrusted folder.") || trimmed.includes("Do you trust the files in this folder?") || trimmed.includes("Trusting a folder allows Gemini CLI to load its local configurations") || trimmed === "1. Trust folder (default)" || trimmed === "2. Trust parent folder (workspaces)" || trimmed === "3. Don't trust" || trimmed.includes("Tips for getting started") || /^Create GEMINI\.md files to customize your interactions$/i.test(trimmed) || /^\/help for more information$/i.test(trimmed) || /^Ask coding questions, edit code or run commands$/i.test(trimmed) || /^Be specific for the best results$/i.test(trimmed) || trimmed.includes("? for shortcuts") || trimmed.includes("YOLO Ctrl+Y") || trimmed.includes("Type your message or @path/to/file") || trimmed.includes("workspace (/directory)") || /^~\/.+\s+\S+\s+no sandbox\s+\S+/i.test(trimmed) || /^Thinking\.\.\. \(esc to cancel,\s*\d+s\)$/i.test(trimmed) || /^[╭╰│]/.test(trimmed) || /^[-▀▄]{10,}$/.test(trimmed) || /^─+$/.test(trimmed);
63911
+ return trimmed.includes("Gemini CLI v") || trimmed.includes("Signed in with Google") || trimmed.includes("Plan:") || /^[▝▜▄▗▟▀ ]+$/.test(trimmed) || trimmed.includes("We're making changes to Gemini CLI") || trimmed.includes("What's Changing:") || trimmed.includes("How it affects you:") || trimmed.includes("Read more: https://goo.gle/geminicli-updates") || trimmed.includes("Skipping project agents due to untrusted folder.") || trimmed.includes("Do you trust the files in this folder?") || trimmed.includes("Trusting a folder allows Gemini CLI to load its local configurations") || trimmed === "1. Trust folder (default)" || trimmed === "2. Trust parent folder (workspaces)" || trimmed === "3. Don't trust" || trimmed.includes("Tips for getting started") || /^Create GEMINI\.md files to customize your interactions$/i.test(trimmed) || /^\/help for more information$/i.test(trimmed) || /^Ask coding questions, edit code or run commands$/i.test(trimmed) || /^Be specific for the best results$/i.test(trimmed) || trimmed.includes("? for shortcuts") || trimmed.includes("YOLO Ctrl+Y") || trimmed.includes("Type your message or @path/to/file") || trimmed.includes("workspace (/directory)") || /^~\/.+\s+\S+\s+no sandbox\s+\S+/i.test(trimmed) || isTimerDrivenStatusLine(trimmed) || /^[╭╰│]/.test(trimmed) || /^[-▀▄]{10,}$/.test(trimmed) || /^─+$/.test(trimmed);
63612
63912
  }
63613
63913
  function normalizeBoundaryLine(line) {
63614
63914
  return line.trim().replace(/^(?::eight_spoked_asterisk:|[-*•◦·✽✶])\s+/, "");
@@ -63642,16 +63942,21 @@ function collapseAdjacentDuplicateLines(raw) {
63642
63942
  return collapseBlankLines(collapsed).join(`
63643
63943
  `).trim();
63644
63944
  }
63645
- function cleanInteractionSnapshot(raw) {
63945
+ function cleanInteractionSnapshotInternal(raw, options) {
63646
63946
  const lines = splitNormalizedLines(raw);
63647
63947
  const isCodex = looksLikeCodexSnapshot(lines);
63648
63948
  const isClaude = looksLikeClaudeSnapshot(lines);
63649
63949
  const isGemini = looksLikeGeminiSnapshot(lines);
63650
63950
  const promptStripped = isCodex ? dropCodexPromptBlocks(lines) : isClaude ? dropClaudePromptBlocks(lines) : isGemini ? dropGeminiPromptBlocks(lines) : lines;
63951
+ const timerStatusLines = [];
63651
63952
  const filtered = promptStripped.filter((line) => {
63652
63953
  if (shouldDropDeliveryReportLine(line)) {
63653
63954
  return false;
63654
63955
  }
63956
+ if (options?.preserveTimerStatusLines && isTimerDrivenStatusLine(line)) {
63957
+ timerStatusLines.push(line.trim());
63958
+ return false;
63959
+ }
63655
63960
  if (isCodex && shouldDropCodexChromeLine(line)) {
63656
63961
  return false;
63657
63962
  }
@@ -63665,8 +63970,21 @@ function cleanInteractionSnapshot(raw) {
63665
63970
  });
63666
63971
  const normalized = isCodex ? unwrapCodexMessageBlocks(filtered) : isClaude ? unwrapClaudeMessageBlocks(filtered) : isGemini ? filtered.map((line) => line.replace(/^\s*>\s*/, "")) : filtered;
63667
63972
  const unwrapped = unwrapSoftWrappedLines(normalized);
63668
- return collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
63973
+ const cleanedBody = collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(unwrapped)).join(`
63669
63974
  `));
63975
+ const cleanedTimerStatus = options?.preserveTimerStatusLines ? collapseAdjacentDuplicateLines(collapseBlankLines(trimBlankLines(timerStatusLines)).join(`
63976
+ `)) : "";
63977
+ return [cleanedBody, cleanedTimerStatus].filter(Boolean).join(`
63978
+
63979
+ `).trim();
63980
+ }
63981
+ function cleanInteractionSnapshot(raw) {
63982
+ return cleanInteractionSnapshotInternal(raw);
63983
+ }
63984
+ function cleanRunningInteractionSnapshot(raw) {
63985
+ return cleanInteractionSnapshotInternal(raw, {
63986
+ preserveTimerStatusLines: true
63987
+ });
63670
63988
  }
63671
63989
  // src/shared/transcript-delta.ts
63672
63990
  function diffText(previous, current) {
@@ -63727,16 +64045,8 @@ function extractScrolledAppend(previous, current) {
63727
64045
  }
63728
64046
  return "";
63729
64047
  }
63730
- function deriveRunningInteractionText(previousSnapshot, currentSnapshot) {
63731
- const previous = cleanInteractionSnapshot(previousSnapshot);
63732
- const current = cleanInteractionSnapshot(currentSnapshot);
63733
- if (!current || current === previous) {
63734
- return "";
63735
- }
63736
- if (!previous) {
63737
- return current;
63738
- }
63739
- return extractScrolledAppend(previous, current);
64048
+ function deriveRunningInteractionSnapshot(currentSnapshot) {
64049
+ return cleanRunningInteractionSnapshot(currentSnapshot);
63740
64050
  }
63741
64051
  function deriveInteractionText(initialSnapshot, currentSnapshot) {
63742
64052
  const previous = cleanInteractionSnapshot(initialSnapshot);
@@ -63940,6 +64250,36 @@ function renderInteractionBody(params) {
63940
64250
  }
63941
64251
  return truncateTail(completedBody, params.maxChars);
63942
64252
  }
64253
+ function normalizeLeadingStatusLine(line) {
64254
+ return line.trim().replace(/^[_*`\s]+|[_*`\s]+$/g, "");
64255
+ }
64256
+ function startsWithExplicitErrorLabel(body) {
64257
+ const firstLine = body.split(`
64258
+ `).map((line) => normalizeLeadingStatusLine(line)).find((line) => line.length > 0);
64259
+ if (!firstLine) {
64260
+ return false;
64261
+ }
64262
+ return /^(error|failed|failure|denied|forbidden)(?::|\.)/i.test(firstLine);
64263
+ }
64264
+ function shouldInlineErrorPrefix(body) {
64265
+ return !body.includes(`
64266
+ `) && !body.includes("```");
64267
+ }
64268
+ function renderErrorInteractionBody(body, footer) {
64269
+ const trimmedBody = body.trim();
64270
+ if (!trimmedBody) {
64271
+ return footer;
64272
+ }
64273
+ if (startsWithExplicitErrorLabel(trimmedBody)) {
64274
+ return trimmedBody;
64275
+ }
64276
+ if (shouldInlineErrorPrefix(trimmedBody)) {
64277
+ return `Error: ${trimmedBody}`;
64278
+ }
64279
+ return `${trimmedBody}
64280
+
64281
+ ${footer}`;
64282
+ }
63943
64283
  function renderSlackInteraction(params) {
63944
64284
  const body = renderInteractionBody(params);
63945
64285
  if (params.status === "queued") {
@@ -63967,9 +64307,7 @@ _Timed out waiting for more output._` : "_Timed out waiting for visible output._
63967
64307
  _${note}_` : `_${note}_`;
63968
64308
  }
63969
64309
  if (params.status === "error") {
63970
- return body ? `${body}
63971
-
63972
- _Error._` : "_Error._";
64310
+ return renderErrorInteractionBody(body, "_Error._");
63973
64311
  }
63974
64312
  return body || "_Completed with no new visible output._";
63975
64313
  }
@@ -64000,9 +64338,7 @@ Timed out waiting for more output.` : "Timed out waiting for visible output.";
64000
64338
  ${note}` : note;
64001
64339
  }
64002
64340
  if (params.status === "error") {
64003
- return body ? `${body}
64004
-
64005
- Error.` : "Error.";
64341
+ return renderErrorInteractionBody(body, "Error.");
64006
64342
  }
64007
64343
  return body || "Completed with no new visible output.";
64008
64344
  }
@@ -64452,6 +64788,7 @@ var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
64452
64788
  var TMUX_TRANSIENT_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window)/i;
64453
64789
  var SESSION_READY_CAPTURE_RETRY_COUNT = 5;
64454
64790
  var SESSION_READY_CAPTURE_RETRY_DELAY_MS = 100;
64791
+ var SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS = 15000;
64455
64792
  function summarizeSnapshot(snapshot) {
64456
64793
  const compact = snapshot.split(`
64457
64794
  `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
@@ -64483,6 +64820,7 @@ class RunnerService {
64483
64820
  sessionState;
64484
64821
  resolveTarget;
64485
64822
  cleanupInFlight = false;
64823
+ sessionIdentityCaptureRetryAt = new Map;
64486
64824
  constructor(loadedConfig, tmux, sessionState, resolveTarget) {
64487
64825
  this.loadedConfig = loadedConfig;
64488
64826
  this.tmux = tmux;
@@ -64534,12 +64872,24 @@ class RunnerService {
64534
64872
  async syncSessionIdentity(resolved) {
64535
64873
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
64536
64874
  if (existing?.sessionId) {
64875
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64537
64876
  return this.sessionState.touchSessionEntry(resolved, {
64538
64877
  sessionId: existing.sessionId,
64539
64878
  runnerCommand: resolved.runner.command
64540
64879
  });
64541
64880
  }
64881
+ const retryAt = this.sessionIdentityCaptureRetryAt.get(resolved.sessionKey) ?? 0;
64882
+ if (retryAt > Date.now()) {
64883
+ return this.sessionState.touchSessionEntry(resolved, {
64884
+ runnerCommand: resolved.runner.command
64885
+ });
64886
+ }
64542
64887
  const sessionId = await this.captureSessionIdentity(resolved);
64888
+ if (sessionId) {
64889
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64890
+ } else {
64891
+ this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
64892
+ }
64543
64893
  return this.sessionState.touchSessionEntry(resolved, {
64544
64894
  sessionId,
64545
64895
  runnerCommand: resolved.runner.command
@@ -64694,6 +65044,7 @@ class RunnerService {
64694
65044
  sessionName: resolved.sessionName,
64695
65045
  stateDir: this.loadedConfig.stateDir
64696
65046
  });
65047
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
64697
65048
  try {
64698
65049
  await this.tmux.newSession({
64699
65050
  sessionName: resolved.sessionName,
@@ -64936,15 +65287,19 @@ class RunnerService {
64936
65287
  }
64937
65288
 
64938
65289
  // src/agents/run-recovery.ts
65290
+ var MID_RUN_RECOVERY_MAX_ATTEMPTS = 2;
65291
+ var MID_RUN_RECOVERY_CONTINUE_PROMPT = "continue exactly where you left off";
64939
65292
  function mergeRunSnapshot(snapshotPrefix, snapshot) {
64940
65293
  return appendInteractionText(snapshotPrefix, snapshot);
64941
65294
  }
64942
- function buildRunRecoveryNote(kind) {
65295
+ function buildRunRecoveryNote(kind, params) {
64943
65296
  if (kind === "resume-attempt") {
64944
- return "Runner session was lost. Attempting recovery 1/2 by reopening the same conversation context.";
65297
+ const attempt = params?.attempt ?? 1;
65298
+ const maxAttempts = params?.maxAttempts ?? MID_RUN_RECOVERY_MAX_ATTEMPTS;
65299
+ return `Runner session was lost. Attempting recovery ${attempt}/${maxAttempts} by reopening the same conversation context.`;
64945
65300
  }
64946
65301
  if (kind === "resume-success") {
64947
- return "Recovery succeeded. Continuing the current run.";
65302
+ return "Recovery succeeded. Asking the runner to continue exactly where it left off.";
64948
65303
  }
64949
65304
  if (kind === "fresh-attempt") {
64950
65305
  return "The previous runner session could not be resumed. Opening a fresh runner session 2/2 without replaying your prompt.";
@@ -64956,11 +65311,12 @@ function buildRunRecoveryNote(kind) {
64956
65311
  var FIRST_OUTPUT_POLL_INTERVAL_MS = 250;
64957
65312
  async function monitorTmuxRun(params) {
64958
65313
  let previousSnapshot = params.initialSnapshot;
64959
- let lastChangeAt = Date.now();
64960
- let sawChange = false;
64961
- let cumulativeInteractionSnapshot = "";
65314
+ let previousRunningSnapshot = "";
65315
+ let lastActivityAt = params.startedAt;
65316
+ let sawActivity = false;
64962
65317
  let detachedNotified = params.detachedAlready;
64963
65318
  let firstMeaningfulDeltaLogged = false;
65319
+ let noOutputThresholdLogged = false;
64964
65320
  if (params.prompt) {
64965
65321
  logLatencyDebug("tmux-submit-start", params.timingContext, {
64966
65322
  sessionName: params.sessionName,
@@ -64981,61 +65337,57 @@ async function monitorTmuxRun(params) {
64981
65337
  });
64982
65338
  }
64983
65339
  while (true) {
64984
- await sleep(sawChange ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
65340
+ await sleep(sawActivity ? params.updateIntervalMs : Math.min(params.updateIntervalMs, FIRST_OUTPUT_POLL_INTERVAL_MS));
64985
65341
  const snapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, params.captureLines));
64986
65342
  const now = Date.now();
64987
- if (snapshot !== previousSnapshot) {
64988
- const priorSnapshot = previousSnapshot;
64989
- lastChangeAt = now;
64990
- previousSnapshot = snapshot;
64991
- const interactionDelta = deriveRunningInteractionText(priorSnapshot, snapshot);
64992
- const nextInteractionSnapshot = appendInteractionText(cumulativeInteractionSnapshot, interactionDelta);
64993
- if (nextInteractionSnapshot && nextInteractionSnapshot !== cumulativeInteractionSnapshot) {
64994
- sawChange = true;
64995
- cumulativeInteractionSnapshot = nextInteractionSnapshot;
64996
- if (!firstMeaningfulDeltaLogged) {
64997
- firstMeaningfulDeltaLogged = true;
64998
- logLatencyDebug("tmux-first-meaningful-delta", params.timingContext, {
64999
- sessionName: params.sessionName,
65000
- elapsedMs: now - params.startedAt
65001
- });
65002
- }
65003
- await params.onRunning({
65004
- snapshot: cumulativeInteractionSnapshot,
65005
- fullSnapshot: snapshot,
65006
- initialSnapshot: params.initialSnapshot
65343
+ const runningSnapshot = deriveRunningInteractionSnapshot(snapshot);
65344
+ previousSnapshot = snapshot;
65345
+ if (runningSnapshot && runningSnapshot !== previousRunningSnapshot) {
65346
+ previousRunningSnapshot = runningSnapshot;
65347
+ lastActivityAt = now;
65348
+ sawActivity = true;
65349
+ if (!firstMeaningfulDeltaLogged) {
65350
+ firstMeaningfulDeltaLogged = true;
65351
+ logLatencyDebug("tmux-first-meaningful-delta", params.timingContext, {
65352
+ sessionName: params.sessionName,
65353
+ elapsedMs: now - params.startedAt
65007
65354
  });
65008
65355
  }
65356
+ await params.onRunning({
65357
+ snapshot: runningSnapshot,
65358
+ fullSnapshot: snapshot,
65359
+ initialSnapshot: params.initialSnapshot
65360
+ });
65009
65361
  }
65010
65362
  if (!detachedNotified && now - params.startedAt >= params.maxRuntimeMs) {
65011
65363
  detachedNotified = true;
65012
65364
  await params.onDetached({
65013
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65365
+ snapshot: previousRunningSnapshot || deriveInteractionText(params.initialSnapshot, snapshot),
65014
65366
  fullSnapshot: previousSnapshot,
65015
65367
  initialSnapshot: params.initialSnapshot
65016
65368
  });
65017
65369
  }
65018
- if (sawChange && now - lastChangeAt >= params.idleTimeoutMs) {
65370
+ if (sawActivity && now - lastActivityAt >= params.idleTimeoutMs) {
65019
65371
  await params.onCompleted({
65020
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65372
+ snapshot: deriveInteractionText(params.initialSnapshot, previousSnapshot),
65021
65373
  fullSnapshot: previousSnapshot,
65022
65374
  initialSnapshot: params.initialSnapshot
65023
65375
  });
65024
65376
  return;
65025
65377
  }
65026
- if (!sawChange && now - params.startedAt >= params.noOutputTimeoutMs) {
65027
- await params.onTimeout({
65028
- snapshot: cumulativeInteractionSnapshot || deriveInteractionText(params.initialSnapshot, previousSnapshot),
65029
- fullSnapshot: previousSnapshot,
65030
- initialSnapshot: params.initialSnapshot
65378
+ if (!noOutputThresholdLogged && !sawActivity && now - params.startedAt >= params.noOutputTimeoutMs) {
65379
+ noOutputThresholdLogged = true;
65380
+ logLatencyDebug("tmux-no-output-threshold-crossed", params.timingContext, {
65381
+ sessionName: params.sessionName,
65382
+ elapsedMs: now - params.startedAt
65031
65383
  });
65032
- return;
65033
65384
  }
65034
65385
  }
65035
65386
  }
65036
65387
 
65037
65388
  // src/agents/session-service.ts
65038
65389
  var OBSERVER_RETRYABLE_FAILURE_LIMIT = 3;
65390
+ var DETACHED_OBSERVER_INTERVAL_MS = 5 * 60000;
65039
65391
  function formatObserverError(error) {
65040
65392
  return error instanceof Error ? error.stack ?? error.message : String(error);
65041
65393
  }
@@ -65283,7 +65635,10 @@ class SessionService {
65283
65635
  detached: false
65284
65636
  };
65285
65637
  }
65286
- observer.mode = "passive-final";
65638
+ observer.mode = "poll";
65639
+ observer.intervalMs = DETACHED_OBSERVER_INTERVAL_MS;
65640
+ observer.expiresAt = undefined;
65641
+ observer.lastSentAt = Date.now();
65287
65642
  run.observerFailures.delete(observerId);
65288
65643
  return {
65289
65644
  detached: true
@@ -65306,7 +65661,19 @@ class SessionService {
65306
65661
  this.activeRuns.clear();
65307
65662
  }
65308
65663
  buildDetachedNote(resolved) {
65309
- return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot will keep monitoring it and will post the final result here when it completes. Use \`/attach\` to resume live updates, \`/watch every 30s\` for interval updates, or \`/stop\` to interrupt it.`;
65664
+ return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot will keep monitoring it, switch this thread to sparse progress updates, and post the final result here when it completes. Use \`/attach\` to resume live updates, \`/watch every 30s\` for interval updates, or \`/stop\` to interrupt it.`;
65665
+ }
65666
+ applyDetachedObserverPolicy(run) {
65667
+ const now = Date.now();
65668
+ for (const observer of run.observers.values()) {
65669
+ if (observer.mode !== "live") {
65670
+ continue;
65671
+ }
65672
+ observer.mode = "poll";
65673
+ observer.intervalMs = DETACHED_OBSERVER_INTERVAL_MS;
65674
+ observer.expiresAt = undefined;
65675
+ observer.lastSentAt = now;
65676
+ }
65310
65677
  }
65311
65678
  createRunUpdate(params) {
65312
65679
  return {
@@ -65416,10 +65783,13 @@ class SessionService {
65416
65783
  agentId: run.resolved.agentId,
65417
65784
  sessionKey: run.resolved.sessionKey
65418
65785
  };
65786
+ const recoveryAttempt = params.recoveryAttempt ?? 1;
65419
65787
  const snapshotPrefix = run.latestUpdate.snapshot;
65420
- const previousFullSnapshot = run.latestUpdate.fullSnapshot;
65421
65788
  const detachedAlready = run.latestUpdate.status === "detached";
65422
- await this.notifyRecoveryStep(run, buildRunRecoveryNote("resume-attempt"));
65789
+ await this.notifyRecoveryStep(run, buildRunRecoveryNote("resume-attempt", {
65790
+ attempt: recoveryAttempt,
65791
+ maxAttempts: MID_RUN_RECOVERY_MAX_ATTEMPTS
65792
+ }));
65423
65793
  try {
65424
65794
  const recovered = await this.runnerSessions.reopenRunContext(target, params.timingContext);
65425
65795
  const currentRun = this.activeRuns.get(sessionKey);
@@ -65438,15 +65808,22 @@ class SessionService {
65438
65808
  });
65439
65809
  await this.notifyRunObservers(currentRun, currentRun.latestUpdate);
65440
65810
  this.startRunMonitor(sessionKey, {
65441
- prompt: undefined,
65442
- initialSnapshot: previousFullSnapshot,
65811
+ prompt: MID_RUN_RECOVERY_CONTINUE_PROMPT,
65812
+ initialSnapshot: recovered.initialSnapshot,
65443
65813
  startedAt: currentRun.startedAt,
65444
65814
  detachedAlready,
65445
65815
  timingContext: params.timingContext,
65446
- snapshotPrefix
65816
+ snapshotPrefix,
65817
+ recoveryAttempt
65447
65818
  });
65448
65819
  return true;
65449
- } catch {
65820
+ } catch (reopenError) {
65821
+ if (recoveryAttempt < MID_RUN_RECOVERY_MAX_ATTEMPTS && this.runnerSessions.canRecoverMidRun(reopenError)) {
65822
+ return await this.recoverLostMidRun(sessionKey, {
65823
+ timingContext: params.timingContext,
65824
+ recoveryAttempt: recoveryAttempt + 1
65825
+ }, reopenError);
65826
+ }
65450
65827
  const currentRun = this.activeRuns.get(sessionKey);
65451
65828
  if (!currentRun) {
65452
65829
  return true;
@@ -65527,7 +65904,8 @@ class SessionService {
65527
65904
  startedAt: params.startedAt,
65528
65905
  detachedAt: Date.now()
65529
65906
  });
65530
- currentRun.latestUpdate = detachedUpdate;
65907
+ await this.notifyRunObservers(currentRun, detachedUpdate);
65908
+ this.applyDetachedObserverPolicy(currentRun);
65531
65909
  currentRun.initialResult.resolve(detachedUpdate);
65532
65910
  },
65533
65911
  onCompleted: async (update) => {
@@ -65539,20 +65917,13 @@ class SessionService {
65539
65917
  initialSnapshot: update.initialSnapshot
65540
65918
  });
65541
65919
  await this.finishActiveRun(sessionKey, runUpdate);
65542
- },
65543
- onTimeout: async (update) => {
65544
- const runUpdate = this.createRunUpdate({
65545
- resolved: run.resolved,
65546
- status: "timeout",
65547
- snapshot: mergeRunSnapshot(params.snapshotPrefix ?? "", update.snapshot),
65548
- fullSnapshot: update.fullSnapshot,
65549
- initialSnapshot: update.initialSnapshot
65550
- });
65551
- await this.finishActiveRun(sessionKey, runUpdate);
65552
65920
  }
65553
65921
  });
65554
65922
  } catch (error) {
65555
- if (await this.recoverLostMidRun(sessionKey, { timingContext: params.timingContext }, error)) {
65923
+ if (await this.recoverLostMidRun(sessionKey, {
65924
+ timingContext: params.timingContext,
65925
+ recoveryAttempt: (params.recoveryAttempt ?? 0) + 1
65926
+ }, error)) {
65556
65927
  return;
65557
65928
  }
65558
65929
  await this.failActiveRun(sessionKey, await this.runnerSessions.mapRunError(error, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
@@ -65562,6 +65933,28 @@ class SessionService {
65562
65933
  }
65563
65934
 
65564
65935
  // src/agents/agent-service.ts
65936
+ function shellQuote3(value) {
65937
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
65938
+ return value;
65939
+ }
65940
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
65941
+ }
65942
+ function buildCommandString2(command, args) {
65943
+ return [command, ...args].map(shellQuote3).join(" ");
65944
+ }
65945
+ function stripWorkspaceArgs(args) {
65946
+ const filtered = [];
65947
+ for (let index = 0;index < args.length; index += 1) {
65948
+ const current = args[index];
65949
+ if (current === "-C") {
65950
+ index += 1;
65951
+ continue;
65952
+ }
65953
+ filtered.push(current);
65954
+ }
65955
+ return filtered;
65956
+ }
65957
+
65565
65958
  class AgentService {
65566
65959
  loadedConfig;
65567
65960
  tmuxClient;
@@ -65652,6 +66045,15 @@ class AgentService {
65652
66045
  async getSessionRuntime(target) {
65653
66046
  return this.sessionState.getSessionRuntime(target);
65654
66047
  }
66048
+ async getSessionDiagnostics(target) {
66049
+ const resolved = this.resolveTarget(target);
66050
+ const entry = await this.sessionState.getEntry(target.sessionKey);
66051
+ const sessionId = entry?.sessionId?.trim() || undefined;
66052
+ return {
66053
+ sessionId,
66054
+ resumeCommand: this.buildResumeCommandPreview(resolved, sessionId)
66055
+ };
66056
+ }
65655
66057
  async listActiveSessionRuntimes() {
65656
66058
  return this.sessionState.listActiveSessionRuntimes();
65657
66059
  }
@@ -65894,6 +66296,21 @@ class AgentService {
65894
66296
  this.scheduleIntervalLoopTimer(persisted.id, Math.max(0, persisted.nextRunAt - Date.now()));
65895
66297
  }
65896
66298
  }
66299
+ buildResumeCommandPreview(resolved, sessionId) {
66300
+ if (!sessionId || resolved.runner.sessionId.resume.mode !== "command") {
66301
+ return;
66302
+ }
66303
+ const values = {
66304
+ agentId: resolved.agentId,
66305
+ workspace: resolved.workspacePath,
66306
+ sessionName: resolved.sessionName,
66307
+ sessionKey: resolved.sessionKey,
66308
+ sessionId
66309
+ };
66310
+ const command = resolved.runner.sessionId.resume.command ?? resolved.runner.command;
66311
+ const args = stripWorkspaceArgs(resolved.runner.sessionId.resume.args.map((value) => applyTemplate(value, values)));
66312
+ return buildCommandString2(command, args);
66313
+ }
65897
66314
  async isManagedLoopPersisted(managed) {
65898
66315
  const entry = await this.sessionState.getEntry(managed.target.sessionKey);
65899
66316
  return (entry?.intervalLoops ?? []).some((loop) => loop.id === managed.loop.id);
@@ -66398,6 +66815,12 @@ function parseAgentCommand(text, options = {}) {
66398
66815
  if (lowered === "loop") {
66399
66816
  const loopText = withoutSlash.slice(command.length).trim();
66400
66817
  const loweredLoopText = loopText.toLowerCase();
66818
+ if (!loweredLoopText || loweredLoopText === "help") {
66819
+ return {
66820
+ type: "control",
66821
+ name: "loop-help"
66822
+ };
66823
+ }
66401
66824
  if (loweredLoopText === "status") {
66402
66825
  return {
66403
66826
  type: "loop-control",
@@ -66445,6 +66868,12 @@ function parseAgentCommand(text, options = {}) {
66445
66868
  const queueText = withoutSlash.slice(command.length).trim();
66446
66869
  const normalizedQueueText = queueText.toLowerCase();
66447
66870
  if (lowered === "queue") {
66871
+ if (normalizedQueueText === "help") {
66872
+ return {
66873
+ type: "control",
66874
+ name: "queue-help"
66875
+ };
66876
+ }
66448
66877
  if (normalizedQueueText === "list") {
66449
66878
  return {
66450
66879
  type: "control",
@@ -66511,7 +66940,7 @@ function renderAgentControlSlashHelp() {
66511
66940
  "- `/whoami`: show the current platform, route, and sender identity details",
66512
66941
  "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
66513
66942
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
66514
- "- `/detach`: stop live updates for this thread while still allowing final settlement here",
66943
+ "- `/detach`: stop live updates for this thread, switch to sparse progress updates, and still allow final settlement here",
66515
66944
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
66516
66945
  "- `/stop`: send Escape to interrupt the current conversation session",
66517
66946
  "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
@@ -66528,9 +66957,11 @@ function renderAgentControlSlashHelp() {
66528
66957
  "- `/additionalmessagemode steer`: send later user messages straight into the active session",
66529
66958
  "- `/additionalmessagemode queue`: queue later user messages behind the active run for this surface",
66530
66959
  "- `/queue <message>` or `\\q <message>`: enqueue a later message behind the active run and let clisbot deliver it in order",
66960
+ "- `/queue help`: show queue-specific help and examples",
66531
66961
  "- `/steer <message>` or `\\s <message>`: inject a steering message into the active run immediately",
66532
66962
  "- `/queue list`: show queued messages that have not started yet",
66533
66963
  "- `/queue clear`: clear queued messages that have not started yet",
66964
+ "- `/loop help`: show loop-specific help and syntax examples",
66534
66965
  ...renderLoopHelpLines(),
66535
66966
  "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
66536
66967
  "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
@@ -66539,6 +66970,15 @@ function renderAgentControlSlashHelp() {
66539
66970
  ].join(`
66540
66971
  `);
66541
66972
  }
66973
+ function renderQueueHelpLines() {
66974
+ return [
66975
+ "- `/queue <message>` or `\\q <message>`: enqueue one later message behind the active run",
66976
+ "- `/queue list`: show queued messages that have not started yet",
66977
+ "- `/queue clear`: clear queued messages that have not started yet",
66978
+ "- `/queue help`: show this queue help again",
66979
+ "- `/steer <message>` or `\\s <message>`: inject an immediate steering message instead of queueing"
66980
+ ];
66981
+ }
66542
66982
  function parseWatchCommand(raw) {
66543
66983
  const match = raw.match(/^every\s+(\S+)(?:\s+for\s+(\S+))?$/i);
66544
66984
  if (!match) {
@@ -66760,7 +67200,8 @@ function renderWhoAmIMessage(params) {
66760
67200
  `platform: \`${params.identity.platform}\``,
66761
67201
  `conversationKind: \`${params.identity.conversationKind}\``,
66762
67202
  `agentId: \`${params.route.agentId}\``,
66763
- `sessionKey: \`${params.sessionTarget.sessionKey}\``
67203
+ `sessionKey: \`${params.sessionTarget.sessionKey}\``,
67204
+ `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
66764
67205
  ];
66765
67206
  if (params.identity.senderId) {
66766
67207
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -66777,7 +67218,7 @@ function renderWhoAmIMessage(params) {
66777
67218
  if (params.identity.topicId) {
66778
67219
  lines.push(`topicId: \`${params.identity.topicId}\``);
66779
67220
  }
66780
- lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
67221
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
66781
67222
  return lines.join(`
66782
67223
  `);
66783
67224
  }
@@ -66788,7 +67229,8 @@ function renderRouteStatusMessage(params) {
66788
67229
  `platform: \`${params.identity.platform}\``,
66789
67230
  `conversationKind: \`${params.identity.conversationKind}\``,
66790
67231
  `agentId: \`${params.route.agentId}\``,
66791
- `sessionKey: \`${params.sessionTarget.sessionKey}\``
67232
+ `sessionKey: \`${params.sessionTarget.sessionKey}\``,
67233
+ `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
66792
67234
  ];
66793
67235
  if (params.identity.senderId) {
66794
67236
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -66805,7 +67247,7 @@ function renderRouteStatusMessage(params) {
66805
67247
  if (params.identity.topicId) {
66806
67248
  lines.push(`topicId: \`${params.identity.topicId}\``);
66807
67249
  }
66808
- lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
67250
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone: \`${params.route.timezone ?? "(inherit host/app)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
66809
67251
  if (params.runtimeState.startedAt) {
66810
67252
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
66811
67253
  }
@@ -66819,7 +67261,7 @@ function renderRouteStatusMessage(params) {
66819
67261
  lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
66820
67262
  }
66821
67263
  }
66822
- lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/streaming status|on|off|latest|all`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/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`");
67264
+ lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/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`");
66823
67265
  return lines.join(`
66824
67266
  `);
66825
67267
  }
@@ -66877,25 +67319,6 @@ function buildChannelObserverId(identity) {
66877
67319
  identity.topicId ?? ""
66878
67320
  ].join(":");
66879
67321
  }
66880
- function buildSteeringMessage(text, protectedControlMutationRule) {
66881
- const systemLines = [
66882
- "A new user message arrived while you were still working.",
66883
- "Adjust your current work if needed and continue."
66884
- ];
66885
- if (protectedControlMutationRule) {
66886
- systemLines.push("", protectedControlMutationRule);
66887
- }
66888
- return [
66889
- "<system>",
66890
- ...systemLines,
66891
- "</system>",
66892
- "",
66893
- "<user>",
66894
- text,
66895
- "</user>"
66896
- ].join(`
66897
- `);
66898
- }
66899
67322
  function renderQueuedMessagesList(items) {
66900
67323
  if (items.length === 0) {
66901
67324
  return "Queue is empty.";
@@ -66910,9 +67333,22 @@ function renderQueuedMessagesList(items) {
66910
67333
  return lines.join(`
66911
67334
  `).trimEnd();
66912
67335
  }
67336
+ function renderQueueUsage() {
67337
+ return [
67338
+ "Queue commands",
67339
+ "",
67340
+ ...renderQueueHelpLines(),
67341
+ "",
67342
+ "Notes:",
67343
+ "- use queue when the current run should finish first and the next user request can wait in order",
67344
+ "- use steer when the active run should be nudged or redirected immediately"
67345
+ ].join(`
67346
+ `);
67347
+ }
66913
67348
  function renderLoopUsage() {
66914
67349
  return [
66915
67350
  "Usage:",
67351
+ "- `/loop help`",
66916
67352
  "- `/loop 5m check CI`",
66917
67353
  "- `/loop 1m --force check CI`",
66918
67354
  "- `/loop 5m`",
@@ -67463,6 +67899,7 @@ async function processChannelInteraction(params) {
67463
67899
  };
67464
67900
  let replyRecorded = false;
67465
67901
  let renderChain = Promise.resolve();
67902
+ const sessionDiagnostics = await params.agentService.getSessionDiagnostics?.(params.sessionTarget) ?? {};
67466
67903
  async function recordReplyIfNeeded() {
67467
67904
  if (replyRecorded) {
67468
67905
  return;
@@ -67537,6 +67974,7 @@ async function processChannelInteraction(params) {
67537
67974
  route: params.route,
67538
67975
  auth,
67539
67976
  sessionTarget: params.sessionTarget,
67977
+ sessionDiagnostics,
67540
67978
  followUpState,
67541
67979
  runtimeState,
67542
67980
  loopState: {
@@ -67557,7 +67995,8 @@ async function processChannelInteraction(params) {
67557
67995
  identity: params.identity,
67558
67996
  route: params.route,
67559
67997
  auth,
67560
- sessionTarget: params.sessionTarget
67998
+ sessionTarget: params.sessionTarget,
67999
+ sessionDiagnostics
67561
68000
  }));
67562
68001
  await params.agentService.recordConversationReply(params.sessionTarget);
67563
68002
  return interactionResult;
@@ -67714,6 +68153,11 @@ async function processChannelInteraction(params) {
67714
68153
  await params.agentService.recordConversationReply(params.sessionTarget);
67715
68154
  return interactionResult;
67716
68155
  }
68156
+ if (slashCommand.name === "queue-help") {
68157
+ await params.postText(renderQueueUsage());
68158
+ await params.agentService.recordConversationReply(params.sessionTarget);
68159
+ return interactionResult;
68160
+ }
67717
68161
  if (slashCommand.name === "queue-clear") {
67718
68162
  const clearedCount = params.agentService.clearQueuedPrompts?.(params.sessionTarget) ?? 0;
67719
68163
  await params.postText(clearedCount > 0 ? `Cleared ${clearedCount} queued message${clearedCount === 1 ? "" : "s"}.` : "Queue was already empty.");
@@ -67721,6 +68165,11 @@ async function processChannelInteraction(params) {
67721
68165
  return interactionResult;
67722
68166
  }
67723
68167
  }
68168
+ if (slashCommand?.type === "control" && slashCommand.name === "loop-help") {
68169
+ await params.postText(renderLoopUsage());
68170
+ await params.agentService.recordConversationReply(params.sessionTarget);
68171
+ return interactionResult;
68172
+ }
67724
68173
  if (slashCommand?.type === "loop-control") {
67725
68174
  if (slashCommand.action === "status") {
67726
68175
  await params.postText(renderLoopStatusMessage({
@@ -67943,7 +68392,10 @@ ${escapeCodeFence(shellResult.output)}
67943
68392
  await params.agentService.recordConversationReply(params.sessionTarget);
67944
68393
  return interactionResult;
67945
68394
  }
67946
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage, params.protectedControlMutationRule));
68395
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringPromptText({
68396
+ text: explicitSteerMessage,
68397
+ protectedControlMutationRule: params.protectedControlMutationRule
68398
+ }));
67947
68399
  await params.postText("Steered.");
67948
68400
  await params.agentService.recordConversationReply(params.sessionTarget);
67949
68401
  return {
@@ -67952,7 +68404,10 @@ ${escapeCodeFence(shellResult.output)}
67952
68404
  }
67953
68405
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
67954
68406
  if (sessionBusy && canSteerActiveRun) {
67955
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text, params.protectedControlMutationRule));
68407
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringPromptText({
68408
+ text: params.text,
68409
+ protectedControlMutationRule: params.protectedControlMutationRule
68410
+ }));
67956
68411
  return {
67957
68412
  processingIndicatorLifecycle: "active-run"
67958
68413
  };
@@ -68126,6 +68581,43 @@ function resolveChannelAuth(params) {
68126
68581
 
68127
68582
  // src/auth/owner-claim.ts
68128
68583
  var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
68584
+ import { statSync as statSync3 } from "node:fs";
68585
+
68586
+ // src/control/config-reload-suppression.ts
68587
+ var suppressedReloads = new Map;
68588
+ var MTIME_MATCH_EPSILON_MS = 1;
68589
+ function normalizeConfigPath(configPath) {
68590
+ return configPath.trim();
68591
+ }
68592
+ function suppressConfigReload(configPath, mtimeMs) {
68593
+ const normalizedPath = normalizeConfigPath(configPath);
68594
+ if (!normalizedPath || !Number.isFinite(mtimeMs)) {
68595
+ return;
68596
+ }
68597
+ const existing = suppressedReloads.get(normalizedPath) ?? [];
68598
+ existing.push(mtimeMs);
68599
+ suppressedReloads.set(normalizedPath, existing);
68600
+ }
68601
+ function consumeSuppressedConfigReload(configPath, mtimeMs) {
68602
+ const normalizedPath = normalizeConfigPath(configPath);
68603
+ if (!normalizedPath || !Number.isFinite(mtimeMs)) {
68604
+ return false;
68605
+ }
68606
+ const existing = suppressedReloads.get(normalizedPath);
68607
+ if (!existing || existing.length === 0) {
68608
+ return false;
68609
+ }
68610
+ const next = existing.filter((candidate) => Math.abs(candidate - mtimeMs) > MTIME_MATCH_EPSILON_MS);
68611
+ const matched = next.length !== existing.length;
68612
+ if (next.length === 0) {
68613
+ suppressedReloads.delete(normalizedPath);
68614
+ } else {
68615
+ suppressedReloads.set(normalizedPath, next);
68616
+ }
68617
+ return matched;
68618
+ }
68619
+
68620
+ // src/auth/owner-claim.ts
68129
68621
  var OWNER_CLAIM_RUNTIME_STARTED_AT_MS = Date.now();
68130
68622
  var CONFIG_LOCK_OPTIONS = {
68131
68623
  retries: {
@@ -68221,6 +68713,7 @@ async function claimFirstOwnerFromDirectMessage(params) {
68221
68713
  }
68222
68714
  freshConfig.app.auth.roles.owner.users = [...currentOwners, principal];
68223
68715
  await writeEditableConfig(expandedPath, freshConfig);
68716
+ suppressConfigReload(expandedPath, statSync3(expandedPath).mtimeMs);
68224
68717
  syncOwnerUsers(params.config, freshConfig);
68225
68718
  ownerClaimRuntimeState.closed = true;
68226
68719
  console.log(`clisbot auto-claimed first owner ${principal}`);
@@ -68503,7 +68996,7 @@ async function clearSlackAssistantThreadStatus(client, target) {
68503
68996
 
68504
68997
  // src/channels/processing-indicator.ts
68505
68998
  function shouldResolveIndicatorWait(update) {
68506
- return isTerminalRunStatus(update.status) || update.status === "detached";
68999
+ return isTerminalRunStatus(update.status);
68507
69000
  }
68508
69001
  async function waitForProcessingIndicatorLifecycle(params) {
68509
69002
  if (params.lifecycle !== "active-run") {
@@ -68634,6 +69127,7 @@ class ConversationProcessingIndicatorCoordinator {
68634
69127
  }
68635
69128
 
68636
69129
  // src/channels/slack/processing-decoration.ts
69130
+ var DEFAULT_STATUS_REFRESH_INTERVAL_MS = 2000;
68637
69131
  async function activateSlackProcessingDecoration(params) {
68638
69132
  const [reactionResult, statusResult] = await Promise.allSettled([
68639
69133
  params.addReaction(),
@@ -68655,7 +69149,38 @@ async function activateSlackProcessingDecoration(params) {
68655
69149
  throw statusResult.reason;
68656
69150
  }
68657
69151
  }
69152
+ let statusRefreshTimer;
69153
+ let closed = false;
69154
+ let refreshInFlight = false;
69155
+ let refreshPromise;
69156
+ if (statusApplied) {
69157
+ const refreshIntervalMs = Math.max(0, params.statusRefreshIntervalMs ?? DEFAULT_STATUS_REFRESH_INTERVAL_MS);
69158
+ if (refreshIntervalMs > 0) {
69159
+ statusRefreshTimer = setInterval(() => {
69160
+ if (closed || refreshInFlight) {
69161
+ return;
69162
+ }
69163
+ refreshInFlight = true;
69164
+ refreshPromise = params.setStatus().then(() => {
69165
+ return;
69166
+ }).catch((error) => {
69167
+ if (!closed) {
69168
+ params.onUnexpectedError?.("refresh-status", error);
69169
+ }
69170
+ }).finally(() => {
69171
+ refreshInFlight = false;
69172
+ refreshPromise = undefined;
69173
+ });
69174
+ }, refreshIntervalMs);
69175
+ }
69176
+ }
68658
69177
  return async () => {
69178
+ closed = true;
69179
+ if (statusRefreshTimer) {
69180
+ clearInterval(statusRefreshTimer);
69181
+ statusRefreshTimer = undefined;
69182
+ }
69183
+ await refreshPromise;
68659
69184
  if (reactionApplied) {
68660
69185
  try {
68661
69186
  await params.removeReaction();
@@ -68726,6 +69251,14 @@ function stripBotMention(text, botUserId) {
68726
69251
  }
68727
69252
  return text.replaceAll(`<@${botUserId}>`, "").replaceAll(/<@[^>]+>/g, "").trim();
68728
69253
  }
69254
+ function resolveSlackDirectReplyThreadTs(params) {
69255
+ const resolvedThreadTs = (params.resolvedThreadTs ?? "").trim();
69256
+ if (resolvedThreadTs) {
69257
+ return resolvedThreadTs;
69258
+ }
69259
+ const messageTs = (params.messageTs ?? "").trim();
69260
+ return messageTs || undefined;
69261
+ }
68729
69262
 
68730
69263
  // src/channels/slack/reactions.ts
68731
69264
  function normalizeSlackReactionName(value) {
@@ -69501,6 +70034,10 @@ class SlackSocketService {
69501
70034
  await this.processedEventsStore.markCompleted(eventId);
69502
70035
  return;
69503
70036
  }
70037
+ const directReplyThreadTs = resolveSlackDirectReplyThreadTs({
70038
+ messageTs,
70039
+ resolvedThreadTs: await this.resolveThreadTs(event)
70040
+ });
69504
70041
  let ownerClaimed = false;
69505
70042
  let ownerPrincipal;
69506
70043
  try {
@@ -69518,6 +70055,7 @@ class SlackSocketService {
69518
70055
  try {
69519
70056
  await postSlackText(this.app.client, {
69520
70057
  channel: channelId,
70058
+ threadTs: directReplyThreadTs,
69521
70059
  text: renderFirstOwnerClaimMessage({
69522
70060
  principal: ownerPrincipal,
69523
70061
  ownerClaimWindowMinutes: this.loadedConfig.raw.app.auth.ownerClaimWindowMinutes
@@ -69540,19 +70078,21 @@ class SlackSocketService {
69540
70078
  });
69541
70079
  if (!allowed) {
69542
70080
  if (dmConfig.policy === "pairing") {
69543
- const { code, created } = await upsertChannelPairingRequest({
70081
+ const pairingRequest = await upsertChannelPairingRequest({
69544
70082
  channel: "slack",
69545
70083
  id: directUserId
69546
70084
  });
69547
- if (created && code) {
70085
+ const pairingReply = buildPairingReplyFromRequest({
70086
+ channel: "slack",
70087
+ idLine: `Your Slack user id: ${directUserId}`,
70088
+ pairingRequest
70089
+ });
70090
+ if (pairingReply) {
69548
70091
  try {
69549
70092
  await postSlackText(this.app.client, {
69550
70093
  channel: channelId,
69551
- text: buildPairingReply({
69552
- channel: "slack",
69553
- idLine: `Your Slack user id: ${directUserId}`,
69554
- code
69555
- })
70094
+ threadTs: directReplyThreadTs,
70095
+ text: pairingReply
69556
70096
  });
69557
70097
  } catch (error) {
69558
70098
  console.error("slack pairing reply failed", error);
@@ -71123,6 +71663,7 @@ class TelegramPollingService {
71123
71663
  try {
71124
71664
  await callTelegramApi(this.accountConfig.botToken, "sendMessage", {
71125
71665
  chat_id: message.chat.id,
71666
+ ...message.message_id != null ? { reply_to_message_id: message.message_id } : {},
71126
71667
  text: renderFirstOwnerClaimMessage({
71127
71668
  principal: ownerPrincipal,
71128
71669
  ownerClaimWindowMinutes: this.loadedConfig.raw.app.auth.ownerClaimWindowMinutes
@@ -71146,7 +71687,7 @@ class TelegramPollingService {
71146
71687
  });
71147
71688
  if (!allowed) {
71148
71689
  if (directMessages.policy === "pairing") {
71149
- const { code, created } = await upsertChannelPairingRequest({
71690
+ const pairingRequest = await upsertChannelPairingRequest({
71150
71691
  channel: "telegram",
71151
71692
  id: senderId,
71152
71693
  meta: {
@@ -71155,15 +71696,16 @@ class TelegramPollingService {
71155
71696
  lastName: message.from?.last_name
71156
71697
  }
71157
71698
  });
71158
- if (created && code) {
71699
+ const pairingReply = buildPairingReplyFromRequest({
71700
+ channel: "telegram",
71701
+ idLine: `Your Telegram user id: ${senderId}`,
71702
+ pairingRequest
71703
+ });
71704
+ if (pairingReply) {
71159
71705
  try {
71160
71706
  await callTelegramApi(this.accountConfig.botToken, "sendMessage", {
71161
71707
  chat_id: message.chat.id,
71162
- text: buildPairingReply({
71163
- channel: "telegram",
71164
- idLine: `Your Telegram user id: ${senderId}`,
71165
- code
71166
- })
71708
+ text: pairingReply
71167
71709
  });
71168
71710
  } catch (error) {
71169
71711
  console.error("telegram pairing reply failed", error);
@@ -71809,19 +72351,33 @@ function summarizeExit(params) {
71809
72351
  return `code ${params.code ?? 0}`;
71810
72352
  }
71811
72353
  function getRestartPlan(config, restartNumber) {
71812
- let completedRestarts = 0;
71813
- const totalRestarts = config.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
72354
+ const fastRetryMaxRestarts = config.fastRetry.maxRestarts;
72355
+ const totalRestarts = fastRetryMaxRestarts + config.stages.reduce((sum, stage) => sum + stage.maxRestarts, 0);
72356
+ if (restartNumber >= 1 && restartNumber <= fastRetryMaxRestarts) {
72357
+ return {
72358
+ mode: "fast-retry",
72359
+ stageIndex: -1,
72360
+ delayMs: config.fastRetry.delaySeconds * 1000,
72361
+ restartAttemptInStage: restartNumber,
72362
+ restartsRemaining: totalRestarts - restartNumber,
72363
+ totalRestarts,
72364
+ stageMaxRestarts: fastRetryMaxRestarts
72365
+ };
72366
+ }
72367
+ let completedRestarts = fastRetryMaxRestarts;
71814
72368
  for (let index = 0;index < config.stages.length; index += 1) {
71815
72369
  const stage = config.stages[index];
71816
72370
  const stageStart = completedRestarts + 1;
71817
72371
  const stageEnd = completedRestarts + stage.maxRestarts;
71818
72372
  if (restartNumber >= stageStart && restartNumber <= stageEnd) {
71819
72373
  return {
72374
+ mode: "backoff",
71820
72375
  stageIndex: index,
71821
- delayMinutes: stage.delayMinutes,
72376
+ delayMs: stage.delayMinutes * 60000,
71822
72377
  restartAttemptInStage: restartNumber - completedRestarts,
71823
72378
  restartsRemaining: totalRestarts - restartNumber,
71824
- totalRestarts
72379
+ totalRestarts,
72380
+ stageMaxRestarts: stage.maxRestarts
71825
72381
  };
71826
72382
  }
71827
72383
  completedRestarts = stageEnd;
@@ -71955,7 +72511,7 @@ function renderBackoffAlertMessage(params) {
71955
72511
  `next restart: ${params.nextRestartAt}`,
71956
72512
  `restart: ${params.restartNumber}/${params.totalRestarts}`,
71957
72513
  `stage: ${params.stageIndex + 1}/${params.config.restartBackoff.stages.length}`,
71958
- `stage attempt: ${params.restartAttemptInStage}/${params.config.restartBackoff.stages[params.stageIndex]?.maxRestarts ?? params.restartAttemptInStage}`
72514
+ `stage attempt: ${params.restartAttemptInStage}/${params.stageMaxRestarts}`
71959
72515
  ].join(`
71960
72516
  `);
71961
72517
  }
@@ -72048,20 +72604,23 @@ class RuntimeMonitor {
72048
72604
  }
72049
72605
  restartNumber = nextRestartNumber;
72050
72606
  totalRestarts = plan.totalRestarts;
72051
- const nextRestartAt = new Date(this.dependencies.now() + plan.delayMinutes * 60000).toISOString();
72052
- await this.maybeSendAlert("backoff", monitorConfig, renderBackoffAlertMessage({
72053
- config: monitorConfig,
72054
- restartNumber,
72055
- stageIndex: plan.stageIndex,
72056
- restartAttemptInStage: plan.restartAttemptInStage,
72057
- totalRestarts,
72058
- nextRestartAt,
72059
- exit: {
72060
- code: exit.code,
72061
- signal: exit.signal,
72062
- at: exitAt
72063
- }
72064
- }));
72607
+ const nextRestartAt = new Date(this.dependencies.now() + plan.delayMs).toISOString();
72608
+ if (plan.mode === "backoff") {
72609
+ await this.maybeSendAlert("backoff", monitorConfig, renderBackoffAlertMessage({
72610
+ config: monitorConfig,
72611
+ restartNumber,
72612
+ stageIndex: plan.stageIndex,
72613
+ restartAttemptInStage: plan.restartAttemptInStage,
72614
+ stageMaxRestarts: plan.stageMaxRestarts,
72615
+ totalRestarts,
72616
+ nextRestartAt,
72617
+ exit: {
72618
+ code: exit.code,
72619
+ signal: exit.signal,
72620
+ at: exitAt
72621
+ }
72622
+ }));
72623
+ }
72065
72624
  await this.writeState({
72066
72625
  phase: "backoff",
72067
72626
  runtimePid: undefined,
@@ -72071,6 +72630,7 @@ class RuntimeMonitor {
72071
72630
  at: exitAt
72072
72631
  },
72073
72632
  restart: {
72633
+ mode: plan.mode,
72074
72634
  stageIndex: plan.stageIndex,
72075
72635
  restartNumber,
72076
72636
  restartAttemptInStage: plan.restartAttemptInStage,
@@ -72078,7 +72638,7 @@ class RuntimeMonitor {
72078
72638
  nextRestartAt
72079
72639
  }
72080
72640
  });
72081
- await this.sleepWithStop(plan.delayMinutes * 60000);
72641
+ await this.sleepWithStop(plan.delayMs);
72082
72642
  }
72083
72643
  } finally {
72084
72644
  await this.stopActiveChild();
@@ -72239,17 +72799,87 @@ var PROCESS_POLL_INTERVAL_MS = 100;
72239
72799
  function resolveConfigPath(configPath) {
72240
72800
  return expandHomePath(configPath ?? process.env.CLISBOT_CONFIG_PATH ?? getDefaultConfigPath());
72241
72801
  }
72242
- function resolvePidPath(pidPath) {
72243
- return expandHomePath(pidPath ?? process.env.CLISBOT_PID_PATH ?? getDefaultRuntimePidPath());
72802
+ function deriveRuntimeSiblingPath(configPath, filename) {
72803
+ if (!configPath) {
72804
+ return null;
72805
+ }
72806
+ return join11(dirname13(expandHomePath(configPath)), "state", filename);
72807
+ }
72808
+ function resolvePidPath(pidPath, configPath, options = {}) {
72809
+ if (pidPath) {
72810
+ return expandHomePath(pidPath);
72811
+ }
72812
+ if (options.preferConfigSibling) {
72813
+ const derivedFromExplicitConfig = deriveRuntimeSiblingPath(configPath, "clisbot.pid");
72814
+ if (derivedFromExplicitConfig) {
72815
+ return derivedFromExplicitConfig;
72816
+ }
72817
+ }
72818
+ if (process.env.CLISBOT_PID_PATH) {
72819
+ return expandHomePath(process.env.CLISBOT_PID_PATH);
72820
+ }
72821
+ const derivedFromConfig = deriveRuntimeSiblingPath(configPath ?? process.env.CLISBOT_CONFIG_PATH, "clisbot.pid");
72822
+ if (derivedFromConfig) {
72823
+ return derivedFromConfig;
72824
+ }
72825
+ return expandHomePath(getDefaultRuntimePidPath());
72244
72826
  }
72245
- function resolveLogPath(logPath) {
72246
- return expandHomePath(logPath ?? process.env.CLISBOT_LOG_PATH ?? getDefaultRuntimeLogPath());
72827
+ function resolveLogPath(logPath, configPath, options = {}) {
72828
+ if (logPath) {
72829
+ return expandHomePath(logPath);
72830
+ }
72831
+ if (options.preferConfigSibling) {
72832
+ const derivedFromExplicitConfig = deriveRuntimeSiblingPath(configPath, "clisbot.log");
72833
+ if (derivedFromExplicitConfig) {
72834
+ return derivedFromExplicitConfig;
72835
+ }
72836
+ }
72837
+ if (process.env.CLISBOT_LOG_PATH) {
72838
+ return expandHomePath(process.env.CLISBOT_LOG_PATH);
72839
+ }
72840
+ const derivedFromConfig = deriveRuntimeSiblingPath(configPath ?? process.env.CLISBOT_CONFIG_PATH, "clisbot.log");
72841
+ if (derivedFromConfig) {
72842
+ return derivedFromConfig;
72843
+ }
72844
+ return expandHomePath(getDefaultRuntimeLogPath());
72247
72845
  }
72248
- function resolveMonitorStatePath(monitorStatePath) {
72249
- return expandHomePath(monitorStatePath ?? process.env.CLISBOT_RUNTIME_MONITOR_STATE_PATH ?? getDefaultRuntimeMonitorStatePath());
72846
+ function resolveMonitorStatePath(monitorStatePath, configPath, options = {}) {
72847
+ if (monitorStatePath) {
72848
+ return expandHomePath(monitorStatePath);
72849
+ }
72850
+ if (options.preferConfigSibling) {
72851
+ const derivedFromExplicitConfig = deriveRuntimeSiblingPath(configPath, "clisbot-monitor.json");
72852
+ if (derivedFromExplicitConfig) {
72853
+ return derivedFromExplicitConfig;
72854
+ }
72855
+ }
72856
+ if (process.env.CLISBOT_RUNTIME_MONITOR_STATE_PATH) {
72857
+ return expandHomePath(process.env.CLISBOT_RUNTIME_MONITOR_STATE_PATH);
72858
+ }
72859
+ const derivedFromConfig = deriveRuntimeSiblingPath(configPath ?? process.env.CLISBOT_CONFIG_PATH, "clisbot-monitor.json");
72860
+ if (derivedFromConfig) {
72861
+ return derivedFromConfig;
72862
+ }
72863
+ return expandHomePath(getDefaultRuntimeMonitorStatePath());
72250
72864
  }
72251
- function resolveRuntimeCredentialsPath(runtimeCredentialsPath) {
72252
- return expandHomePath(runtimeCredentialsPath ?? process.env.CLISBOT_RUNTIME_CREDENTIALS_PATH ?? getDefaultRuntimeCredentialsPath());
72865
+ function resolveRuntimeCredentialsPath(runtimeCredentialsPath, configPath, options = {}) {
72866
+ if (runtimeCredentialsPath) {
72867
+ return expandHomePath(runtimeCredentialsPath);
72868
+ }
72869
+ if (options.preferConfigSibling) {
72870
+ const derivedFromExplicitConfig = deriveRuntimeSiblingPath(configPath, "runtime-credentials.json");
72871
+ if (derivedFromExplicitConfig) {
72872
+ return derivedFromExplicitConfig;
72873
+ }
72874
+ }
72875
+ if (process.env.CLISBOT_RUNTIME_CREDENTIALS_PATH) {
72876
+ return expandHomePath(process.env.CLISBOT_RUNTIME_CREDENTIALS_PATH);
72877
+ }
72878
+ const derivedFromConfig = deriveRuntimeSiblingPath(configPath ?? process.env.CLISBOT_CONFIG_PATH, "runtime-credentials.json");
72879
+ if (derivedFromConfig) {
72880
+ return derivedFromConfig;
72881
+ }
72882
+ return expandHomePath(getDefaultRuntimeCredentialsPath());
72253
72883
  }
72254
72884
 
72255
72885
  class StartDetachedRuntimeError extends Error {
@@ -72326,10 +72956,14 @@ async function ensureConfigFile(configPath, options = {}) {
72326
72956
  };
72327
72957
  }
72328
72958
  async function startDetachedRuntime(params) {
72329
- const pidPath = resolvePidPath(params.pidPath);
72330
- const logPath = resolveLogPath(params.logPath);
72331
- const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath);
72332
- const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath);
72959
+ const configPath = resolveConfigPath(params.configPath);
72960
+ const preferConfigSibling = params.configPath != null;
72961
+ const pidPath = resolvePidPath(params.pidPath, configPath, { preferConfigSibling });
72962
+ const logPath = resolveLogPath(params.logPath, configPath, { preferConfigSibling });
72963
+ const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath, configPath, {
72964
+ preferConfigSibling
72965
+ });
72966
+ const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath, configPath, { preferConfigSibling });
72333
72967
  const existingPid = await readRuntimePid(pidPath);
72334
72968
  const existingMonitorState = await readRuntimeMonitorState(monitorStatePath);
72335
72969
  if (existingPid && isProcessRunning(existingPid)) {
@@ -72337,7 +72971,7 @@ async function startDetachedRuntime(params) {
72337
72971
  alreadyRunning: true,
72338
72972
  createdConfig: false,
72339
72973
  pid: existingPid,
72340
- configPath: resolveConfigPath(params.configPath),
72974
+ configPath,
72341
72975
  logPath
72342
72976
  };
72343
72977
  }
@@ -72402,9 +73036,13 @@ async function startDetachedRuntime(params) {
72402
73036
  };
72403
73037
  }
72404
73038
  async function stopDetachedRuntime(params, dependencies = {}) {
72405
- const pidPath = resolvePidPath(params.pidPath);
72406
- const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath);
72407
- const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath);
73039
+ const configPath = resolveConfigPath(params.configPath);
73040
+ const preferConfigSibling = params.configPath != null;
73041
+ const pidPath = resolvePidPath(params.pidPath, configPath, { preferConfigSibling });
73042
+ const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath, configPath, {
73043
+ preferConfigSibling
73044
+ });
73045
+ const runtimeCredentialsPath = resolveRuntimeCredentialsPath(params.runtimeCredentialsPath, configPath, { preferConfigSibling });
72408
73046
  const existingPid = await readRuntimePid(pidPath);
72409
73047
  const monitorState = await readRuntimeMonitorState(monitorStatePath);
72410
73048
  let stopped = false;
@@ -72445,7 +73083,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
72445
73083
  }
72446
73084
  rmSync3(pidPath, { force: true });
72447
73085
  removeRuntimeCredentials(runtimeCredentialsPath);
72448
- await disableExpiredMemAccountsInConfig(params.configPath);
73086
+ await disableExpiredMemAccountsInConfig(configPath);
72449
73087
  if (monitorState) {
72450
73088
  await writeRuntimeMonitorState(monitorStatePath, {
72451
73089
  ...monitorState,
@@ -72456,7 +73094,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
72456
73094
  });
72457
73095
  }
72458
73096
  if (params.hard) {
72459
- const socketPath = await resolveTmuxSocketPath(params.configPath);
73097
+ const socketPath = await resolveTmuxSocketPath(configPath);
72460
73098
  const tmux = new TmuxClient(socketPath);
72461
73099
  try {
72462
73100
  await tmux.killServer();
@@ -72489,9 +73127,12 @@ function removeRuntimePid(pidPath) {
72489
73127
  }
72490
73128
  async function getRuntimeStatus(params = {}) {
72491
73129
  const configPath = resolveConfigPath(params.configPath);
72492
- const pidPath = resolvePidPath(params.pidPath);
72493
- const logPath = resolveLogPath(params.logPath);
72494
- const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath);
73130
+ const preferConfigSibling = params.configPath != null;
73131
+ const pidPath = resolvePidPath(params.pidPath, configPath, { preferConfigSibling });
73132
+ const logPath = resolveLogPath(params.logPath, configPath, { preferConfigSibling });
73133
+ const monitorStatePath = resolveMonitorStatePath(params.monitorStatePath, configPath, {
73134
+ preferConfigSibling
73135
+ });
72495
73136
  const pid = await readRuntimePid(pidPath);
72496
73137
  const liveness = pid ? getProcessLiveness(pid) : "missing";
72497
73138
  const monitorState = await readRuntimeMonitorState(monitorStatePath);
@@ -72508,6 +73149,7 @@ async function getRuntimeStatus(params = {}) {
72508
73149
  runtimePid: monitorState?.runtimePid && getProcessLiveness(monitorState.runtimePid) === "running" ? monitorState.runtimePid : undefined,
72509
73150
  nextRestartAt: monitorState?.restart?.nextRestartAt,
72510
73151
  restartNumber: monitorState?.restart?.restartNumber,
73152
+ restartMode: monitorState?.restart?.mode,
72511
73153
  restartStageIndex: monitorState?.restart?.stageIndex,
72512
73154
  stopReason: monitorState?.stopReason
72513
73155
  };
@@ -72545,7 +73187,7 @@ function getLogSize(logPath) {
72545
73187
  return 0;
72546
73188
  }
72547
73189
  try {
72548
- return statSync3(logPath).size;
73190
+ return statSync4(logPath).size;
72549
73191
  } catch {
72550
73192
  return 0;
72551
73193
  }
@@ -72737,10 +73379,17 @@ function renderAccountsHelp() {
72737
73379
  "",
72738
73380
  "Usage:",
72739
73381
  " clisbot accounts --help",
73382
+ " clisbot accounts help",
72740
73383
  " clisbot accounts add telegram --account <id> --token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
72741
73384
  " clisbot accounts add slack --account <id> --app-token <ENV_NAME|${ENV_NAME}|literal> --bot-token <ENV_NAME|${ENV_NAME}|literal> [--persist]",
72742
73385
  " clisbot accounts persist --channel <slack|telegram> --account <id>",
72743
- " clisbot accounts persist --all"
73386
+ " clisbot accounts persist --all",
73387
+ "",
73388
+ "Notes:",
73389
+ " - env-style input such as `TELEGRAM_BOT_TOKEN` or `${TELEGRAM_BOT_TOKEN}` keeps the account env-backed in config",
73390
+ " - literal token input without `--persist` stays runtime-only and requires a running clisbot runtime",
73391
+ " - `--persist` writes canonical token files so later plain `clisbot start` can reuse the account safely",
73392
+ " - `persist --all` converts every configured `credentialType=mem` account into canonical token files"
72744
73393
  ].join(`
72745
73394
  `);
72746
73395
  }
@@ -72917,7 +73566,7 @@ async function runAccountsCli(args, deps = {}) {
72917
73566
  ...deps
72918
73567
  };
72919
73568
  const action = args[0];
72920
- if (!action || action === "--help" || action === "-h") {
73569
+ if (!action || action === "--help" || action === "-h" || action === "help") {
72921
73570
  console.log(renderAccountsHelp());
72922
73571
  return;
72923
73572
  }
@@ -73021,6 +73670,7 @@ function renderAuthCliHelp() {
73021
73670
  " add-user/remove-user mutate roles.<role>.users",
73022
73671
  " add-permission/remove-permission mutate roles.<role>.allow",
73023
73672
  " agent role edits clone the inherited agent-defaults role into the target agent override on first write",
73673
+ " app `owner` and `admin` principals bypass DM pairing automatically once they are granted",
73024
73674
  "",
73025
73675
  "Examples:",
73026
73676
  " clisbot auth add-user app --role owner --user telegram:1276408333",
@@ -73283,6 +73933,7 @@ function renderChannelsHelp() {
73283
73933
  " - Slack private groups need channels.slack.groups.<groupId>",
73284
73934
  " - Telegram groups need channels.telegram.groups.<chatId>",
73285
73935
  " - Telegram forum topics need channels.telegram.groups.<chatId>.topics.<topicId>",
73936
+ " - route adds for Slack channels, Slack groups, Telegram groups, and Telegram topics default to `requireMention: true` unless you pass `--require-mention false`",
73286
73937
  " - Adding a route puts that surface on the allowlist; other channels, groups, or topics still need to be added explicitly",
73287
73938
  " - Tune route settings such as requireMention and followUp in clisbot.json when a surface should behave differently",
73288
73939
  ` - Manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`,
@@ -73361,6 +74012,7 @@ function renderRouteAddGuidance(params) {
73361
74012
  console.log(` - route added: ${routePath}`);
73362
74013
  console.log(" - direct messages still follow channels.slack.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
73363
74014
  console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention and followUp`);
74015
+ console.log(" - new Slack channel/group routes default to `requireMention: true` unless you passed `--require-mention false`");
73364
74016
  console.log(" - if you want pairing-style access control for DMs, set channels.slack.directMessages.policy to `pairing`");
73365
74017
  console.log(" - if you want stricter route access, keep Slack groups on allowlist and only add the channels/groups you trust");
73366
74018
  console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
@@ -73371,6 +74023,7 @@ function renderRouteAddGuidance(params) {
73371
74023
  console.log(` - route added: ${routePath}`);
73372
74024
  console.log(" - direct messages still follow channels.telegram.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
73373
74025
  console.log(` - this ${params.kind} is now on the Telegram allowlist; other groups or topics still need to be added explicitly`);
74026
+ console.log(" - new Telegram group/topic routes default to `requireMention: true` unless you passed `--require-mention false`");
73374
74027
  console.log(" - if you want pairing-style access control for DMs, set channels.telegram.directMessages.policy to `pairing`");
73375
74028
  console.log(" - tune route settings such as requireMention and followUp in clisbot.json if this surface should behave differently");
73376
74029
  console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
@@ -73569,7 +74222,7 @@ async function addSlackRoute(kind, args) {
73569
74222
  }
73570
74223
  const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
73571
74224
  const agentId = getAgentId(args);
73572
- const requireMention = parseBooleanOption(args, "--require-mention", false);
74225
+ const requireMention = parseBooleanOption(args, "--require-mention", true);
73573
74226
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
73574
74227
  target[routeId] = {
73575
74228
  ...target[routeId] ?? {},
@@ -74800,6 +75453,41 @@ function assertSupportedPlatform(command) {
74800
75453
  }
74801
75454
 
74802
75455
  // src/control/runtime-bootstrap-cli.ts
75456
+ function hasHelpFlag(args) {
75457
+ return args.includes("--help") || args.includes("-h") || args.includes("help");
75458
+ }
75459
+ function renderBootstrapCommandHelp(commandName) {
75460
+ const behavior = commandName === "start" ? "seed config if needed and start the detached runtime" : "seed config and optionally bootstrap the first agent without starting runtime";
75461
+ return [
75462
+ `clisbot ${commandName}`,
75463
+ "",
75464
+ "Usage:",
75465
+ ` clisbot ${commandName} --help`,
75466
+ ` clisbot ${commandName} [--cli <codex|claude|gemini>] [--bot-type <personal|team>] [--persist]`,
75467
+ " [--slack-account <id> --slack-app-token <ENV_NAME|${ENV_NAME}|literal> --slack-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
75468
+ " [--telegram-account <id> --telegram-bot-token <ENV_NAME|${ENV_NAME}|literal>]...",
75469
+ "",
75470
+ "Behavior:",
75471
+ ` - ${behavior}`,
75472
+ " - first-run agent bootstrap needs both `--cli` and `--bot-type`",
75473
+ " - `--bot-type personal` maps to `personal-assistant`; `--bot-type team` maps to `team-assistant`",
75474
+ " - explicit credential flags only enable the channels and accounts you named in this command",
75475
+ " - env-style values such as `SLACK_APP_TOKEN` or `${SLACK_APP_TOKEN}` stay env-backed in config",
75476
+ commandName === "start" ? " - literal token values without `--persist` stay runtime-only for this start invocation" : " - literal token values on `init` require `--persist` because no runtime exists yet",
75477
+ " - `--persist` writes canonical credential files so later plain `clisbot start` can reuse them",
75478
+ "",
75479
+ "Examples:",
75480
+ ` clisbot ${commandName} --cli codex --bot-type personal --telegram-bot-token TELEGRAM_BOT_TOKEN`,
75481
+ ` clisbot ${commandName} --cli codex --bot-type team --slack-app-token SLACK_APP_TOKEN --slack-bot-token SLACK_BOT_TOKEN`,
75482
+ ` clisbot ${commandName} --cli gemini --bot-type personal --telegram-bot-token "$TELEGRAM_BOT_TOKEN" --persist`,
75483
+ "",
75484
+ "Related help:",
75485
+ " - `clisbot agents --help` for lower-level agent bootstrap and binding control",
75486
+ " - `clisbot accounts --help` for account persistence after first run",
75487
+ " - `clisbot channels --help` for route setup after bootstrap"
75488
+ ].join(`
75489
+ `);
75490
+ }
74803
75491
  function getPrimaryWorkspacePath(summary) {
74804
75492
  const preferredAgentId = summary.channelSummaries.find((channel) => channel.enabled)?.defaultAgentId ?? "default";
74805
75493
  return summary.agentSummaries.find((agent) => agent.id === preferredAgentId)?.workspacePath ?? summary.agentSummaries[0]?.workspacePath;
@@ -75016,6 +75704,10 @@ async function printStartedRuntimeSummary(pid, configPath, logPath) {
75016
75704
  }
75017
75705
  }
75018
75706
  async function initConfig(args = []) {
75707
+ if (hasHelpFlag(args)) {
75708
+ console.log(renderBootstrapCommandHelp("init"));
75709
+ return;
75710
+ }
75019
75711
  const state = await prepareBootstrapState(args, "init");
75020
75712
  if (!state) {
75021
75713
  return;
@@ -75034,6 +75726,10 @@ async function initConfig(args = []) {
75034
75726
  }
75035
75727
  }
75036
75728
  async function start(args = []) {
75729
+ if (hasHelpFlag(args)) {
75730
+ console.log(renderBootstrapCommandHelp("start"));
75731
+ return;
75732
+ }
75037
75733
  const runtimeStatus = await getRuntimeStatus();
75038
75734
  const bootstrapFlags = parseBootstrapFlags(args);
75039
75735
  const restartForLiteralBootstrap = runtimeStatus.running && hasLiteralMemCredentials(bootstrapFlags);
@@ -75086,7 +75782,7 @@ async function start(args = []) {
75086
75782
  }
75087
75783
 
75088
75784
  // src/control/runtime-supervisor.ts
75089
- import { statSync as statSync4, watch } from "node:fs";
75785
+ import { statSync as statSync5, watch } from "node:fs";
75090
75786
  import { basename as basename4, dirname as dirname15 } from "node:path";
75091
75787
 
75092
75788
  // src/channels/processed-events-store.ts
@@ -75249,12 +75945,22 @@ class RuntimeSupervisor {
75249
75945
  let nextRuntime;
75250
75946
  try {
75251
75947
  const loadedConfig = await this.dependencies.loadConfig(this.configPath);
75948
+ const configMtimeMs = statSync5(loadedConfig.configPath).mtimeMs;
75949
+ if (reason === "watch" && consumeSuppressedConfigReload(loadedConfig.configPath, configMtimeMs)) {
75950
+ await this.reconcileConfigWatcher(loadedConfig);
75951
+ await this.dependencies.runtimeHealthStore.setReload({
75952
+ status: "success",
75953
+ reason,
75954
+ configMtimeMs
75955
+ });
75956
+ return;
75957
+ }
75252
75958
  nextRuntime = await this.createRuntime(loadedConfig);
75253
75959
  await this.reconcileConfigWatcher(loadedConfig);
75254
75960
  await this.dependencies.runtimeHealthStore.setReload({
75255
75961
  status: "success",
75256
75962
  reason,
75257
- configMtimeMs: statSync4(loadedConfig.configPath).mtimeMs
75963
+ configMtimeMs
75258
75964
  });
75259
75965
  this.activeRuntime = nextRuntime;
75260
75966
  if (previousRuntime) {
@@ -75596,6 +76302,9 @@ async function printStatusSummary() {
75596
76302
  if (runtimeStatus.restartNumber) {
75597
76303
  console.log(`restart attempt: ${runtimeStatus.restartNumber}`);
75598
76304
  }
76305
+ if (runtimeStatus.restartMode) {
76306
+ console.log(`restart mode: ${runtimeStatus.restartMode}`);
76307
+ }
75599
76308
  if (runtimeStatus.restartStageIndex != null && runtimeStatus.restartStageIndex >= 0) {
75600
76309
  console.log(`restart stage: ${runtimeStatus.restartStageIndex + 1}`);
75601
76310
  }