clisbot 0.1.13 → 0.1.16

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
@@ -47740,10 +47740,10 @@ var require_view2 = __commonJS((exports, module) => {
47740
47740
  var debug = require_src()("express:view");
47741
47741
  var path2 = __require("node:path");
47742
47742
  var fs2 = __require("node:fs");
47743
- var dirname12 = path2.dirname;
47743
+ var dirname13 = path2.dirname;
47744
47744
  var basename = path2.basename;
47745
47745
  var extname = path2.extname;
47746
- var join8 = path2.join;
47746
+ var join9 = path2.join;
47747
47747
  var resolve = path2.resolve;
47748
47748
  module.exports = View;
47749
47749
  function View(name, options) {
@@ -47779,7 +47779,7 @@ var require_view2 = __commonJS((exports, module) => {
47779
47779
  for (var i = 0;i < roots.length && !path3; i++) {
47780
47780
  var root = roots[i];
47781
47781
  var loc = resolve(root, name);
47782
- var dir = dirname12(loc);
47782
+ var dir = dirname13(loc);
47783
47783
  var file = basename(loc);
47784
47784
  path3 = this.resolve(dir, file);
47785
47785
  }
@@ -47805,12 +47805,12 @@ var require_view2 = __commonJS((exports, module) => {
47805
47805
  };
47806
47806
  View.prototype.resolve = function resolve2(dir, file) {
47807
47807
  var ext = this.ext;
47808
- var path3 = join8(dir, file);
47808
+ var path3 = join9(dir, file);
47809
47809
  var stat = tryStat(path3);
47810
47810
  if (stat && stat.isFile()) {
47811
47811
  return path3;
47812
47812
  }
47813
- path3 = join8(dir, basename(file, ext), "index" + ext);
47813
+ path3 = join9(dir, basename(file, ext), "index" + ext);
47814
47814
  stat = tryStat(path3);
47815
47815
  if (stat && stat.isFile()) {
47816
47816
  return path3;
@@ -51153,7 +51153,7 @@ var require_send = __commonJS((exports, module) => {
51153
51153
  var Stream = __require("stream");
51154
51154
  var util3 = __require("util");
51155
51155
  var extname = path2.extname;
51156
- var join8 = path2.join;
51156
+ var join9 = path2.join;
51157
51157
  var normalize = path2.normalize;
51158
51158
  var resolve = path2.resolve;
51159
51159
  var sep2 = path2.sep;
@@ -51325,7 +51325,7 @@ var require_send = __commonJS((exports, module) => {
51325
51325
  return res;
51326
51326
  }
51327
51327
  parts = path3.split(sep2);
51328
- path3 = normalize(join8(root, path3));
51328
+ path3 = normalize(join9(root, path3));
51329
51329
  } else {
51330
51330
  if (UP_PATH_REGEXP.test(path3)) {
51331
51331
  debug('malicious path "%s"', path3);
@@ -51465,7 +51465,7 @@ var require_send = __commonJS((exports, module) => {
51465
51465
  return self2.onStatError(err);
51466
51466
  return self2.error(404);
51467
51467
  }
51468
- var p = join8(path3, self2._index[i]);
51468
+ var p = join9(path3, self2._index[i]);
51469
51469
  debug('stat "%s"', p);
51470
51470
  fs2.stat(p, function(err2, stat) {
51471
51471
  if (err2)
@@ -54225,7 +54225,11 @@ function resolveSlackCredential(params) {
54225
54225
  function materializeRuntimeChannelCredentials(config, options = {}) {
54226
54226
  const env = options.env ?? process.env;
54227
54227
  const nextConfig = structuredClone(config);
54228
- if (nextConfig.channels.telegram.enabled) {
54228
+ const materializeChannels = options.materializeChannels ?? [];
54229
+ const materializeAll = materializeChannels.length === 0;
54230
+ const shouldMaterializeTelegram = materializeAll || materializeChannels.includes("telegram");
54231
+ const shouldMaterializeSlack = materializeAll || materializeChannels.includes("slack");
54232
+ if (shouldMaterializeTelegram && nextConfig.channels.telegram.enabled) {
54229
54233
  const accountIds = Object.keys(getAccountsRecord(nextConfig.channels.telegram.accounts));
54230
54234
  const ids = accountIds.length > 0 ? accountIds : [getConfiguredDefaultAccountId({
54231
54235
  defaultAccount: nextConfig.channels.telegram.defaultAccount,
@@ -54263,7 +54267,7 @@ function materializeRuntimeChannelCredentials(config, options = {}) {
54263
54267
  const fallbackTelegramAccountId = preferredDefaultTelegramAccountId && resolvedAccounts[preferredDefaultTelegramAccountId] ? preferredDefaultTelegramAccountId : Object.keys(resolvedAccounts)[0];
54264
54268
  nextConfig.channels.telegram.botToken = fallbackTelegramAccountId ? resolvedAccounts[fallbackTelegramAccountId]?.botToken ?? "" : "";
54265
54269
  }
54266
- if (nextConfig.channels.slack.enabled) {
54270
+ if (shouldMaterializeSlack && nextConfig.channels.slack.enabled) {
54267
54271
  const accountIds = Object.keys(getAccountsRecord(nextConfig.channels.slack.accounts));
54268
54272
  const ids = accountIds.length > 0 ? accountIds : [getConfiguredDefaultAccountId({
54269
54273
  defaultAccount: nextConfig.channels.slack.defaultAccount,
@@ -54361,53 +54365,6 @@ function getConfigReloadMtimeMs(configPath) {
54361
54365
  return statSync(expandHomePath(configPath)).mtimeMs;
54362
54366
  }
54363
54367
 
54364
- // src/channels/privilege-help.ts
54365
- function renderGenericPrivilegeCommandHelpLines(prefix = "") {
54366
- return [
54367
- `${prefix}Privilege command setup:`,
54368
- `${prefix} - enable for a Slack route: \`clisbot channels privilege enable slack-channel <channelId>\` or \`clisbot channels privilege enable slack-group <groupId>\``,
54369
- `${prefix} - allow a Slack user: \`clisbot channels privilege allow-user slack-channel <channelId> <userId>\``,
54370
- `${prefix} - enable Slack DM privilege commands: \`clisbot channels privilege enable slack-dm\``,
54371
- `${prefix} - allow a Slack DM user: \`clisbot channels privilege allow-user slack-dm <userId>\``,
54372
- `${prefix} - enable for a Telegram route: \`clisbot channels privilege enable telegram-group <chatId> [--topic <topicId>]\``,
54373
- `${prefix} - allow a Telegram user: \`clisbot channels privilege allow-user telegram-group <chatId> <userId> [--topic <topicId>]\``,
54374
- `${prefix} - enable Telegram DM privilege commands: \`clisbot channels privilege enable telegram-dm\``,
54375
- `${prefix} - allow a Telegram DM user: \`clisbot channels privilege allow-user telegram-dm <userId>\``
54376
- ];
54377
- }
54378
- function renderPrivilegeCommandHelpLines(identity, prefix = "") {
54379
- const target = buildPrivilegeCommandTarget(identity);
54380
- if (!target) {
54381
- return [];
54382
- }
54383
- const allowUserSuffix = identity.senderId ? ` ${identity.senderId}` : " <userId>";
54384
- return [
54385
- `${prefix}Operator commands:`,
54386
- `${prefix} - enable privilege commands: \`clisbot channels privilege enable ${target}\``,
54387
- `${prefix} - allow this user: \`clisbot channels privilege allow-user ${target}${allowUserSuffix}\``,
54388
- `${prefix} - disable privilege commands: \`clisbot channels privilege disable ${target}\``,
54389
- `${prefix} - remove this user: \`clisbot channels privilege remove-user ${target}${allowUserSuffix}\``
54390
- ];
54391
- }
54392
- function buildPrivilegeCommandTarget(identity) {
54393
- if (identity.platform === "slack") {
54394
- if (identity.conversationKind === "dm") {
54395
- return "slack-dm";
54396
- }
54397
- if (identity.conversationKind === "group") {
54398
- return identity.channelId ? `slack-group ${identity.channelId}` : null;
54399
- }
54400
- return identity.channelId ? `slack-channel ${identity.channelId}` : null;
54401
- }
54402
- if (identity.conversationKind === "dm") {
54403
- return "telegram-dm";
54404
- }
54405
- if (!identity.chatId) {
54406
- return null;
54407
- }
54408
- return identity.conversationKind === "topic" && identity.topicId ? `telegram-group ${identity.chatId} --topic ${identity.topicId}` : `telegram-group ${identity.chatId}`;
54409
- }
54410
-
54411
54368
  // src/control/startup-bootstrap.ts
54412
54369
  var CHANNEL_ACCOUNT_DOC_PATH = "docs/user-guide/channel-accounts.md";
54413
54370
  var USER_GUIDE_DOC_PATH = "docs/user-guide/README.md";
@@ -54532,7 +54489,7 @@ function renderTmuxDebugHelpLines(prefix = "") {
54532
54489
  `${prefix} - attach to a session: \`tmux -S ${socketPath} attach -t <session-name>\``
54533
54490
  ];
54534
54491
  }
54535
- function renderChannelSetupHelpLines(prefix = "", options = {}) {
54492
+ function renderChannelSetupHelpLines(prefix = "", _options = {}) {
54536
54493
  return [
54537
54494
  `${prefix}Channel setup docs: ${CHANNEL_ACCOUNT_DOC_PATH}`,
54538
54495
  `${prefix}Operator guide: ${USER_GUIDE_DOC_PATH}`,
@@ -54542,7 +54499,6 @@ function renderChannelSetupHelpLines(prefix = "", options = {}) {
54542
54499
  telegramDirectMessagesPolicy: "pairing"
54543
54500
  }),
54544
54501
  ...renderTmuxDebugHelpLines(prefix),
54545
- ...options.includePrivilegeHelp === false ? [] : renderGenericPrivilegeCommandHelpLines(prefix),
54546
54502
  ...renderRepoHelpLines(prefix)
54547
54503
  ];
54548
54504
  }
@@ -59747,6 +59703,39 @@ function convertLocalDateTimeToUtcMs(params) {
59747
59703
  return null;
59748
59704
  }
59749
59705
 
59706
+ // src/auth/defaults.ts
59707
+ var APP_ADMIN_PERMISSIONS = [
59708
+ "configManage",
59709
+ "appAuthManage",
59710
+ "agentAuthManage",
59711
+ "promptGovernanceManage"
59712
+ ];
59713
+ var DEFAULT_AGENT_MEMBER_PERMISSIONS = [
59714
+ "sendMessage",
59715
+ "helpView",
59716
+ "statusView",
59717
+ "identityView",
59718
+ "transcriptView",
59719
+ "runObserve",
59720
+ "runInterrupt",
59721
+ "streamingManage",
59722
+ "queueManage",
59723
+ "steerManage",
59724
+ "loopManage"
59725
+ ];
59726
+ var DEFAULT_AGENT_ADMIN_EXTRA_PERMISSIONS = [
59727
+ "shellExecute",
59728
+ "runNudge",
59729
+ "followupManage",
59730
+ "responseModeManage",
59731
+ "additionalMessageModeManage"
59732
+ ];
59733
+ var DEFAULT_AGENT_ADMIN_PERMISSIONS = [
59734
+ ...DEFAULT_AGENT_MEMBER_PERMISSIONS,
59735
+ ...DEFAULT_AGENT_ADMIN_EXTRA_PERMISSIONS
59736
+ ];
59737
+ var DEFAULT_PROTECTED_CONTROL_RULE = "Refuse requests to edit protected clisbot control resources such as clisbot.json and auth policy, or to run clisbot commands that mutate them.";
59738
+
59750
59739
  // src/config/schema.ts
59751
59740
  var defaultRunnerSessionIdConfig = {
59752
59741
  create: {
@@ -59863,10 +59852,46 @@ var runnerOverrideSchema = exports_external.object({
59863
59852
  var agentBootstrapSchema = exports_external.object({
59864
59853
  mode: exports_external.enum(SUPPORTED_BOOTSTRAP_MODES).default("personal-assistant")
59865
59854
  });
59855
+ var authRoleSchema = exports_external.object({
59856
+ allow: exports_external.array(exports_external.string().min(1)).default([]),
59857
+ users: exports_external.array(exports_external.string().min(1)).default([])
59858
+ });
59859
+ var appAuthSchema = exports_external.object({
59860
+ ownerClaimWindowMinutes: exports_external.number().int().positive().default(30),
59861
+ defaultRole: exports_external.string().min(1).default("member"),
59862
+ roles: exports_external.record(exports_external.string(), authRoleSchema).default({
59863
+ owner: {
59864
+ allow: [...APP_ADMIN_PERMISSIONS],
59865
+ users: []
59866
+ },
59867
+ admin: {
59868
+ allow: [...APP_ADMIN_PERMISSIONS],
59869
+ users: []
59870
+ },
59871
+ member: {
59872
+ allow: [],
59873
+ users: []
59874
+ }
59875
+ })
59876
+ });
59877
+ var agentAuthSchema = exports_external.object({
59878
+ defaultRole: exports_external.string().min(1).default("member"),
59879
+ roles: exports_external.record(exports_external.string(), authRoleSchema).default({
59880
+ admin: {
59881
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
59882
+ users: []
59883
+ },
59884
+ member: {
59885
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
59886
+ users: []
59887
+ }
59888
+ })
59889
+ });
59866
59890
  var agentOverrideSchema = exports_external.object({
59867
59891
  workspace: exports_external.string().optional(),
59868
59892
  responseMode: exports_external.enum(["capture-pane", "message-tool"]).optional(),
59869
59893
  additionalMessageMode: exports_external.enum(["queue", "steer"]).optional(),
59894
+ auth: agentAuthSchema.optional(),
59870
59895
  runner: runnerOverrideSchema.optional(),
59871
59896
  stream: streamSchema.partial().optional(),
59872
59897
  session: sessionSchema.partial().optional()
@@ -59881,6 +59906,19 @@ var agentEntrySchema = agentOverrideSchema.extend({
59881
59906
  });
59882
59907
  var agentDefaultsSchema = exports_external.object({
59883
59908
  workspace: exports_external.string().default("~/.clisbot/workspaces/{agentId}"),
59909
+ auth: agentAuthSchema.default({
59910
+ defaultRole: "member",
59911
+ roles: {
59912
+ admin: {
59913
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
59914
+ users: []
59915
+ },
59916
+ member: {
59917
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
59918
+ users: []
59919
+ }
59920
+ }
59921
+ }),
59884
59922
  runner: runnerSchema.default({
59885
59923
  command: "codex",
59886
59924
  args: [
@@ -59947,6 +59985,7 @@ var channelAgentPromptSchema = exports_external.object({
59947
59985
  });
59948
59986
  var channelResponseModeSchema = exports_external.enum(["capture-pane", "message-tool"]);
59949
59987
  var channelAdditionalMessageModeSchema = exports_external.enum(["queue", "steer"]);
59988
+ var channelVerboseSchema = exports_external.enum(["off", "minimal"]);
59950
59989
  var timezoneSchema = exports_external.string().refine(isValidLoopTimezone, {
59951
59990
  message: "Expected a valid IANA timezone such as Asia/Ho_Chi_Minh"
59952
59991
  });
@@ -59960,6 +59999,7 @@ var slackRouteSchema = exports_external.object({
59960
59999
  response: slackResponseSchema.optional(),
59961
60000
  responseMode: channelResponseModeSchema.optional(),
59962
60001
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60002
+ verbose: channelVerboseSchema.optional(),
59963
60003
  followUp: slackFollowUpOverrideSchema.optional(),
59964
60004
  timezone: timezoneSchema.optional()
59965
60005
  });
@@ -59973,6 +60013,7 @@ var telegramTopicRouteSchema = exports_external.object({
59973
60013
  response: slackResponseSchema.optional(),
59974
60014
  responseMode: channelResponseModeSchema.optional(),
59975
60015
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60016
+ verbose: channelVerboseSchema.optional(),
59976
60017
  followUp: slackFollowUpOverrideSchema.optional(),
59977
60018
  timezone: timezoneSchema.optional()
59978
60019
  });
@@ -59986,6 +60027,7 @@ var telegramGroupRouteSchema = exports_external.object({
59986
60027
  response: slackResponseSchema.optional(),
59987
60028
  responseMode: channelResponseModeSchema.optional(),
59988
60029
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60030
+ verbose: channelVerboseSchema.optional(),
59989
60031
  followUp: slackFollowUpOverrideSchema.optional(),
59990
60032
  timezone: timezoneSchema.optional(),
59991
60033
  topics: exports_external.record(exports_external.string(), telegramTopicRouteSchema).default({})
@@ -60003,6 +60045,7 @@ var telegramDirectMessagesSchema = exports_external.object({
60003
60045
  response: slackResponseSchema.optional(),
60004
60046
  responseMode: channelResponseModeSchema.optional(),
60005
60047
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60048
+ verbose: channelVerboseSchema.optional(),
60006
60049
  followUp: slackFollowUpOverrideSchema.optional(),
60007
60050
  timezone: timezoneSchema.optional()
60008
60051
  });
@@ -60030,10 +60073,7 @@ var telegramSchema = exports_external.object({
60030
60073
  allowBots: exports_external.boolean().default(false),
60031
60074
  groupPolicy: slackConversationPolicySchema.default("allowlist"),
60032
60075
  defaultAgentId: exports_external.string().default("default"),
60033
- privilegeCommands: privilegeCommandsSchema.default({
60034
- enabled: false,
60035
- allowUsers: []
60036
- }),
60076
+ privilegeCommands: privilegeCommandsSchema.optional(),
60037
60077
  commandPrefixes: commandPrefixesSchema.default({
60038
60078
  slash: ["::", "\\"],
60039
60079
  bash: ["!"]
@@ -60042,6 +60082,7 @@ var telegramSchema = exports_external.object({
60042
60082
  response: slackResponseSchema.default("final"),
60043
60083
  responseMode: channelResponseModeSchema.default("message-tool"),
60044
60084
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60085
+ verbose: channelVerboseSchema.default("minimal"),
60045
60086
  followUp: slackFollowUpSchema.default({
60046
60087
  mode: "auto",
60047
60088
  participationTtlMin: 5
@@ -60072,6 +60113,7 @@ var directMessagesSchema = exports_external.object({
60072
60113
  response: slackResponseSchema.optional(),
60073
60114
  responseMode: channelResponseModeSchema.optional(),
60074
60115
  additionalMessageMode: channelAdditionalMessageModeSchema.optional(),
60116
+ verbose: channelVerboseSchema.optional(),
60075
60117
  followUp: slackFollowUpOverrideSchema.optional(),
60076
60118
  timezone: timezoneSchema.optional()
60077
60119
  });
@@ -60106,10 +60148,7 @@ var slackSchema = exports_external.object({
60106
60148
  channelPolicy: slackConversationPolicySchema.default("allowlist"),
60107
60149
  groupPolicy: slackConversationPolicySchema.default("allowlist"),
60108
60150
  defaultAgentId: exports_external.string().default("default"),
60109
- privilegeCommands: privilegeCommandsSchema.default({
60110
- enabled: false,
60111
- allowUsers: []
60112
- }),
60151
+ privilegeCommands: privilegeCommandsSchema.optional(),
60113
60152
  commandPrefixes: commandPrefixesSchema.default({
60114
60153
  slash: ["::", "\\"],
60115
60154
  bash: ["!"]
@@ -60118,6 +60157,7 @@ var slackSchema = exports_external.object({
60118
60157
  response: slackResponseSchema.default("final"),
60119
60158
  responseMode: channelResponseModeSchema.default("message-tool"),
60120
60159
  additionalMessageMode: channelAdditionalMessageModeSchema.default("steer"),
60160
+ verbose: channelVerboseSchema.default("minimal"),
60121
60161
  followUp: slackFollowUpSchema.default({
60122
60162
  mode: "auto",
60123
60163
  participationTtlMin: 5
@@ -60177,6 +60217,45 @@ var clisbotConfigSchema = exports_external.object({
60177
60217
  identityLinks: {},
60178
60218
  storePath: "~/.clisbot/state/sessions.json"
60179
60219
  }),
60220
+ app: exports_external.object({
60221
+ auth: appAuthSchema.default({
60222
+ ownerClaimWindowMinutes: 30,
60223
+ defaultRole: "member",
60224
+ roles: {
60225
+ owner: {
60226
+ allow: [...APP_ADMIN_PERMISSIONS],
60227
+ users: []
60228
+ },
60229
+ admin: {
60230
+ allow: [...APP_ADMIN_PERMISSIONS],
60231
+ users: []
60232
+ },
60233
+ member: {
60234
+ allow: [],
60235
+ users: []
60236
+ }
60237
+ }
60238
+ })
60239
+ }).default({
60240
+ auth: {
60241
+ ownerClaimWindowMinutes: 30,
60242
+ defaultRole: "member",
60243
+ roles: {
60244
+ owner: {
60245
+ allow: [...APP_ADMIN_PERMISSIONS],
60246
+ users: []
60247
+ },
60248
+ admin: {
60249
+ allow: [...APP_ADMIN_PERMISSIONS],
60250
+ users: []
60251
+ },
60252
+ member: {
60253
+ allow: [],
60254
+ users: []
60255
+ }
60256
+ }
60257
+ }
60258
+ }),
60180
60259
  agents: exports_external.object({
60181
60260
  defaults: agentDefaultsSchema,
60182
60261
  list: exports_external.array(agentEntrySchema).default([
@@ -60229,6 +60308,7 @@ var clisbotConfigSchema = exports_external.object({
60229
60308
  response: "final",
60230
60309
  responseMode: "message-tool",
60231
60310
  additionalMessageMode: "steer",
60311
+ verbose: "minimal",
60232
60312
  followUp: {
60233
60313
  mode: "auto",
60234
60314
  participationTtlMin: 5
@@ -60268,17 +60348,19 @@ function resolveMaxRuntimeMs(stream) {
60268
60348
  defaultMinutes: 15
60269
60349
  });
60270
60350
  }
60271
- async function loadConfig(configPath = getDefaultConfigPath()) {
60351
+ async function loadConfig(configPath = getDefaultConfigPath(), options = {}) {
60272
60352
  const expandedConfigPath = expandHomePath(configPath);
60273
60353
  const text = await readTextFile(expandedConfigPath);
60274
60354
  const parsed = JSON.parse(text);
60355
+ assertNoLegacyPrivilegeCommands(parsed);
60275
60356
  const withDynamicDefaults = clisbotConfigSchema.parse(applyDynamicPathDefaults(parsed));
60276
60357
  const substituted = resolveConfigEnvVars(withDynamicDefaults, process.env, {
60277
60358
  skipPaths: getCredentialSkipPaths(withDynamicDefaults)
60278
60359
  });
60279
60360
  const validated = clisbotConfigSchema.parse(substituted);
60280
60361
  const materialized = materializeRuntimeChannelCredentials(validated, {
60281
- env: process.env
60362
+ env: process.env,
60363
+ materializeChannels: options.materializeChannels
60282
60364
  });
60283
60365
  return materializeLoadedConfig(expandedConfigPath, materialized);
60284
60366
  }
@@ -60286,6 +60368,7 @@ async function loadConfigWithoutEnvResolution(configPath = getDefaultConfigPath(
60286
60368
  const expandedConfigPath = expandHomePath(configPath);
60287
60369
  const text = await readTextFile(expandedConfigPath);
60288
60370
  const parsed = JSON.parse(text);
60371
+ assertNoLegacyPrivilegeCommands(parsed);
60289
60372
  const validated = clisbotConfigSchema.parse(applyDynamicPathDefaults(parsed));
60290
60373
  return materializeLoadedConfig(expandedConfigPath, validated);
60291
60374
  }
@@ -60348,6 +60431,21 @@ function applyDynamicPathDefaults(parsed, env = process.env) {
60348
60431
  function isRecord2(value) {
60349
60432
  return typeof value === "object" && value !== null && !Array.isArray(value);
60350
60433
  }
60434
+ function assertNoLegacyPrivilegeCommands(value, path2 = "root") {
60435
+ if (Array.isArray(value)) {
60436
+ value.forEach((entry, index) => assertNoLegacyPrivilegeCommands(entry, `${path2}[${index}]`));
60437
+ return;
60438
+ }
60439
+ if (!isRecord2(value)) {
60440
+ return;
60441
+ }
60442
+ if (Object.prototype.hasOwnProperty.call(value, "privilegeCommands")) {
60443
+ throw new Error(`Unsupported config key at ${path2}.privilegeCommands. Move routed permissions to app.auth and agents.<id>.auth.`);
60444
+ }
60445
+ for (const [key, entry] of Object.entries(value)) {
60446
+ assertNoLegacyPrivilegeCommands(entry, `${path2}.${key}`);
60447
+ }
60448
+ }
60351
60449
  function getAgentEntry(config, agentId) {
60352
60450
  return config.raw.agents.list.find((entry) => entry.id === agentId);
60353
60451
  }
@@ -60384,9 +60482,42 @@ function renderDefaultConfigTemplate(options = {}) {
60384
60482
  identityLinks: {},
60385
60483
  storePath: sessionStorePath
60386
60484
  },
60485
+ app: {
60486
+ auth: {
60487
+ ownerClaimWindowMinutes: 30,
60488
+ defaultRole: "member",
60489
+ roles: {
60490
+ owner: {
60491
+ allow: [...APP_ADMIN_PERMISSIONS],
60492
+ users: []
60493
+ },
60494
+ admin: {
60495
+ allow: [...APP_ADMIN_PERMISSIONS],
60496
+ users: []
60497
+ },
60498
+ member: {
60499
+ allow: [],
60500
+ users: []
60501
+ }
60502
+ }
60503
+ }
60504
+ },
60387
60505
  agents: {
60388
60506
  defaults: {
60389
60507
  workspace: workspaceTemplate,
60508
+ auth: {
60509
+ defaultRole: "member",
60510
+ roles: {
60511
+ admin: {
60512
+ allow: [...DEFAULT_AGENT_ADMIN_PERMISSIONS],
60513
+ users: []
60514
+ },
60515
+ member: {
60516
+ allow: [...DEFAULT_AGENT_MEMBER_PERMISSIONS],
60517
+ users: []
60518
+ }
60519
+ }
60520
+ },
60390
60521
  runner: {
60391
60522
  command: "codex",
60392
60523
  args: [
@@ -60486,10 +60617,6 @@ function renderDefaultConfigTemplate(options = {}) {
60486
60617
  channelPolicy: "allowlist",
60487
60618
  groupPolicy: "allowlist",
60488
60619
  defaultAgentId: "default",
60489
- privilegeCommands: {
60490
- enabled: false,
60491
- allowUsers: []
60492
- },
60493
60620
  commandPrefixes: {
60494
60621
  slash: ["::", "\\"],
60495
60622
  bash: ["!"]
@@ -60498,6 +60625,7 @@ function renderDefaultConfigTemplate(options = {}) {
60498
60625
  response: "final",
60499
60626
  responseMode: "message-tool",
60500
60627
  additionalMessageMode: "steer",
60628
+ verbose: "minimal",
60501
60629
  followUp: {
60502
60630
  mode: "auto",
60503
60631
  participationTtlMin: 5
@@ -60509,11 +60637,7 @@ function renderDefaultConfigTemplate(options = {}) {
60509
60637
  policy: "pairing",
60510
60638
  allowFrom: [],
60511
60639
  requireMention: false,
60512
- agentId: "default",
60513
- privilegeCommands: {
60514
- enabled: false,
60515
- allowUsers: []
60516
- }
60640
+ agentId: "default"
60517
60641
  }
60518
60642
  },
60519
60643
  telegram: {
@@ -60534,10 +60658,6 @@ function renderDefaultConfigTemplate(options = {}) {
60534
60658
  allowBots: false,
60535
60659
  groupPolicy: "allowlist",
60536
60660
  defaultAgentId: "default",
60537
- privilegeCommands: {
60538
- enabled: false,
60539
- allowUsers: []
60540
- },
60541
60661
  commandPrefixes: {
60542
60662
  slash: ["::", "\\"],
60543
60663
  bash: ["!"]
@@ -60546,6 +60666,7 @@ function renderDefaultConfigTemplate(options = {}) {
60546
60666
  response: "final",
60547
60667
  responseMode: "message-tool",
60548
60668
  additionalMessageMode: "steer",
60669
+ verbose: "minimal",
60549
60670
  followUp: {
60550
60671
  mode: "auto",
60551
60672
  participationTtlMin: 5
@@ -60561,11 +60682,7 @@ function renderDefaultConfigTemplate(options = {}) {
60561
60682
  allowFrom: [],
60562
60683
  requireMention: false,
60563
60684
  allowBots: false,
60564
- agentId: "default",
60565
- privilegeCommands: {
60566
- enabled: false,
60567
- allowUsers: []
60568
- }
60685
+ agentId: "default"
60569
60686
  }
60570
60687
  }
60571
60688
  }
@@ -61494,22 +61611,103 @@ class RuntimeHealthStore {
61494
61611
  // src/control/runtime-process.ts
61495
61612
  import { execFileSync, spawn as spawn2 } from "node:child_process";
61496
61613
  import { closeSync, existsSync as existsSync6, openSync, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
61497
- import { dirname as dirname9 } from "node:path";
61614
+ import { dirname as dirname10 } from "node:path";
61498
61615
  import { kill } from "node:process";
61499
61616
 
61500
61617
  // src/control/clisbot-wrapper.ts
61501
61618
  import { chmod } from "node:fs/promises";
61502
61619
  import { fileURLToPath as fileURLToPath3 } from "node:url";
61503
- import { dirname as dirname7, join as join5, sep } from "node:path";
61620
+ import { dirname as dirname8, join as join6, sep } from "node:path";
61621
+
61622
+ // src/control/runner-exit-diagnostics.ts
61623
+ import { unlink } from "node:fs/promises";
61624
+ import { dirname as dirname7, join as join5 } from "node:path";
61625
+ function shellQuote(value) {
61626
+ if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
61627
+ return value;
61628
+ }
61629
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
61630
+ }
61631
+ function buildCommandString(command, args) {
61632
+ return [command, ...args].map(shellQuote).join(" ");
61633
+ }
61634
+ function sanitizeSessionName2(sessionName) {
61635
+ return sessionName.replace(/[^a-zA-Z0-9._-]+/g, "_");
61636
+ }
61637
+ function getRunnerExitRecordPath(stateDir, sessionName) {
61638
+ return join5(stateDir, "runner-exits", `${sanitizeSessionName2(sessionName)}.json`);
61639
+ }
61640
+ function buildRunnerLaunchCommand(params) {
61641
+ const runnerCommand = buildCommandString(params.command, params.args);
61642
+ const exitRecordPath = getRunnerExitRecordPath(params.stateDir, params.sessionName);
61643
+ const exitWriterScript = [
61644
+ "const fs = require('fs');",
61645
+ "const path = require('path');",
61646
+ "const filePath = process.argv[1];",
61647
+ "const sessionName = process.argv[2];",
61648
+ "const exitCode = Number(process.argv[3]);",
61649
+ "const command = process.argv[4];",
61650
+ "fs.mkdirSync(path.dirname(filePath), { recursive: true });",
61651
+ "fs.writeFileSync(filePath, JSON.stringify({ sessionName, exitCode, command, exitedAt: new Date().toISOString() }) + '\\n');"
61652
+ ].join(" ");
61653
+ const exports = [
61654
+ `export PATH=${shellQuote(params.wrapperDir)}:"$PATH"`,
61655
+ `export CLISBOT_BIN=${shellQuote(params.wrapperPath)}`
61656
+ ];
61657
+ return [
61658
+ ...exports,
61659
+ `rm -f ${shellQuote(exitRecordPath)}`,
61660
+ runnerCommand,
61661
+ "status=$?",
61662
+ `node -e ${shellQuote(exitWriterScript)} ${shellQuote(exitRecordPath)} ${shellQuote(params.sessionName)} "$status" ${shellQuote(runnerCommand)} || true`,
61663
+ 'exit "$status"'
61664
+ ].join("; ");
61665
+ }
61666
+ async function clearRunnerExitRecord(stateDir, sessionName) {
61667
+ const exitRecordPath = getRunnerExitRecordPath(stateDir, sessionName);
61668
+ if (!await fileExists(exitRecordPath)) {
61669
+ return;
61670
+ }
61671
+ try {
61672
+ await unlink(exitRecordPath);
61673
+ } catch {}
61674
+ }
61675
+ async function readRunnerExitRecord(stateDir, sessionName) {
61676
+ const exitRecordPath = getRunnerExitRecordPath(stateDir, sessionName);
61677
+ if (!await fileExists(exitRecordPath)) {
61678
+ return null;
61679
+ }
61680
+ try {
61681
+ const text = await readTextFile(exitRecordPath);
61682
+ const parsed = JSON.parse(text);
61683
+ const exitCode = parsed.exitCode;
61684
+ if (typeof parsed.sessionName !== "string" || typeof exitCode !== "number" || !Number.isFinite(exitCode) || typeof parsed.command !== "string" || typeof parsed.exitedAt !== "string") {
61685
+ return null;
61686
+ }
61687
+ return {
61688
+ sessionName: parsed.sessionName,
61689
+ exitCode,
61690
+ command: parsed.command,
61691
+ exitedAt: parsed.exitedAt
61692
+ };
61693
+ } catch {
61694
+ return null;
61695
+ }
61696
+ }
61697
+ async function ensureRunnerExitRecordDir(stateDir, sessionName) {
61698
+ await ensureDir(dirname7(getRunnerExitRecordPath(stateDir, sessionName)));
61699
+ }
61700
+
61701
+ // src/control/clisbot-wrapper.ts
61504
61702
  function getDefaultClisbotBinDir(env = process.env) {
61505
- return join5(resolveAppHomeDir(env), "bin");
61703
+ return join6(resolveAppHomeDir(env), "bin");
61506
61704
  }
61507
61705
  function getDefaultClisbotWrapperPath(env = process.env) {
61508
- return join5(getDefaultClisbotBinDir(env), "clisbot");
61706
+ return join6(getDefaultClisbotBinDir(env), "clisbot");
61509
61707
  }
61510
61708
  var DEFAULT_CLISBOT_BIN_DIR = getDefaultClisbotBinDir();
61511
61709
  var DEFAULT_CLISBOT_WRAPPER_PATH = getDefaultClisbotWrapperPath();
61512
- function shellQuote(value) {
61710
+ function shellQuote2(value) {
61513
61711
  if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
61514
61712
  return value;
61515
61713
  }
@@ -61532,7 +61730,7 @@ function getClisbotPromptCommand() {
61532
61730
  return isPackagedRuntime() ? "clis" : getClisbotWrapperPath();
61533
61731
  }
61534
61732
  function getClisbotWrapperDir() {
61535
- return dirname7(getClisbotWrapperPath());
61733
+ return dirname8(getClisbotWrapperPath());
61536
61734
  }
61537
61735
  function renderClisbotWrapperScript() {
61538
61736
  const execPath = process.execPath;
@@ -61540,14 +61738,14 @@ function renderClisbotWrapperScript() {
61540
61738
  return [
61541
61739
  "#!/usr/bin/env bash",
61542
61740
  "set -euo pipefail",
61543
- `exec ${shellQuote(execPath)} ${shellQuote(mainScriptPath)} "$@"`,
61741
+ `exec ${shellQuote2(execPath)} ${shellQuote2(mainScriptPath)} "$@"`,
61544
61742
  ""
61545
61743
  ].join(`
61546
61744
  `);
61547
61745
  }
61548
61746
  async function ensureClisbotWrapper() {
61549
61747
  const wrapperPath = getClisbotWrapperPath();
61550
- const wrapperDir = dirname7(wrapperPath);
61748
+ const wrapperDir = dirname8(wrapperPath);
61551
61749
  await ensureDir2(wrapperDir);
61552
61750
  const nextScript = renderClisbotWrapperScript();
61553
61751
  const existing = await fileExists(wrapperPath) ? await readTextFile(wrapperPath) : null;
@@ -61564,7 +61762,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
61564
61762
  // src/shared/process.ts
61565
61763
  import { spawn } from "node:child_process";
61566
61764
  import { existsSync as existsSync5 } from "node:fs";
61567
- import { delimiter, join as join6 } from "node:path";
61765
+ import { delimiter, join as join7 } from "node:path";
61568
61766
  function sleep(ms) {
61569
61767
  return new Promise((resolve) => {
61570
61768
  setTimeout(resolve, ms);
@@ -61582,7 +61780,7 @@ function commandExists(command) {
61582
61780
  const executableNames = getExecutableNames(command);
61583
61781
  for (const directory of pathEntries) {
61584
61782
  for (const executableName of executableNames) {
61585
- if (existsSync5(join6(directory, executableName))) {
61783
+ if (existsSync5(join7(directory, executableName))) {
61586
61784
  return true;
61587
61785
  }
61588
61786
  }
@@ -61906,7 +62104,7 @@ function getProcessLiveness(pid, dependencies = {}) {
61906
62104
  async function ensureConfigFile(configPath, options = {}) {
61907
62105
  await ensureClisbotWrapper();
61908
62106
  const expandedConfigPath = resolveConfigPath(configPath);
61909
- await ensureDir2(dirname9(expandedConfigPath));
62107
+ await ensureDir2(dirname10(expandedConfigPath));
61910
62108
  if (existsSync6(expandedConfigPath)) {
61911
62109
  return {
61912
62110
  configPath: expandedConfigPath,
@@ -61943,8 +62141,8 @@ async function startDetachedRuntime(params) {
61943
62141
  rmSync2(pidPath, { force: true });
61944
62142
  }
61945
62143
  const configResult = await ensureConfigFile(params.configPath);
61946
- await ensureDir2(dirname9(pidPath));
61947
- await ensureDir2(dirname9(logPath));
62144
+ await ensureDir2(dirname10(pidPath));
62145
+ await ensureDir2(dirname10(logPath));
61948
62146
  const logStartOffset = getLogSize(logPath);
61949
62147
  const logFd = openSync(logPath, "a");
61950
62148
  const child = spawn2(process.execPath, [params.scriptPath, "serve-foreground"], {
@@ -62034,7 +62232,7 @@ async function disableExpiredMemAccountsInConfig(configPath) {
62034
62232
  }
62035
62233
  async function writeRuntimePid(pidPath, pid = process.pid) {
62036
62234
  const expandedPidPath = resolvePidPath(pidPath);
62037
- await ensureDir2(dirname9(expandedPidPath));
62235
+ await ensureDir2(dirname10(expandedPidPath));
62038
62236
  await writeTextFile(expandedPidPath, `${pid}
62039
62237
  `);
62040
62238
  }
@@ -62655,7 +62853,13 @@ function buildConfiguredTargetFromIdentity(identity) {
62655
62853
  };
62656
62854
  }
62657
62855
  function renderFieldLabel(field) {
62658
- return field === "responseMode" ? "response-mode" : "additional-message-mode";
62856
+ if (field === "responseMode") {
62857
+ return "response-mode";
62858
+ }
62859
+ if (field === "additionalMessageMode") {
62860
+ return "additional-message-mode";
62861
+ }
62862
+ return "streaming";
62659
62863
  }
62660
62864
 
62661
62865
  // src/channels/additional-message-mode-config.ts
@@ -62747,217 +62951,21 @@ async function setConfiguredResponseMode(params) {
62747
62951
  }
62748
62952
 
62749
62953
  // src/control/channel-privilege-cli.ts
62750
- function getEditableConfigPath5() {
62751
- return process.env.CLISBOT_CONFIG_PATH;
62752
- }
62753
- function parseTarget(raw) {
62754
- if (raw === "slack-dm" || raw === "slack-channel" || raw === "slack-group" || raw === "telegram-dm" || raw === "telegram-group") {
62755
- return raw;
62756
- }
62757
- throw new Error(renderPrivilegeCliHelp());
62758
- }
62759
- function parseOptionValue2(args, name) {
62760
- const index = args.findIndex((arg) => arg === name);
62761
- if (index === -1) {
62762
- return;
62763
- }
62764
- const value = args[index + 1]?.trim();
62765
- if (!value) {
62766
- throw new Error(`Missing value for ${name}`);
62767
- }
62768
- return value;
62769
- }
62770
- function ensureAllowUsersList(value) {
62771
- return {
62772
- enabled: value?.enabled ?? false,
62773
- allowUsers: value?.allowUsers ?? []
62774
- };
62775
- }
62776
- function renderPrivilegeCliHelp() {
62954
+ function renderChannelPrivilegeCliRemovedMessage() {
62777
62955
  return [
62778
- "Usage:",
62779
- " clisbot channels privilege enable slack-dm",
62780
- " clisbot channels privilege disable slack-dm",
62781
- " clisbot channels privilege allow-user slack-dm <userId>",
62782
- " clisbot channels privilege remove-user slack-dm <userId>",
62783
- " clisbot channels privilege enable slack-channel <channelId>",
62784
- " clisbot channels privilege allow-user slack-channel <channelId> <userId>",
62785
- " clisbot channels privilege enable slack-group <groupId>",
62786
- " clisbot channels privilege allow-user slack-group <groupId> <userId>",
62787
- " clisbot channels privilege enable telegram-dm",
62788
- " clisbot channels privilege allow-user telegram-dm <userId>",
62789
- " clisbot channels privilege enable telegram-group <chatId> [--topic <topicId>]",
62790
- " clisbot channels privilege allow-user telegram-group <chatId> <userId> [--topic <topicId>]"
62956
+ "`clisbot channels privilege` has been removed.",
62957
+ "Manage routed permissions through `app.auth` and `agents.<id>.auth` instead.",
62958
+ "Grant `shellExecute` on the target agent role when `/bash` should be allowed."
62791
62959
  ].join(`
62792
62960
  `);
62793
62961
  }
62794
- function addUniqueUser(users, userId) {
62795
- const normalized = userId.trim();
62796
- return normalized && !users.includes(normalized) ? [...users, normalized] : users;
62797
- }
62798
- function removeUser(users, userId) {
62799
- const normalized = userId.trim();
62800
- return users.filter((value) => value !== normalized);
62801
- }
62802
- async function runChannelPrivilegeCli(args) {
62803
- const action = args[0];
62804
- const target = parseTarget(args[1]);
62805
- const rest = args.slice(2);
62806
- const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
62807
- if (target === "slack-dm") {
62808
- const current2 = ensureAllowUsersList(config.channels.slack.directMessages.privilegeCommands);
62809
- await applyPrivilegeAction({
62810
- action,
62811
- current: current2,
62812
- args: rest,
62813
- set: (next) => {
62814
- config.channels.slack.directMessages.privilegeCommands = next;
62815
- },
62816
- configPath,
62817
- label: "slack direct messages",
62818
- save: async () => writeEditableConfig(configPath, config)
62819
- });
62820
- return;
62821
- }
62822
- if (target === "telegram-dm") {
62823
- const current2 = ensureAllowUsersList(config.channels.telegram.directMessages.privilegeCommands);
62824
- await applyPrivilegeAction({
62825
- action,
62826
- current: current2,
62827
- args: rest,
62828
- set: (next) => {
62829
- config.channels.telegram.directMessages.privilegeCommands = next;
62830
- },
62831
- configPath,
62832
- label: "telegram direct messages",
62833
- save: async () => writeEditableConfig(configPath, config)
62834
- });
62835
- return;
62836
- }
62837
- if (target === "slack-channel" || target === "slack-group") {
62838
- const routeId = rest[0]?.trim();
62839
- if (!routeId) {
62840
- throw new Error(renderPrivilegeCliHelp());
62841
- }
62842
- const routes = target === "slack-channel" ? config.channels.slack.channels : config.channels.slack.groups;
62843
- const route = routes[routeId];
62844
- if (!route) {
62845
- throw new Error(`Route not configured yet: ${target} ${routeId}. Add the route first with \`clisbot channels add ...\`.`);
62846
- }
62847
- const current2 = ensureAllowUsersList(route.privilegeCommands);
62848
- await applyPrivilegeAction({
62849
- action,
62850
- current: current2,
62851
- args: rest.slice(1),
62852
- set: (next) => {
62853
- route.privilegeCommands = next;
62854
- },
62855
- configPath,
62856
- label: `${target} ${routeId}`,
62857
- save: async () => writeEditableConfig(configPath, config)
62858
- });
62859
- return;
62860
- }
62861
- const chatId = rest[0]?.trim();
62862
- if (!chatId) {
62863
- throw new Error(renderPrivilegeCliHelp());
62864
- }
62865
- const topicId = parseOptionValue2(rest, "--topic");
62866
- const group = config.channels.telegram.groups[chatId];
62867
- if (!group) {
62868
- throw new Error(renderTelegramRouteChoiceMessage({ chatId }));
62869
- }
62870
- if (topicId) {
62871
- const topic = group.topics?.[topicId];
62872
- if (!topic) {
62873
- throw new Error(renderTelegramRouteChoiceMessage({ chatId, topicId }));
62874
- }
62875
- const current2 = ensureAllowUsersList(topic.privilegeCommands);
62876
- await applyPrivilegeAction({
62877
- action,
62878
- current: current2,
62879
- args: rest.filter((value, index) => {
62880
- if (index === 0) {
62881
- return false;
62882
- }
62883
- return value !== "--topic" && value !== topicId;
62884
- }),
62885
- set: (next) => {
62886
- topic.privilegeCommands = next;
62887
- },
62888
- configPath,
62889
- label: `telegram topic ${chatId}/${topicId}`,
62890
- save: async () => writeEditableConfig(configPath, config)
62891
- });
62892
- return;
62893
- }
62894
- const current = ensureAllowUsersList(group.privilegeCommands);
62895
- await applyPrivilegeAction({
62896
- action,
62897
- current,
62898
- args: rest.slice(1),
62899
- set: (next) => {
62900
- group.privilegeCommands = next;
62901
- },
62902
- configPath,
62903
- label: `telegram group ${chatId}`,
62904
- save: async () => writeEditableConfig(configPath, config)
62905
- });
62906
- }
62907
- async function applyPrivilegeAction(params) {
62908
- if (params.action === "enable") {
62909
- params.set({
62910
- enabled: true,
62911
- allowUsers: params.current.allowUsers
62912
- });
62913
- await params.save();
62914
- console.log(`enabled privilege commands for ${params.label}`);
62915
- console.log(`config: ${params.configPath}`);
62916
- return;
62917
- }
62918
- if (params.action === "disable") {
62919
- params.set({
62920
- enabled: false,
62921
- allowUsers: params.current.allowUsers
62922
- });
62923
- await params.save();
62924
- console.log(`disabled privilege commands for ${params.label}`);
62925
- console.log(`config: ${params.configPath}`);
62926
- return;
62927
- }
62928
- if (params.action === "allow-user") {
62929
- const userId = params.args[0]?.trim();
62930
- if (!userId) {
62931
- throw new Error(renderPrivilegeCliHelp());
62932
- }
62933
- params.set({
62934
- enabled: params.current.enabled,
62935
- allowUsers: addUniqueUser(params.current.allowUsers, userId)
62936
- });
62937
- await params.save();
62938
- console.log(`allowed ${userId} to use privilege commands for ${params.label}`);
62939
- console.log(`config: ${params.configPath}`);
62940
- return;
62941
- }
62942
- if (params.action === "remove-user") {
62943
- const userId = params.args[0]?.trim();
62944
- if (!userId) {
62945
- throw new Error(renderPrivilegeCliHelp());
62946
- }
62947
- params.set({
62948
- enabled: params.current.enabled,
62949
- allowUsers: removeUser(params.current.allowUsers, userId)
62950
- });
62951
- await params.save();
62952
- console.log(`removed ${userId} from privilege commands for ${params.label}`);
62953
- console.log(`config: ${params.configPath}`);
62954
- return;
62955
- }
62956
- throw new Error(renderPrivilegeCliHelp());
62962
+ async function runChannelPrivilegeCli(_args) {
62963
+ throw new Error(renderChannelPrivilegeCliRemovedMessage());
62957
62964
  }
62958
62965
 
62959
62966
  // src/control/channels-cli.ts
62960
- function getEditableConfigPath6() {
62967
+ var AUTH_USER_GUIDE_DOC_PATH = "docs/user-guide/auth-and-roles.md";
62968
+ function getEditableConfigPath5() {
62961
62969
  return process.env.CLISBOT_CONFIG_PATH;
62962
62970
  }
62963
62971
  function renderChannelsHelp() {
@@ -62975,7 +62983,6 @@ function renderChannelsHelp() {
62975
62983
  " clisbot channels remove slack-channel <channelId>",
62976
62984
  " clisbot channels add slack-group <groupId> [--agent <id>] [--require-mention true|false]",
62977
62985
  " clisbot channels remove slack-group <groupId>",
62978
- " clisbot channels privilege <enable|disable|allow-user|remove-user> <target> ...",
62979
62986
  " clisbot channels response-mode status --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
62980
62987
  " clisbot channels response-mode set <capture-pane|message-tool> --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
62981
62988
  " clisbot channels additional-message-mode status --channel <slack|telegram> [--target <target>] [--topic <topicId>]",
@@ -62991,7 +62998,8 @@ function renderChannelsHelp() {
62991
62998
  " - Telegram groups need channels.telegram.groups.<chatId>",
62992
62999
  " - Telegram forum topics need channels.telegram.groups.<chatId>.topics.<topicId>",
62993
63000
  " - Adding a route puts that surface on the allowlist; other channels, groups, or topics still need to be added explicitly",
62994
- " - Tune route settings such as requireMention, privilegeCommands, and followUp in clisbot.json when a surface should behave differently",
63001
+ " - Tune route settings such as requireMention and followUp in clisbot.json when a surface should behave differently",
63002
+ ` - Manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`,
62995
63003
  " - Response delivery can be tuned with responseMode: `capture-pane` or `message-tool`",
62996
63004
  " - Busy-session follow-up can be tuned with additionalMessageMode: `steer` or `queue`",
62997
63005
  " - Slack response-mode targets use `channel:<id>`, `group:<id>`, or `dm:<id>`",
@@ -63024,7 +63032,6 @@ function renderChannelsHelp() {
63024
63032
  "Next steps:",
63025
63033
  " - Run `clisbot status` to inspect routes and current channel state",
63026
63034
  " - Run `clisbot logs` if the bot is still not responding",
63027
- ...renderGenericPrivilegeCommandHelpLines(),
63028
63035
  ...renderChannelSetupHelpLines("", { includePrivilegeHelp: false })
63029
63036
  ].join(`
63030
63037
  `);
@@ -63079,7 +63086,7 @@ function parseResponseModeTarget(channel, raw) {
63079
63086
  }
63080
63087
  return target;
63081
63088
  }
63082
- function parseOptionValue3(args, name) {
63089
+ function parseOptionValue2(args, name) {
63083
63090
  const index = args.findIndex((arg) => arg === name);
63084
63091
  if (index === -1) {
63085
63092
  return;
@@ -63091,7 +63098,7 @@ function parseOptionValue3(args, name) {
63091
63098
  return value;
63092
63099
  }
63093
63100
  function parseBooleanOption(args, name, fallback) {
63094
- const raw = parseOptionValue3(args, name);
63101
+ const raw = parseOptionValue2(args, name);
63095
63102
  if (!raw) {
63096
63103
  return fallback;
63097
63104
  }
@@ -63104,10 +63111,10 @@ function parseBooleanOption(args, name, fallback) {
63104
63111
  throw new Error(`${name} requires true or false`);
63105
63112
  }
63106
63113
  function getAgentId(args) {
63107
- return parseOptionValue3(args, "--agent") ?? "default";
63114
+ return parseOptionValue2(args, "--agent") ?? "default";
63108
63115
  }
63109
63116
  async function setChannelEnabled(action, channel) {
63110
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63117
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63111
63118
  const enabled = action === "enable";
63112
63119
  const current = config.channels[channel].enabled;
63113
63120
  if (current === enabled) {
@@ -63127,8 +63134,8 @@ async function addTelegramGroup(args) {
63127
63134
  if (!chatId) {
63128
63135
  throw new Error("Usage: clisbot channels add telegram-group <chatId> [--topic <topicId>] [--agent <id>] [--require-mention true|false]");
63129
63136
  }
63130
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63131
- const topicId = parseOptionValue3(args, "--topic");
63137
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63138
+ const topicId = parseOptionValue2(args, "--topic");
63132
63139
  const agentId = getAgentId(args);
63133
63140
  const requireMention = parseBooleanOption(args, "--require-mention", true);
63134
63141
  const groupRoute = config.channels.telegram.groups[chatId] ?? {
@@ -63180,8 +63187,8 @@ async function removeTelegramGroup(args) {
63180
63187
  if (!chatId) {
63181
63188
  throw new Error("Usage: clisbot channels remove telegram-group <chatId> [--topic <topicId>]");
63182
63189
  }
63183
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63184
- const topicId = parseOptionValue3(args, "--topic");
63190
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63191
+ const topicId = parseOptionValue2(args, "--topic");
63185
63192
  const groupRoute = config.channels.telegram.groups[chatId];
63186
63193
  if (!groupRoute) {
63187
63194
  console.log(`telegram group route ${chatId} is not configured`);
@@ -63210,7 +63217,7 @@ async function addSlackRoute(kind, args) {
63210
63217
  if (!routeId) {
63211
63218
  throw new Error(`Usage: clisbot channels add slack-${kind} <${kind}Id> [--agent <id>] [--require-mention true|false]`);
63212
63219
  }
63213
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63220
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63214
63221
  const agentId = getAgentId(args);
63215
63222
  const requireMention = parseBooleanOption(args, "--require-mention", false);
63216
63223
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
@@ -63234,7 +63241,7 @@ async function removeSlackRoute(kind, args) {
63234
63241
  if (!routeId) {
63235
63242
  throw new Error(`Usage: clisbot channels remove slack-${kind} <${kind}Id>`);
63236
63243
  }
63237
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63244
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63238
63245
  const target = kind === "channel" ? config.channels.slack.channels : config.channels.slack.groups;
63239
63246
  if (!target[routeId]) {
63240
63247
  console.log(`slack ${kind} route ${routeId} is not configured`);
@@ -63247,7 +63254,7 @@ async function removeSlackRoute(kind, args) {
63247
63254
  console.log(`config: ${configPath}`);
63248
63255
  }
63249
63256
  async function setToken(target, value) {
63250
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63257
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63251
63258
  if (target === "slack-app") {
63252
63259
  config.channels.slack.appToken = value;
63253
63260
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63279,7 +63286,7 @@ async function setToken(target, value) {
63279
63286
  console.log(`config: ${configPath}`);
63280
63287
  }
63281
63288
  async function clearToken(target) {
63282
- const { config, configPath } = await readEditableConfig(getEditableConfigPath6());
63289
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath5());
63283
63290
  if (target === "slack-app") {
63284
63291
  config.channels.slack.appToken = "";
63285
63292
  const defaultAccountId = config.channels.slack.defaultAccount || "default";
@@ -63323,9 +63330,9 @@ async function runResponseModeCli(args) {
63323
63330
  }
63324
63331
  const responseMode = action === "set" ? parseResponseMode2(args[1]) : undefined;
63325
63332
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
63326
- const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
63327
- const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
63328
- const topic = parseOptionValue3(optionArgs, "--topic");
63333
+ const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
63334
+ const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
63335
+ const topic = parseOptionValue2(optionArgs, "--topic");
63329
63336
  if (channel === "slack" && topic) {
63330
63337
  throw new Error("Slack response-mode commands do not support --topic");
63331
63338
  }
@@ -63357,9 +63364,9 @@ async function runAdditionalMessageModeCli(args) {
63357
63364
  }
63358
63365
  const additionalMessageMode = action === "set" ? parseAdditionalMessageMode2(args[1]) : undefined;
63359
63366
  const optionArgs = action === "set" ? args.slice(2) : args.slice(1);
63360
- const channel = parseResponseModeChannel(parseOptionValue3(optionArgs, "--channel"));
63361
- const target = parseResponseModeTarget(channel, parseOptionValue3(optionArgs, "--target"));
63362
- const topic = parseOptionValue3(optionArgs, "--topic");
63367
+ const channel = parseResponseModeChannel(parseOptionValue2(optionArgs, "--channel"));
63368
+ const target = parseResponseModeTarget(channel, parseOptionValue2(optionArgs, "--target"));
63369
+ const topic = parseOptionValue2(optionArgs, "--topic");
63363
63370
  if (channel === "slack" && topic) {
63364
63371
  throw new Error("Slack additional-message-mode commands do not support --topic");
63365
63372
  }
@@ -63422,16 +63429,10 @@ function renderRouteAddGuidance(params) {
63422
63429
  console.log("Slack route next steps:");
63423
63430
  console.log(` - route added: ${routePath}`);
63424
63431
  console.log(" - direct messages still follow channels.slack.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
63425
- console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention, privilegeCommands, and followUp`);
63432
+ console.log(` - this ${routeLabel} still follows channels.slack.groupPolicy and route settings such as requireMention and followUp`);
63426
63433
  console.log(" - if you want pairing-style access control for DMs, set channels.slack.directMessages.policy to `pairing`");
63427
63434
  console.log(" - if you want stricter route access, keep Slack groups on allowlist and only add the channels/groups you trust");
63428
- for (const line of renderPrivilegeCommandHelpLines({
63429
- platform: "slack",
63430
- conversationKind: params.kind === "group" ? "group" : "channel",
63431
- channelId: params.routeId
63432
- }, " ")) {
63433
- console.log(line);
63434
- }
63435
+ console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
63435
63436
  } else {
63436
63437
  const [chatId, topicId] = params.routeId.split("/");
63437
63438
  const routePath = params.kind === "topic" ? `channels.telegram.groups."${chatId}".topics."${topicId}"` : `channels.telegram.groups."${params.routeId}"`;
@@ -63440,15 +63441,8 @@ function renderRouteAddGuidance(params) {
63440
63441
  console.log(" - direct messages still follow channels.telegram.directMessages.policy (`open`, `pairing`, `allowlist`, or `disabled`)");
63441
63442
  console.log(` - this ${params.kind} is now on the Telegram allowlist; other groups or topics still need to be added explicitly`);
63442
63443
  console.log(" - if you want pairing-style access control for DMs, set channels.telegram.directMessages.policy to `pairing`");
63443
- console.log(" - tune route settings such as requireMention, privilegeCommands, and followUp in clisbot.json if this surface should behave differently");
63444
- for (const line of renderPrivilegeCommandHelpLines({
63445
- platform: "telegram",
63446
- conversationKind: params.kind === "topic" ? "topic" : "group",
63447
- chatId: chatId ?? params.routeId,
63448
- topicId
63449
- }, " ")) {
63450
- console.log(line);
63451
- }
63444
+ console.log(" - tune route settings such as requireMention and followUp in clisbot.json if this surface should behave differently");
63445
+ console.log(` - manage routed auth and /bash access in ${AUTH_USER_GUIDE_DOC_PATH}`);
63452
63446
  }
63453
63447
  console.log("Run `clisbot status` to inspect routes and current channel state.");
63454
63448
  console.log("Run `clisbot logs` if the bot is still not responding.");
@@ -63542,7 +63536,7 @@ function parseBotType(rawValue) {
63542
63536
  }
63543
63537
  throw new Error(`Invalid bot type: ${rawValue}`);
63544
63538
  }
63545
- function parseOptionValue4(args, name, index) {
63539
+ function parseOptionValue3(args, name, index) {
63546
63540
  const value = args[index + 1]?.trim();
63547
63541
  if (!value) {
63548
63542
  throw new Error(`Missing value for ${name}`);
@@ -63597,17 +63591,17 @@ function parseBootstrapFlags(args) {
63597
63591
  for (let index = 0;index < args.length; index += 1) {
63598
63592
  const arg = args[index];
63599
63593
  if (arg === "--cli") {
63600
- cliTool = parseOptionValue4(args, arg, index);
63594
+ cliTool = parseOptionValue3(args, arg, index);
63601
63595
  index += 1;
63602
63596
  continue;
63603
63597
  }
63604
63598
  if (arg === "--bootstrap") {
63605
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
63599
+ bootstrap = parseBotType(parseOptionValue3(args, arg, index));
63606
63600
  index += 1;
63607
63601
  continue;
63608
63602
  }
63609
63603
  if (arg === "--bot-type") {
63610
- bootstrap = parseBotType(parseOptionValue4(args, arg, index));
63604
+ bootstrap = parseBotType(parseOptionValue3(args, arg, index));
63611
63605
  index += 1;
63612
63606
  continue;
63613
63607
  }
@@ -63616,7 +63610,7 @@ function parseBootstrapFlags(args) {
63616
63610
  continue;
63617
63611
  }
63618
63612
  if (arg === "--slack-account") {
63619
- const accountId = parseOptionValue4(args, arg, index);
63613
+ const accountId = parseOptionValue3(args, arg, index);
63620
63614
  ensureUniqueAccount(slackAccounts, accountId, "--slack-account");
63621
63615
  currentSlackAccountId = accountId;
63622
63616
  getOrCreateSlackAccount(slackAccounts, accountId);
@@ -63625,7 +63619,7 @@ function parseBootstrapFlags(args) {
63625
63619
  continue;
63626
63620
  }
63627
63621
  if (arg === "--telegram-account") {
63628
- const accountId = parseOptionValue4(args, arg, index);
63622
+ const accountId = parseOptionValue3(args, arg, index);
63629
63623
  ensureUniqueAccount(telegramAccounts, accountId, "--telegram-account");
63630
63624
  currentTelegramAccountId = accountId;
63631
63625
  getOrCreateTelegramAccount(telegramAccounts, accountId);
@@ -63634,7 +63628,7 @@ function parseBootstrapFlags(args) {
63634
63628
  continue;
63635
63629
  }
63636
63630
  if (arg === "--slack-app-token") {
63637
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63631
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63638
63632
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63639
63633
  account.appToken = token;
63640
63634
  sawCredentialFlags = true;
@@ -63643,7 +63637,7 @@ function parseBootstrapFlags(args) {
63643
63637
  continue;
63644
63638
  }
63645
63639
  if (arg === "--slack-bot-token") {
63646
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63640
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63647
63641
  const account = getOrCreateSlackAccount(slackAccounts, currentSlackAccountId ?? "default");
63648
63642
  account.botToken = token;
63649
63643
  sawCredentialFlags = true;
@@ -63652,7 +63646,7 @@ function parseBootstrapFlags(args) {
63652
63646
  continue;
63653
63647
  }
63654
63648
  if (arg === "--telegram-bot-token") {
63655
- const token = parseTokenInput(parseOptionValue4(args, arg, index));
63649
+ const token = parseTokenInput(parseOptionValue3(args, arg, index));
63656
63650
  const account = getOrCreateTelegramAccount(telegramAccounts, currentTelegramAccountId ?? "default");
63657
63651
  account.botToken = token;
63658
63652
  sawCredentialFlags = true;
@@ -63736,6 +63730,8 @@ class AgentSessionState {
63736
63730
  startedAt: entry?.runtime?.startedAt,
63737
63731
  detachedAt: entry?.runtime?.detachedAt,
63738
63732
  finalReplyAt: entry?.runtime?.finalReplyAt,
63733
+ lastMessageToolReplyAt: entry?.runtime?.lastMessageToolReplyAt,
63734
+ messageToolFinalReplyAt: entry?.runtime?.messageToolFinalReplyAt,
63739
63735
  sessionKey: target.sessionKey,
63740
63736
  agentId: target.agentId
63741
63737
  };
@@ -63747,6 +63743,8 @@ class AgentSessionState {
63747
63743
  startedAt: entry.runtime.startedAt,
63748
63744
  detachedAt: entry.runtime.detachedAt,
63749
63745
  finalReplyAt: entry.runtime.finalReplyAt,
63746
+ lastMessageToolReplyAt: entry.runtime.lastMessageToolReplyAt,
63747
+ messageToolFinalReplyAt: entry.runtime.messageToolFinalReplyAt,
63750
63748
  sessionKey: entry.sessionKey,
63751
63749
  agentId: entry.agentId
63752
63750
  }));
@@ -63880,7 +63878,7 @@ class AgentSessionState {
63880
63878
  }
63881
63879
  return this.resetConversationFollowUpMode(resolved);
63882
63880
  }
63883
- async recordConversationReply(resolved, kind = "reply") {
63881
+ async recordConversationReply(resolved, kind = "reply", source = "channel") {
63884
63882
  const repliedAt = Date.now();
63885
63883
  return this.upsertSessionEntry(resolved, (existing) => ({
63886
63884
  sessionId: existing?.sessionId,
@@ -63889,9 +63887,17 @@ class AgentSessionState {
63889
63887
  lastBotReplyAt: repliedAt
63890
63888
  },
63891
63889
  runnerCommand: existing?.runnerCommand ?? resolved.runner.command,
63892
- runtime: kind === "final" && existing?.runtime && existing.runtime.state !== "idle" ? {
63890
+ runtime: existing?.runtime && existing.runtime.state !== "idle" ? {
63893
63891
  ...existing.runtime,
63894
- finalReplyAt: repliedAt
63892
+ ...kind === "final" ? {
63893
+ finalReplyAt: repliedAt
63894
+ } : {},
63895
+ ...source === "message-tool" ? {
63896
+ lastMessageToolReplyAt: repliedAt,
63897
+ ...kind === "final" ? {
63898
+ messageToolFinalReplyAt: repliedAt
63899
+ } : {}
63900
+ } : {}
63895
63901
  } : existing?.runtime,
63896
63902
  intervalLoops: existing?.intervalLoops
63897
63903
  }));
@@ -63918,7 +63924,7 @@ function hasActiveRuntime(entry) {
63918
63924
  }
63919
63925
 
63920
63926
  // src/agents/session-store.ts
63921
- import { dirname as dirname10 } from "node:path";
63927
+ import { dirname as dirname11 } from "node:path";
63922
63928
  import { randomUUID as randomUUID3 } from "node:crypto";
63923
63929
  import { rename } from "node:fs/promises";
63924
63930
  class SessionStore {
@@ -64011,7 +64017,7 @@ class SessionStore {
64011
64017
  return parsed;
64012
64018
  }
64013
64019
  async writeStore(store) {
64014
- await ensureDir2(dirname10(this.storePath));
64020
+ await ensureDir2(dirname11(this.storePath));
64015
64021
  const tempPath = `${this.storePath}.${process.pid}.${randomUUID3()}.tmp`;
64016
64022
  await writeTextFile(tempPath, JSON.stringify(store, null, 2));
64017
64023
  await rename(tempPath, this.storePath);
@@ -64019,7 +64025,7 @@ class SessionStore {
64019
64025
  }
64020
64026
 
64021
64027
  // src/control/loops-cli.ts
64022
- function getEditableConfigPath7() {
64028
+ function getEditableConfigPath6() {
64023
64029
  return process.env.CLISBOT_CONFIG_PATH;
64024
64030
  }
64025
64031
  function renderLoopsHelp() {
@@ -64077,7 +64083,7 @@ function getSessionState(sessionStorePath) {
64077
64083
  return new AgentSessionState(new SessionStore(sessionStorePath));
64078
64084
  }
64079
64085
  async function loadLoopControlState() {
64080
- const configPath = await ensureEditableConfigFile(getEditableConfigPath7());
64086
+ const configPath = await ensureEditableConfigFile(getEditableConfigPath6());
64081
64087
  const loadedConfig = await loadConfigWithoutEnvResolution(configPath);
64082
64088
  const sessionStorePath = resolveSessionStorePath(loadedConfig);
64083
64089
  return {
@@ -64146,23 +64152,158 @@ async function runLoopsCli(args) {
64146
64152
  // src/agents/agent-service.ts
64147
64153
  import { randomUUID as randomUUID4 } from "node:crypto";
64148
64154
 
64149
- // src/agents/session-key.ts
64150
- var DEFAULT_MAIN_KEY = "main";
64151
- var DEFAULT_ACCOUNT_ID = "default";
64152
- function normalizeToken(value) {
64153
- return (value ?? "").trim().toLowerCase();
64154
- }
64155
- function normalizeMainKey(value) {
64156
- return normalizeToken(value) || DEFAULT_MAIN_KEY;
64155
+ // src/channels/agent-prompt.ts
64156
+ function buildAgentPromptText(params) {
64157
+ if (!params.config.enabled) {
64158
+ return params.text;
64159
+ }
64160
+ const systemBlock = renderAgentPromptInstruction(params);
64161
+ return `<system>
64162
+ ${systemBlock}
64163
+ </system>
64164
+
64165
+ <user>
64166
+ ${params.text}
64167
+ </user>`;
64157
64168
  }
64158
- function normalizeAgentId(value) {
64159
- const trimmed = (value ?? "").trim();
64160
- if (!trimmed) {
64161
- return "default";
64169
+ function renderAgentPromptInstruction(params) {
64170
+ const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
64171
+ const progressAllowed = messageToolMode && (params.streaming ?? "all") !== "off";
64172
+ const lines = [
64173
+ `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
64174
+ "",
64175
+ "You are operating inside clisbot.",
64176
+ 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"
64177
+ ];
64178
+ if (messageToolMode) {
64179
+ const replyCommand = buildReplyCommand({
64180
+ command: getClisbotPromptCommand(),
64181
+ identity: params.identity
64182
+ });
64183
+ 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 ? [
64184
+ "- keep progress updates short and meaningful",
64185
+ "- do not send progress updates for trivial internal steps"
64186
+ ] : []);
64162
64187
  }
64163
- return trimmed.toLowerCase().replaceAll(/[^a-z0-9_-]+/g, "-").replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "") || "default";
64188
+ if (params.protectedControlMutationRule) {
64189
+ lines.push("", params.protectedControlMutationRule);
64190
+ }
64191
+ return lines.join(`
64192
+ `);
64164
64193
  }
64165
- function normalizeAccountId2(value) {
64194
+ function renderPromptTimestamp() {
64195
+ const date = new Date;
64196
+ const formatter = new Intl.DateTimeFormat("en-CA", {
64197
+ year: "numeric",
64198
+ month: "2-digit",
64199
+ day: "2-digit",
64200
+ hour: "2-digit",
64201
+ minute: "2-digit",
64202
+ second: "2-digit",
64203
+ hour12: false,
64204
+ timeZoneName: "shortOffset"
64205
+ });
64206
+ return formatter.format(date).replace(",", "");
64207
+ }
64208
+ function renderIdentitySummary(identity) {
64209
+ const segments = [renderConversationSummary(identity)];
64210
+ const sender = renderSenderSummary(identity);
64211
+ if (sender) {
64212
+ segments.push(sender);
64213
+ }
64214
+ return segments.join(" | ");
64215
+ }
64216
+ function renderConversationSummary(identity) {
64217
+ if (identity.platform === "slack") {
64218
+ const scopeLabel = identity.conversationKind === "dm" ? "Slack direct message" : identity.conversationKind === "group" ? "Slack group" : "Slack channel";
64219
+ const segments = [scopeLabel];
64220
+ const channel = renderLabeledTarget(identity.channelName, identity.channelId, "#");
64221
+ if (channel) {
64222
+ segments.push(channel);
64223
+ }
64224
+ if (identity.threadTs) {
64225
+ segments.push(`thread ${identity.threadTs}`);
64226
+ }
64227
+ return segments.join(" ");
64228
+ }
64229
+ if (identity.conversationKind === "dm") {
64230
+ return ["Telegram direct message", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
64231
+ }
64232
+ if (identity.conversationKind === "topic") {
64233
+ const topic = renderNamedValue("topic", identity.topicName, identity.topicId);
64234
+ const group = renderNamedValue("in group", identity.chatName, identity.chatId);
64235
+ return [topic, group].filter(Boolean).join(" ");
64236
+ }
64237
+ return ["Telegram group", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
64238
+ }
64239
+ function renderSenderSummary(identity) {
64240
+ const sender = renderLabeledTarget(identity.senderName, identity.senderId);
64241
+ return sender ? `sender ${sender}` : "";
64242
+ }
64243
+ function renderLabeledTarget(name, id, namePrefix = "") {
64244
+ const normalizedName = name?.trim();
64245
+ const normalizedId = id?.trim();
64246
+ if (normalizedName && normalizedId) {
64247
+ return `${namePrefix}${normalizedName} (${normalizedId})`;
64248
+ }
64249
+ if (normalizedName) {
64250
+ return `${namePrefix}${normalizedName}`;
64251
+ }
64252
+ return normalizedId ?? "";
64253
+ }
64254
+ function renderNamedValue(label, name, id) {
64255
+ const value = renderLabeledTarget(name, id);
64256
+ return value ? `${label} ${value}` : "";
64257
+ }
64258
+ function buildReplyCommand(params) {
64259
+ const lines = [`${params.command} message send \\`];
64260
+ if (params.identity.platform === "slack") {
64261
+ lines.push(" --channel slack \\");
64262
+ lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
64263
+ if (params.identity.threadTs) {
64264
+ lines.push(` --thread-id ${params.identity.threadTs} \\`);
64265
+ }
64266
+ lines.push(" --final \\");
64267
+ lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
64268
+ lines.push("<short progress update>");
64269
+ lines.push("__CLISBOT_MESSAGE__");
64270
+ lines.push(')" \\');
64271
+ lines.push(" [--media /absolute/path/to/file]");
64272
+ return lines.join(`
64273
+ `);
64274
+ }
64275
+ lines.push(" --channel telegram \\");
64276
+ lines.push(` --target ${params.identity.chatId ?? ""} \\`);
64277
+ if (params.identity.topicId) {
64278
+ lines.push(` --thread-id ${params.identity.topicId} \\`);
64279
+ }
64280
+ lines.push(" --final \\");
64281
+ lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
64282
+ lines.push("<short progress update>");
64283
+ lines.push("__CLISBOT_MESSAGE__");
64284
+ lines.push(')" \\');
64285
+ lines.push(" [--media /absolute/path/to/file]");
64286
+ return lines.join(`
64287
+ `);
64288
+ }
64289
+
64290
+ // src/agents/session-key.ts
64291
+ var DEFAULT_MAIN_KEY = "main";
64292
+ var DEFAULT_ACCOUNT_ID = "default";
64293
+ function normalizeToken(value) {
64294
+ return (value ?? "").trim().toLowerCase();
64295
+ }
64296
+ function normalizeMainKey(value) {
64297
+ return normalizeToken(value) || DEFAULT_MAIN_KEY;
64298
+ }
64299
+ function normalizeAgentId(value) {
64300
+ const trimmed = (value ?? "").trim();
64301
+ if (!trimmed) {
64302
+ return "default";
64303
+ }
64304
+ return trimmed.toLowerCase().replaceAll(/[^a-z0-9_-]+/g, "-").replaceAll(/-+/g, "-").replaceAll(/^-|-$/g, "") || "default";
64305
+ }
64306
+ function normalizeAccountId2(value) {
64166
64307
  return normalizeToken(value) || DEFAULT_ACCOUNT_ID;
64167
64308
  }
64168
64309
  function buildAgentMainSessionKey(params) {
@@ -64417,7 +64558,7 @@ class AgentJobQueue {
64417
64558
  }
64418
64559
 
64419
64560
  // src/agents/runner-session.ts
64420
- import { dirname as dirname11 } from "node:path";
64561
+ import { dirname as dirname12 } from "node:path";
64421
64562
 
64422
64563
  // src/shared/transcript-normalization.ts
64423
64564
  import { stripVTControlCharacters } from "node:util";
@@ -64981,7 +65122,11 @@ ${body}` : queueNote;
64981
65122
  _Timed out waiting for more output._` : "_Timed out waiting for visible output._";
64982
65123
  }
64983
65124
  if (params.status === "detached") {
64984
- const note = params.note ?? "This session is still running. Use `/transcript` anytime to check it.";
65125
+ const note = resolveDetachedInteractionNote({
65126
+ baseNote: params.note,
65127
+ allowTranscriptInspection: params.allowTranscriptInspection,
65128
+ transcriptCommand: "`/transcript`"
65129
+ });
64985
65130
  return body ? `${body}
64986
65131
 
64987
65132
  _${note}_` : `_${note}_`;
@@ -65010,7 +65155,11 @@ ${body}` : queueNote;
65010
65155
  Timed out waiting for more output.` : "Timed out waiting for visible output.";
65011
65156
  }
65012
65157
  if (params.status === "detached") {
65013
- const note = params.note ?? "This session is still running. Use /transcript anytime to check it.";
65158
+ const note = resolveDetachedInteractionNote({
65159
+ baseNote: params.note,
65160
+ allowTranscriptInspection: params.allowTranscriptInspection,
65161
+ transcriptCommand: "/transcript"
65162
+ });
65014
65163
  return body ? `${body}
65015
65164
 
65016
65165
  ${note}` : note;
@@ -65046,6 +65195,16 @@ ${body}
65046
65195
  }
65047
65196
  var renderSlackSnapshot = renderSlackTranscript;
65048
65197
  var renderChannelSnapshot = renderSlackSnapshot;
65198
+ function resolveDetachedInteractionNote(params) {
65199
+ const note = params.baseNote ?? "This session is still running. Use `/attach`, `/watch every 30s`, or `/stop` to manage it.";
65200
+ if (!params.allowTranscriptInspection) {
65201
+ return note;
65202
+ }
65203
+ if (note.includes("/transcript")) {
65204
+ return note;
65205
+ }
65206
+ return `${note} You can also use ${params.transcriptCommand} to inspect the current session snapshot.`;
65207
+ }
65049
65208
  // src/control/latency-debug.ts
65050
65209
  function isLatencyDebugEnabled() {
65051
65210
  return process.env.CLISBOT_DEBUG_LATENCY === "1";
@@ -65396,24 +65555,6 @@ function escapeRegExp(raw) {
65396
65555
  // src/agents/runner-session.ts
65397
65556
  var TMUX_MISSING_SESSION_PATTERN = /(?:can't find session:|no server running on )/i;
65398
65557
  var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
65399
- function shellQuote2(value) {
65400
- if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) {
65401
- return value;
65402
- }
65403
- return `'${value.replaceAll("'", `'"'"'`)}'`;
65404
- }
65405
- function buildCommandString(command, args) {
65406
- return [command, ...args].map(shellQuote2).join(" ");
65407
- }
65408
- function buildRunnerLaunchCommand(command, args) {
65409
- const wrapperDir = getClisbotWrapperDir();
65410
- const wrapperPath = getClisbotWrapperPath();
65411
- const exports = [
65412
- `export PATH=${shellQuote2(wrapperDir)}:"$PATH"`,
65413
- `export CLISBOT_BIN=${shellQuote2(wrapperPath)}`
65414
- ];
65415
- return `${exports.join("; ")}; exec ${buildCommandString(command, args)}`;
65416
- }
65417
65558
  function summarizeSnapshot(snapshot) {
65418
65559
  const compact = snapshot.split(`
65419
65560
  `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
@@ -65439,8 +65580,17 @@ class RunnerSessionService {
65439
65580
  this.sessionState = sessionState;
65440
65581
  this.resolveTarget = resolveTarget;
65441
65582
  }
65442
- mapSessionError(error, sessionName, action) {
65583
+ async mapSessionError(error, sessionName, action, lastSnapshot = "") {
65443
65584
  if (isMissingTmuxSessionError(error)) {
65585
+ const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, sessionName);
65586
+ console.error("runner session disappeared", {
65587
+ sessionName,
65588
+ action,
65589
+ exitCode: exitRecord?.exitCode,
65590
+ exitedAt: exitRecord?.exitedAt,
65591
+ runnerCommand: exitRecord?.command,
65592
+ lastVisiblePane: lastSnapshot ? summarizeSnapshot(lastSnapshot).trim() : undefined
65593
+ });
65444
65594
  return new Error(`Runner session "${sessionName}" disappeared ${action}.`);
65445
65595
  }
65446
65596
  return error instanceof Error ? error : new Error(String(error));
@@ -65559,7 +65709,8 @@ class RunnerSessionService {
65559
65709
  };
65560
65710
  logLatencyDebug("ensure-session-ready-start", timingContext);
65561
65711
  await ensureDir2(resolved.workspacePath);
65562
- await ensureDir2(dirname11(this.loadedConfig.raw.tmux.socketPath));
65712
+ await ensureDir2(dirname12(this.loadedConfig.raw.tmux.socketPath));
65713
+ await ensureRunnerExitRecordDir(this.loadedConfig.stateDir, resolved.sessionName);
65563
65714
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
65564
65715
  const serverRunning = await this.tmux.isServerRunning();
65565
65716
  if (serverRunning && await this.tmux.hasSession(resolved.sessionName)) {
@@ -65567,9 +65718,10 @@ class RunnerSessionService {
65567
65718
  hasStoredSessionId: Boolean(existing?.sessionId)
65568
65719
  });
65569
65720
  try {
65721
+ await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
65570
65722
  await this.syncSessionIdentity(resolved);
65571
65723
  } catch (error) {
65572
- throw this.mapSessionError(error, resolved.sessionName, "during startup");
65724
+ throw await this.mapSessionError(error, resolved.sessionName, "during startup");
65573
65725
  }
65574
65726
  logLatencyDebug("ensure-session-ready-complete", timingContext, {
65575
65727
  startupDelayMs: 0,
@@ -65586,7 +65738,15 @@ class RunnerSessionService {
65586
65738
  sessionId: startupSessionId || undefined,
65587
65739
  resume: resumingExistingSession
65588
65740
  });
65589
- const command = buildRunnerLaunchCommand(runnerLaunch.command, runnerLaunch.args);
65741
+ await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
65742
+ const command = buildRunnerLaunchCommand({
65743
+ command: runnerLaunch.command,
65744
+ args: runnerLaunch.args,
65745
+ wrapperDir: getClisbotWrapperDir(),
65746
+ wrapperPath: getClisbotWrapperPath(),
65747
+ sessionName: resolved.sessionName,
65748
+ stateDir: this.loadedConfig.stateDir
65749
+ });
65590
65750
  try {
65591
65751
  await this.tmux.newSession({
65592
65752
  sessionName: resolved.sessionName,
@@ -65640,7 +65800,7 @@ class RunnerSessionService {
65640
65800
  allowFreshRetry: options.allowFreshRetry
65641
65801
  });
65642
65802
  } catch (error) {
65643
- throw this.mapSessionError(error, resolved.sessionName, "during startup");
65803
+ throw await this.mapSessionError(error, resolved.sessionName, "during startup");
65644
65804
  }
65645
65805
  logLatencyDebug("ensure-session-ready-complete", timingContext, {
65646
65806
  startupDelayMs: resolved.runner.startupDelayMs,
@@ -65709,14 +65869,14 @@ class RunnerSessionService {
65709
65869
  } catch (error) {
65710
65870
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
65711
65871
  if (options.allowFreshRetryBeforePrompt === false || !existing?.sessionId || !isMissingTmuxSessionError(error)) {
65712
- throw this.mapSessionError(error, resolved.sessionName, "before prompt submission");
65872
+ throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65713
65873
  }
65714
65874
  const retried = await this.retryFreshStartWithClearedSessionId(target, resolved, {
65715
65875
  allowRetry: true,
65716
65876
  nextAllowFreshRetry: false
65717
65877
  });
65718
65878
  if (!retried) {
65719
- throw this.mapSessionError(error, resolved.sessionName, "before prompt submission");
65879
+ throw await this.mapSessionError(error, resolved.sessionName, "before prompt submission", resolved.sessionName ? await this.captureSessionSnapshot(resolved).catch(() => "") : "");
65720
65880
  }
65721
65881
  resolved = retried;
65722
65882
  return {
@@ -65835,8 +65995,8 @@ class RunnerSessionService {
65835
65995
  workspacePath: resolved.workspacePath
65836
65996
  };
65837
65997
  }
65838
- mapRunError(error, sessionName) {
65839
- return this.mapSessionError(error, sessionName, "while the prompt was running");
65998
+ async mapRunError(error, sessionName, lastSnapshot = "") {
65999
+ return await this.mapSessionError(error, sessionName, "while the prompt was running", lastSnapshot);
65840
66000
  }
65841
66001
  }
65842
66002
 
@@ -66348,7 +66508,7 @@ class ActiveRunManager {
66348
66508
  }
66349
66509
  });
66350
66510
  } catch (error) {
66351
- await this.failActiveRun(sessionKey, this.runnerSessions.mapRunError(error, run.resolved.sessionName));
66511
+ await this.failActiveRun(sessionKey, await this.runnerSessions.mapRunError(error, run.resolved.sessionName, run.latestUpdate.fullSnapshot));
66352
66512
  }
66353
66513
  })();
66354
66514
  }
@@ -66454,8 +66614,8 @@ class AgentService {
66454
66614
  sessionKey: this.loadedConfig.raw.session.mainKey
66455
66615
  });
66456
66616
  }
66457
- async recordConversationReply(target, kind = "reply") {
66458
- return this.sessionState.recordConversationReply(this.resolveTarget(target), kind);
66617
+ async recordConversationReply(target, kind = "reply", source = "channel") {
66618
+ return this.sessionState.recordConversationReply(this.resolveTarget(target), kind, source);
66459
66619
  }
66460
66620
  async runShellCommand(target, command) {
66461
66621
  return this.queue.enqueue(`${target.sessionKey}:bash`, async () => this.runnerSessions.runShellCommand(target, command)).result;
@@ -66512,10 +66672,13 @@ class AgentService {
66512
66672
  updatedAt: Date.now(),
66513
66673
  nextRunAt: Date.now(),
66514
66674
  promptText: params.promptText,
66675
+ canonicalPromptText: params.canonicalPromptText,
66676
+ protectedControlMutationRule: params.protectedControlMutationRule,
66515
66677
  promptSummary: params.promptSummary,
66516
66678
  promptSource: params.promptSource,
66517
66679
  createdBy: params.createdBy,
66518
- force: params.force
66680
+ force: params.force,
66681
+ surfaceBinding: params.surfaceBinding
66519
66682
  };
66520
66683
  const resolved = this.resolveTarget(params.target);
66521
66684
  await this.sessionState.setIntervalLoop(resolved, loop);
@@ -66553,6 +66716,8 @@ class AgentService {
66553
66716
  updatedAt: Date.now(),
66554
66717
  nextRunAt,
66555
66718
  promptText: params.promptText,
66719
+ canonicalPromptText: params.canonicalPromptText,
66720
+ protectedControlMutationRule: params.protectedControlMutationRule,
66556
66721
  promptSummary: params.promptSummary,
66557
66722
  promptSource: params.promptSource,
66558
66723
  createdBy: params.createdBy,
@@ -66562,7 +66727,8 @@ class AgentService {
66562
66727
  hour: params.hour,
66563
66728
  minute: params.minute,
66564
66729
  timezone: params.timezone,
66565
- force: false
66730
+ force: false,
66731
+ surfaceBinding: params.surfaceBinding
66566
66732
  };
66567
66733
  const resolved = this.resolveTarget(params.target);
66568
66734
  await this.sessionState.setIntervalLoop(resolved, loop);
@@ -66718,7 +66884,8 @@ class AgentService {
66718
66884
  this.dropManagedIntervalLoop(loopId);
66719
66885
  return;
66720
66886
  }
66721
- const { result } = this.enqueuePrompt(managed.target, nextLoopState.promptText, {
66887
+ const promptText = this.buildManagedLoopPrompt(managed.target.agentId, nextLoopState);
66888
+ const { result } = this.enqueuePrompt(managed.target, promptText, {
66722
66889
  observerId: `loop:${loopId}:${attemptedRuns}`,
66723
66890
  onUpdate: async () => {
66724
66891
  return;
@@ -66790,6 +66957,52 @@ class AgentService {
66790
66957
  }
66791
66958
  return nowMs + loop.intervalMs;
66792
66959
  }
66960
+ buildManagedLoopPrompt(agentId, loop) {
66961
+ if (!loop.canonicalPromptText || !loop.surfaceBinding) {
66962
+ return loop.promptText;
66963
+ }
66964
+ const identity = this.buildLoopChannelIdentity(loop.surfaceBinding);
66965
+ const channelConfig = identity.platform === "slack" ? this.loadedConfig.raw.channels.slack : this.loadedConfig.raw.channels.telegram;
66966
+ const { responseMode, streaming } = this.resolveLoopSurfaceModes(identity);
66967
+ return buildAgentPromptText({
66968
+ text: loop.canonicalPromptText,
66969
+ identity,
66970
+ config: channelConfig.agentPrompt,
66971
+ cliTool: getAgentEntry(this.loadedConfig, agentId)?.cliTool,
66972
+ responseMode,
66973
+ streaming,
66974
+ protectedControlMutationRule: loop.protectedControlMutationRule
66975
+ });
66976
+ }
66977
+ buildLoopChannelIdentity(binding) {
66978
+ return {
66979
+ platform: binding.platform,
66980
+ conversationKind: binding.conversationKind,
66981
+ channelId: binding.channelId,
66982
+ chatId: binding.chatId,
66983
+ threadTs: binding.threadTs,
66984
+ topicId: binding.topicId
66985
+ };
66986
+ }
66987
+ resolveLoopSurfaceModes(identity) {
66988
+ const channelConfig = identity.platform === "slack" ? this.loadedConfig.raw.channels.slack : this.loadedConfig.raw.channels.telegram;
66989
+ let responseMode = channelConfig.responseMode;
66990
+ let streaming = channelConfig.streaming;
66991
+ if (identity.conversationKind === "dm") {
66992
+ responseMode = channelConfig.directMessages.responseMode ?? responseMode;
66993
+ streaming = channelConfig.directMessages.streaming ?? streaming;
66994
+ }
66995
+ try {
66996
+ responseMode = resolveConfiguredSurfaceModeTarget(this.loadedConfig.raw, "responseMode", buildConfiguredTargetFromIdentity(identity)).get() ?? responseMode;
66997
+ } catch {}
66998
+ try {
66999
+ streaming = resolveConfiguredSurfaceModeTarget(this.loadedConfig.raw, "streaming", buildConfiguredTargetFromIdentity(identity)).get() ?? streaming;
67000
+ } catch {}
67001
+ return {
67002
+ responseMode,
67003
+ streaming
67004
+ };
67005
+ }
66793
67006
  }
66794
67007
 
66795
67008
  // src/config/channel-accounts.ts
@@ -67112,6 +67325,37 @@ function parseAgentCommand(text, options = {}) {
67112
67325
  action: "status"
67113
67326
  };
67114
67327
  }
67328
+ if (lowered === "streaming") {
67329
+ const action = withoutSlash.slice(command.length).trim().toLowerCase();
67330
+ if (!action || action === "status") {
67331
+ return {
67332
+ type: "control",
67333
+ name: "streaming",
67334
+ action: "status"
67335
+ };
67336
+ }
67337
+ if (action === "on") {
67338
+ return {
67339
+ type: "control",
67340
+ name: "streaming",
67341
+ action: "on",
67342
+ streaming: "all"
67343
+ };
67344
+ }
67345
+ if (action === "off" || action === "latest" || action === "all") {
67346
+ return {
67347
+ type: "control",
67348
+ name: "streaming",
67349
+ action,
67350
+ streaming: action
67351
+ };
67352
+ }
67353
+ return {
67354
+ type: "control",
67355
+ name: "streaming",
67356
+ action: "status"
67357
+ };
67358
+ }
67115
67359
  if (lowered === "additionalmessagemode") {
67116
67360
  const action = withoutSlash.slice(command.length).trim().toLowerCase();
67117
67361
  if (!action || action === "status") {
@@ -67240,7 +67484,7 @@ function renderAgentControlSlashHelp() {
67240
67484
  "- `/status`: show the current route status and operator setup commands",
67241
67485
  "- `/help`: show available control slash commands",
67242
67486
  "- `/whoami`: show the current platform, route, and sender identity details",
67243
- "- `/transcript`: show the current conversation session transcript when the route enables sensitive commands",
67487
+ "- `/transcript`: show the current conversation session transcript when the route verbose policy allows it",
67244
67488
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
67245
67489
  "- `/detach`: stop live updates for this thread while still allowing final settlement here",
67246
67490
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
@@ -67251,6 +67495,11 @@ function renderAgentControlSlashHelp() {
67251
67495
  "- `/followup mention-only`: require explicit mention for each later turn",
67252
67496
  "- `/followup pause`: stop passive follow-up until the next explicit mention",
67253
67497
  "- `/followup resume`: clear the runtime override and restore config defaults",
67498
+ "- `/streaming status`: show the configured streaming mode for this surface",
67499
+ "- `/streaming on`: enable streaming for this surface using the current `all` preview behavior",
67500
+ "- `/streaming off`: disable surface streaming previews for this surface",
67501
+ "- `/streaming latest`: prefer the latest preview shape for this surface",
67502
+ "- `/streaming all`: retain the full current preview shape for this surface",
67254
67503
  "- `/responsemode status`: show the configured response mode for this surface",
67255
67504
  "- `/responsemode capture-pane`: settle replies from captured pane output for this surface",
67256
67505
  "- `/responsemode message-tool`: expect the agent to reply through `clisbot message send` for this surface",
@@ -67262,8 +67511,8 @@ function renderAgentControlSlashHelp() {
67262
67511
  "- `/queue-list`: show queued messages that have not started yet",
67263
67512
  "- `/queue-clear`: clear queued messages that have not started yet",
67264
67513
  ...renderLoopHelpLines(),
67265
- "- `/bash` followed by a shell command: requires `privilegeCommands.enabled: true` on the current route",
67266
- "- shortcut prefixes such as `!` run bash when the route allows privilege commands",
67514
+ "- `/bash` followed by a shell command: requires `shellExecute` on the resolved agent role",
67515
+ "- shortcut prefixes such as `!` run bash only when the resolved agent role allows `shellExecute`",
67267
67516
  "",
67268
67517
  "Other slash commands are forwarded to the agent unchanged."
67269
67518
  ].join(`
@@ -67300,6 +67549,7 @@ function buildRenderedMessageState(params) {
67300
67549
  queuePosition: params.queuePosition,
67301
67550
  maxChars: params.maxChars,
67302
67551
  note: params.note,
67552
+ allowTranscriptInspection: params.allowTranscriptInspection,
67303
67553
  responsePolicy: params.responsePolicy
67304
67554
  }),
67305
67555
  body
@@ -67329,35 +67579,45 @@ function formatChannelFollowUpStatus(params) {
67329
67579
  `);
67330
67580
  }
67331
67581
 
67332
- // src/channels/privilege-commands.ts
67333
- function resolvePrivilegeCommands(rootConfig, override) {
67582
+ // src/channels/streaming-config.ts
67583
+ function getEditableConfigPath7() {
67584
+ return process.env.CLISBOT_CONFIG_PATH;
67585
+ }
67586
+ async function getConversationStreaming(params) {
67587
+ const { config } = await readEditableConfig(getEditableConfigPath7());
67588
+ const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67334
67589
  return {
67335
- enabled: override?.enabled ?? rootConfig.enabled,
67336
- allowUsers: override?.allowUsers ?? rootConfig.allowUsers
67590
+ label: target.label,
67591
+ streaming: target.get()
67337
67592
  };
67338
67593
  }
67339
- function canUsePrivilegeCommands(params) {
67340
- if (!params.config.enabled) {
67341
- return false;
67342
- }
67343
- if (!params.config.allowUsers.length) {
67344
- return true;
67345
- }
67346
- const normalizedUserId = params.userId?.trim() ?? "";
67347
- if (!normalizedUserId) {
67348
- return false;
67349
- }
67350
- return params.config.allowUsers.includes(normalizedUserId);
67594
+ async function setConversationStreaming(params) {
67595
+ const { config, configPath } = await readEditableConfig(getEditableConfigPath7());
67596
+ const target = resolveConfiguredSurfaceModeTarget(config, "streaming", buildConfiguredTargetFromIdentity(params.identity));
67597
+ target.set(params.streaming);
67598
+ await writeEditableConfig(configPath, config);
67599
+ return {
67600
+ configPath,
67601
+ label: target.label,
67602
+ streaming: params.streaming
67603
+ };
67351
67604
  }
67352
67605
 
67353
67606
  // src/channels/interaction-processing.ts
67354
- import { join as join7 } from "node:path";
67355
- function renderSensitiveCommandDisabledMessage(identity) {
67607
+ import { join as join8 } from "node:path";
67608
+ var MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS = 3000;
67609
+ var MESSAGE_TOOL_FINAL_GRACE_POLL_MS = 100;
67610
+ function renderSensitiveCommandDisabledMessage() {
67356
67611
  return [
67357
- "Privilege commands are not allowed for this route or user.",
67358
- "Enable `privilegeCommands.enabled` on the route to allow transcript and bash commands. Use `privilegeCommands.allowUsers` to restrict access to specific user ids.",
67359
- "",
67360
- ...renderPrivilegeCommandHelpLines(identity)
67612
+ "Shell execution is not allowed for your current role on this agent.",
67613
+ "Ask an app or agent admin to grant `shellExecute` if this surface should allow `/bash`."
67614
+ ].join(`
67615
+ `);
67616
+ }
67617
+ function renderTranscriptDisabledMessage() {
67618
+ return [
67619
+ "Transcript inspection is disabled for this route.",
67620
+ 'Set `verbose: "minimal"` on the route or channel to allow `/transcript`.'
67361
67621
  ].join(`
67362
67622
  `);
67363
67623
  }
@@ -67385,7 +67645,7 @@ function renderWhoAmIMessage(params) {
67385
67645
  if (params.identity.topicId) {
67386
67646
  lines.push(`topicId: \`${params.identity.topicId}\``);
67387
67647
  }
67388
- lines.push(`privilegeCommands.enabled: \`${params.route.privilegeCommands.enabled}\``, `privilegeCommands.allowUsers: \`${params.route.privilegeCommands.allowUsers.join(", ") || "(all users on route)"}\``);
67648
+ lines.push(`principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
67389
67649
  return lines.join(`
67390
67650
  `);
67391
67651
  }
@@ -67413,7 +67673,7 @@ function renderRouteStatusMessage(params) {
67413
67673
  if (params.identity.topicId) {
67414
67674
  lines.push(`topicId: \`${params.identity.topicId}\``);
67415
67675
  }
67416
- lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `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}\``, `privilegeCommands.enabled: \`${params.route.privilegeCommands.enabled}\``, `privilegeCommands.allowUsers: \`${params.route.privilegeCommands.allowUsers.join(", ") || "(all users on route)"}\``);
67676
+ lines.push(`streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `verbose: \`${params.route.verbose}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `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}\``);
67417
67677
  if (params.runtimeState.startedAt) {
67418
67678
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
67419
67679
  }
@@ -67427,11 +67687,13 @@ function renderRouteStatusMessage(params) {
67427
67687
  lines.push(`- \`${loop.id}\` ${renderLoopStatusSchedule(loop)} remaining \`${loop.remainingRuns}\` nextRunAt \`${new Date(loop.nextRunAt).toISOString()}\``);
67428
67688
  }
67429
67689
  }
67430
- lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/responsemode status`", "- `/additionalmessagemode status`", "- `/loop status`, `/loop cancel`, `/loop cancel <id>`", "- `/queue <message>`, `/steer <message>`", "- `/queue-list`, `/queue-clear`", "- `/transcript` and `/bash` require privilege commands");
67431
- lines.push("", ...renderPrivilegeCommandHelpLines(params.identity));
67690
+ lines.push("", "Useful commands:", "- `/help`", "- `/whoami`", "- `/status`", "- `/attach`, `/detach`, `/watch every 30s`", "- `/followup status`", "- `/streaming status`", "- `/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`");
67432
67691
  return lines.join(`
67433
67692
  `);
67434
67693
  }
67694
+ function allowTranscriptInspectionForRoute(route) {
67695
+ return route.verbose === "minimal";
67696
+ }
67435
67697
  function renderResponseModeStatusMessage(params) {
67436
67698
  const lines = [
67437
67699
  "clisbot response mode",
@@ -67446,6 +67708,18 @@ function renderResponseModeStatusMessage(params) {
67446
67708
  return lines.join(`
67447
67709
  `);
67448
67710
  }
67711
+ function renderStreamingStatusMessage(params) {
67712
+ const lines = [
67713
+ `clisbot streaming mode: \`${params.route.streaming}\``
67714
+ ];
67715
+ if (params.persisted) {
67716
+ lines.push("");
67717
+ lines.push(`config.target: \`${params.persisted.label}\``);
67718
+ }
67719
+ lines.push("", "Available values:", "- `off`: do not show live surface preview updates", "- `on`: slash-command shorthand that persists as `all`", "- `all`: keep streaming enabled with the current full preview behavior", "- `latest`: keep streaming enabled; current runtime behavior still matches `all` until preview shaping is refined");
67720
+ return lines.join(`
67721
+ `);
67722
+ }
67449
67723
  function renderAdditionalMessageModeStatusMessage(params) {
67450
67724
  const lines = [
67451
67725
  "clisbot additional message mode",
@@ -67471,11 +67745,17 @@ function buildChannelObserverId(identity) {
67471
67745
  identity.topicId ?? ""
67472
67746
  ].join(":");
67473
67747
  }
67474
- function buildSteeringMessage(text) {
67748
+ function buildSteeringMessage(text, protectedControlMutationRule) {
67749
+ const systemLines = [
67750
+ "A new user message arrived while you were still working.",
67751
+ "Adjust your current work if needed and continue."
67752
+ ];
67753
+ if (protectedControlMutationRule) {
67754
+ systemLines.push("", protectedControlMutationRule);
67755
+ }
67475
67756
  return [
67476
67757
  "<system>",
67477
- "A new user message arrived while you were still working.",
67478
- "Adjust your current work if needed and continue.",
67758
+ ...systemLines,
67479
67759
  "</system>",
67480
67760
  "",
67481
67761
  "<user>",
@@ -67614,7 +67894,7 @@ async function resolveLoopPromptText(params) {
67614
67894
  };
67615
67895
  }
67616
67896
  const workspacePath = params.agentService.getWorkspacePath(params.sessionTarget);
67617
- const loopPromptPath = join7(workspacePath, "LOOP.md");
67897
+ const loopPromptPath = join8(workspacePath, "LOOP.md");
67618
67898
  if (!await fileExists(loopPromptPath)) {
67619
67899
  throw new Error(`No loop prompt was provided and LOOP.md was not found in \`${workspacePath}\`. Create LOOP.md there if you want maintenance loops.`);
67620
67900
  }
@@ -67627,29 +67907,93 @@ async function resolveLoopPromptText(params) {
67627
67907
  maintenancePrompt: true
67628
67908
  };
67629
67909
  }
67910
+ function buildLoopSurfaceBinding(identity) {
67911
+ return {
67912
+ platform: identity.platform,
67913
+ conversationKind: identity.conversationKind,
67914
+ channelId: identity.channelId,
67915
+ chatId: identity.chatId,
67916
+ threadTs: identity.threadTs,
67917
+ topicId: identity.topicId
67918
+ };
67919
+ }
67630
67920
  async function executePromptDelivery(params) {
67631
67921
  let responseChunks = [];
67632
67922
  let renderedState;
67633
67923
  let renderChain = Promise.resolve();
67634
67924
  let replyRecorded = false;
67925
+ let finalReplyRecorded = false;
67635
67926
  let loggedFirstRunningUpdate = false;
67636
- const channelManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67637
- async function recordReplyIfNeeded() {
67927
+ let activePreviewStartedAt;
67928
+ let lastFrozenPreviewText;
67929
+ const paneManagedDelivery = params.route.responseMode === "capture-pane" || params.forceQueuedDelivery === true;
67930
+ const messageToolPreview = params.route.responseMode === "message-tool" && params.forceQueuedDelivery !== true && params.route.streaming !== "off";
67931
+ const previewEnabled = params.route.streaming !== "off" && (paneManagedDelivery || messageToolPreview);
67932
+ async function recordVisibleReply(kind = "reply", source = "channel") {
67933
+ if (kind === "final") {
67934
+ if (finalReplyRecorded) {
67935
+ return;
67936
+ }
67937
+ await params.agentService.recordConversationReply(params.sessionTarget, "final", source);
67938
+ finalReplyRecorded = true;
67939
+ replyRecorded = true;
67940
+ return;
67941
+ }
67638
67942
  if (replyRecorded) {
67639
67943
  return;
67640
67944
  }
67641
- await params.agentService.recordConversationReply(params.sessionTarget);
67945
+ await params.agentService.recordConversationReply(params.sessionTarget, "reply", source);
67642
67946
  replyRecorded = true;
67643
67947
  }
67644
67948
  async function renderResponseText(nextText) {
67645
67949
  if (!responseChunks.length) {
67646
67950
  responseChunks = await params.postText(nextText);
67647
- if (responseChunks.length > 0) {
67648
- await recordReplyIfNeeded();
67649
- }
67650
- return;
67951
+ return responseChunks.length > 0;
67651
67952
  }
67652
67953
  responseChunks = await params.reconcileText(responseChunks, nextText);
67954
+ return false;
67955
+ }
67956
+ async function clearResponseText() {
67957
+ if (!responseChunks.length) {
67958
+ return;
67959
+ }
67960
+ responseChunks = await params.reconcileText(responseChunks, "");
67961
+ renderedState = undefined;
67962
+ activePreviewStartedAt = undefined;
67963
+ }
67964
+ function resetPreviewBoundary() {
67965
+ lastFrozenPreviewText = renderedState?.text ?? lastFrozenPreviewText;
67966
+ responseChunks = [];
67967
+ renderedState = undefined;
67968
+ activePreviewStartedAt = undefined;
67969
+ }
67970
+ async function getMessageToolRuntimeSignals() {
67971
+ if (params.route.responseMode !== "message-tool" || params.forceQueuedDelivery === true) {
67972
+ return {
67973
+ lastMessageToolReplyAt: undefined,
67974
+ messageToolFinalReplyAt: undefined
67975
+ };
67976
+ }
67977
+ const runtime = await params.agentService.getSessionRuntime?.(params.sessionTarget);
67978
+ return {
67979
+ lastMessageToolReplyAt: runtime?.lastMessageToolReplyAt,
67980
+ messageToolFinalReplyAt: runtime?.messageToolFinalReplyAt
67981
+ };
67982
+ }
67983
+ async function waitForMessageToolFinalReply() {
67984
+ const deadline = Date.now() + MESSAGE_TOOL_FINAL_GRACE_WINDOW_MS;
67985
+ while (true) {
67986
+ const signals = await getMessageToolRuntimeSignals();
67987
+ const toolFinalSeen = typeof signals.messageToolFinalReplyAt === "number" && Number.isFinite(signals.messageToolFinalReplyAt);
67988
+ if (toolFinalSeen) {
67989
+ return true;
67990
+ }
67991
+ const remainingMs = deadline - Date.now();
67992
+ if (remainingMs <= 0) {
67993
+ return false;
67994
+ }
67995
+ await sleep(Math.min(MESSAGE_TOOL_FINAL_GRACE_POLL_MS, remainingMs));
67996
+ }
67653
67997
  }
67654
67998
  logLatencyDebug("channel-enqueue-start", params.timingContext, {
67655
67999
  agentId: params.route.agentId,
@@ -67660,7 +68004,7 @@ async function executePromptDelivery(params) {
67660
68004
  observerId: params.observerId,
67661
68005
  timingContext: params.timingContext,
67662
68006
  onUpdate: async (update) => {
67663
- if (!channelManagedDelivery) {
68007
+ if (!paneManagedDelivery && !messageToolPreview) {
67664
68008
  return;
67665
68009
  }
67666
68010
  if (params.route.streaming === "off" && update.status === "running") {
@@ -67674,6 +68018,15 @@ async function executePromptDelivery(params) {
67674
68018
  });
67675
68019
  }
67676
68020
  await (renderChain = renderChain.then(async () => {
68021
+ const signals = await getMessageToolRuntimeSignals();
68022
+ if (messageToolPreview && typeof activePreviewStartedAt === "number" && typeof signals.messageToolFinalReplyAt === "number" && signals.messageToolFinalReplyAt >= activePreviewStartedAt) {
68023
+ lastFrozenPreviewText = renderedState?.text ?? lastFrozenPreviewText;
68024
+ activePreviewStartedAt = undefined;
68025
+ return;
68026
+ }
68027
+ if (messageToolPreview && typeof activePreviewStartedAt === "number" && typeof signals.lastMessageToolReplyAt === "number" && signals.lastMessageToolReplyAt >= activePreviewStartedAt) {
68028
+ resetPreviewBoundary();
68029
+ }
67677
68030
  const nextState2 = buildRenderedMessageState({
67678
68031
  platform: params.identity.platform,
67679
68032
  status: update.status,
@@ -67681,21 +68034,26 @@ async function executePromptDelivery(params) {
67681
68034
  queuePosition: positionAhead,
67682
68035
  maxChars: Number.POSITIVE_INFINITY,
67683
68036
  note: update.note,
68037
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
67684
68038
  previousState: renderedState,
67685
68039
  responsePolicy: params.route.response
67686
68040
  });
67687
68041
  if (renderedState?.text === nextState2.text) {
67688
68042
  return;
67689
68043
  }
67690
- if (!responseChunks.length) {
68044
+ if (!renderedState && lastFrozenPreviewText === nextState2.text) {
67691
68045
  return;
67692
68046
  }
67693
- await renderResponseText(nextState2.text);
68047
+ const postedNew2 = await renderResponseText(nextState2.text);
68048
+ if (postedNew2) {
68049
+ await recordVisibleReply("reply", "channel");
68050
+ activePreviewStartedAt = Date.now();
68051
+ }
67694
68052
  renderedState = nextState2;
67695
68053
  }));
67696
68054
  }
67697
68055
  });
67698
- if (channelManagedDelivery && params.route.streaming !== "off") {
68056
+ if (previewEnabled) {
67699
68057
  const placeholderText = renderPlatformInteraction({
67700
68058
  platform: params.identity.platform,
67701
68059
  status: positionAhead > 0 ? "queued" : "running",
@@ -67704,13 +68062,16 @@ async function executePromptDelivery(params) {
67704
68062
  maxChars: Number.POSITIVE_INFINITY,
67705
68063
  note: positionAhead > 0 ? "Waiting for the agent queue to clear." : "Working..."
67706
68064
  });
67707
- responseChunks = await params.postText(placeholderText);
67708
- await recordReplyIfNeeded();
68065
+ const postedNew2 = await renderResponseText(placeholderText);
68066
+ if (postedNew2) {
68067
+ await recordVisibleReply("reply", "channel");
68068
+ activePreviewStartedAt = Date.now();
68069
+ }
67709
68070
  renderedState = {
67710
68071
  text: placeholderText,
67711
68072
  body: ""
67712
68073
  };
67713
- } else if (channelManagedDelivery && positionAhead > 0) {
68074
+ } else if (paneManagedDelivery && positionAhead > 0 && params.route.streaming !== "off") {
67714
68075
  const queuedText = renderPlatformInteraction({
67715
68076
  platform: params.identity.platform,
67716
68077
  status: "queued",
@@ -67719,8 +68080,10 @@ async function executePromptDelivery(params) {
67719
68080
  maxChars: Number.POSITIVE_INFINITY,
67720
68081
  note: "Waiting for the agent queue to clear."
67721
68082
  });
67722
- responseChunks = await params.postText(queuedText);
67723
- await recordReplyIfNeeded();
68083
+ const postedNew2 = await renderResponseText(queuedText);
68084
+ if (postedNew2) {
68085
+ await recordVisibleReply("reply", "channel");
68086
+ }
67724
68087
  renderedState = {
67725
68088
  text: queuedText,
67726
68089
  body: ""
@@ -67728,55 +68091,86 @@ async function executePromptDelivery(params) {
67728
68091
  }
67729
68092
  const finalResult = await result;
67730
68093
  await renderChain;
67731
- if (!channelManagedDelivery) {
67732
- if (finalResult.status !== "error") {
67733
- return;
67734
- }
67735
- await params.postText(renderPlatformInteraction({
67736
- platform: params.identity.platform,
67737
- status: finalResult.status,
67738
- content: finalResult.note ?? finalResult.snapshot,
67739
- maxChars: Number.POSITIVE_INFINITY,
67740
- note: finalResult.note,
67741
- responsePolicy: "final"
67742
- }));
67743
- await recordReplyIfNeeded();
67744
- return;
67745
- }
67746
68094
  const nextState = buildRenderedMessageState({
67747
68095
  platform: params.identity.platform,
67748
68096
  status: finalResult.status,
67749
68097
  snapshot: finalResult.snapshot,
67750
68098
  maxChars: Number.POSITIVE_INFINITY,
67751
68099
  note: finalResult.note,
68100
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
67752
68101
  previousState: renderedState,
67753
68102
  responsePolicy: params.route.response
67754
68103
  });
67755
- if (params.route.streaming === "off") {
67756
- await params.postText(renderPlatformInteraction({
68104
+ if (paneManagedDelivery) {
68105
+ if (params.route.streaming === "off") {
68106
+ const postedNew3 = await renderResponseText(renderPlatformInteraction({
68107
+ platform: params.identity.platform,
68108
+ status: finalResult.status,
68109
+ content: nextState.body,
68110
+ maxChars: Number.POSITIVE_INFINITY,
68111
+ note: finalResult.note,
68112
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68113
+ responsePolicy: params.route.response
68114
+ }));
68115
+ if (postedNew3 || finalResult.status === "completed") {
68116
+ await recordVisibleReply(finalResult.status === "completed" ? "final" : "reply", "channel");
68117
+ }
68118
+ return;
68119
+ }
68120
+ const postedNew2 = await renderResponseText(nextState.text);
68121
+ if (postedNew2) {
68122
+ await recordVisibleReply("reply", "channel");
68123
+ }
68124
+ if (finalResult.status === "completed") {
68125
+ await recordVisibleReply("final", "channel");
68126
+ }
68127
+ return;
68128
+ }
68129
+ const toolFinalSeen = finalResult.status === "completed" ? await waitForMessageToolFinalReply() : false;
68130
+ if (finalResult.status === "completed" && toolFinalSeen) {
68131
+ if (params.route.response === "final") {
68132
+ await clearResponseText();
68133
+ }
68134
+ return;
68135
+ }
68136
+ if (finalResult.status === "completed") {
68137
+ return;
68138
+ }
68139
+ if (params.route.streaming === "off" || responseChunks.length === 0) {
68140
+ const postedNew2 = await renderResponseText(renderPlatformInteraction({
67757
68141
  platform: params.identity.platform,
67758
68142
  status: finalResult.status,
67759
68143
  content: nextState.body,
67760
68144
  maxChars: Number.POSITIVE_INFINITY,
67761
68145
  note: finalResult.note,
67762
- responsePolicy: "final"
68146
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68147
+ responsePolicy: params.route.response
67763
68148
  }));
67764
- await recordReplyIfNeeded();
68149
+ if (postedNew2) {
68150
+ await recordVisibleReply("reply", "channel");
68151
+ }
67765
68152
  return;
67766
68153
  }
67767
- await renderResponseText(nextState.text);
68154
+ const postedNew = await renderResponseText(nextState.text);
68155
+ if (postedNew) {
68156
+ await recordVisibleReply("reply", "channel");
68157
+ }
67768
68158
  } catch (error) {
67769
68159
  if (error instanceof ClearedQueuedTaskError) {
67770
68160
  return;
67771
68161
  }
67772
68162
  if (error instanceof ActiveRunInProgressError) {
67773
- const activeText = error.update.note ?? String(error);
68163
+ const activeText = error.update.status === "detached" ? resolveDetachedInteractionNote({
68164
+ baseNote: error.update.note ?? String(error),
68165
+ allowTranscriptInspection: allowTranscriptInspectionForRoute(params.route),
68166
+ transcriptCommand: params.identity.platform === "telegram" ? "/transcript" : "`/transcript`"
68167
+ }) : error.update.note ?? String(error);
67774
68168
  if (params.route.streaming !== "off" && responseChunks.length > 0) {
67775
68169
  await params.reconcileText(responseChunks, activeText);
67776
68170
  } else {
67777
68171
  await params.postText(activeText);
67778
68172
  }
67779
- await params.agentService.recordConversationReply(params.sessionTarget);
68173
+ await recordVisibleReply("reply", "channel");
67780
68174
  return;
67781
68175
  }
67782
68176
  const errorText = renderPlatformInteraction({
@@ -67790,12 +68184,24 @@ async function executePromptDelivery(params) {
67790
68184
  } else {
67791
68185
  await params.postText(errorText);
67792
68186
  }
68187
+ await recordVisibleReply("reply", "channel");
67793
68188
  }
67794
68189
  }
67795
68190
  async function processChannelInteraction(params) {
68191
+ const interactionResult = {
68192
+ processingIndicatorLifecycle: "handler"
68193
+ };
67796
68194
  let responseChunks = [];
67797
68195
  let renderedState;
67798
68196
  const observerId = buildChannelObserverId(params.identity);
68197
+ const auth = params.auth ?? {
68198
+ principal: params.senderId ? `${params.identity.platform}:${params.senderId}` : undefined,
68199
+ appRole: "member",
68200
+ agentRole: "member",
68201
+ mayBypassPairing: false,
68202
+ mayManageProtectedResources: false,
68203
+ canUseShell: false
68204
+ };
67799
68205
  let replyRecorded = false;
67800
68206
  let renderChain = Promise.resolve();
67801
68207
  async function recordReplyIfNeeded() {
@@ -67852,14 +68258,12 @@ async function processChannelInteraction(params) {
67852
68258
  const sessionBusy = await (params.agentService.isAwaitingFollowUpRouting?.(params.sessionTarget) ?? params.agentService.isSessionBusy?.(params.sessionTarget) ?? false);
67853
68259
  const queueByMode = !explicitQueueMessage && params.route.additionalMessageMode === "queue" && sessionBusy;
67854
68260
  const forceQueuedDelivery = typeof explicitQueueMessage === "string" || queueByMode;
67855
- const isSensitiveCommand = slashCommand?.type === "bash" || slashCommand?.type === "control" && slashCommand.name === "transcript";
67856
- if (isSensitiveCommand && !canUsePrivilegeCommands({
67857
- config: params.route.privilegeCommands,
67858
- userId: params.senderId
67859
- })) {
67860
- await params.postText(renderSensitiveCommandDisabledMessage(params.identity));
68261
+ const delayedPromptText = explicitQueueMessage ? params.agentPromptBuilder ? params.agentPromptBuilder(explicitQueueMessage) : explicitQueueMessage : params.agentPromptText ?? params.text;
68262
+ const isSensitiveCommand = slashCommand?.type === "bash";
68263
+ if (isSensitiveCommand && !auth.canUseShell) {
68264
+ await params.postText(renderSensitiveCommandDisabledMessage());
67861
68265
  await params.agentService.recordConversationReply(params.sessionTarget);
67862
- return;
68266
+ return interactionResult;
67863
68267
  }
67864
68268
  if (slashCommand?.type === "control") {
67865
68269
  if (slashCommand.name === "start" || slashCommand.name === "status") {
@@ -67871,6 +68275,7 @@ async function processChannelInteraction(params) {
67871
68275
  await params.postText(renderRouteStatusMessage({
67872
68276
  identity: params.identity,
67873
68277
  route: params.route,
68278
+ auth,
67874
68279
  sessionTarget: params.sessionTarget,
67875
68280
  followUpState,
67876
68281
  runtimeState,
@@ -67880,23 +68285,29 @@ async function processChannelInteraction(params) {
67880
68285
  }
67881
68286
  }));
67882
68287
  await params.agentService.recordConversationReply(params.sessionTarget);
67883
- return;
68288
+ return interactionResult;
67884
68289
  }
67885
68290
  if (slashCommand.name === "help") {
67886
68291
  await params.postText(renderAgentControlSlashHelp());
67887
68292
  await params.agentService.recordConversationReply(params.sessionTarget);
67888
- return;
68293
+ return interactionResult;
67889
68294
  }
67890
68295
  if (slashCommand.name === "whoami") {
67891
68296
  await params.postText(renderWhoAmIMessage({
67892
68297
  identity: params.identity,
67893
68298
  route: params.route,
68299
+ auth,
67894
68300
  sessionTarget: params.sessionTarget
67895
68301
  }));
67896
68302
  await params.agentService.recordConversationReply(params.sessionTarget);
67897
- return;
68303
+ return interactionResult;
67898
68304
  }
67899
68305
  if (slashCommand.name === "transcript") {
68306
+ if (params.route.verbose === "off") {
68307
+ await params.postText(renderTranscriptDisabledMessage());
68308
+ await params.agentService.recordConversationReply(params.sessionTarget);
68309
+ return interactionResult;
68310
+ }
67900
68311
  const transcript = await params.agentService.captureTranscript(params.sessionTarget);
67901
68312
  await params.postText(renderChannelSnapshot({
67902
68313
  agentId: transcript.agentId,
@@ -67907,20 +68318,20 @@ async function processChannelInteraction(params) {
67907
68318
  maxChars: params.maxChars,
67908
68319
  note: "transcript command"
67909
68320
  }));
67910
- return;
68321
+ return interactionResult;
67911
68322
  }
67912
68323
  if (slashCommand.name === "attach") {
67913
68324
  const observation = await params.agentService.observeRun(params.sessionTarget, buildRunObserver({
67914
68325
  mode: "live"
67915
68326
  }));
67916
68327
  await applyRunUpdate(observation.update);
67917
- return;
68328
+ return interactionResult;
67918
68329
  }
67919
68330
  if (slashCommand.name === "detach") {
67920
68331
  const detached = await params.agentService.detachRunObserver(params.sessionTarget, observerId);
67921
68332
  await params.postText(detached.detached ? "Detached this thread from live updates. clisbot will still post the final settlement here when the run completes." : "This thread was not attached to an active run.");
67922
68333
  await params.agentService.recordConversationReply(params.sessionTarget);
67923
- return;
68334
+ return interactionResult;
67924
68335
  }
67925
68336
  if (slashCommand.name === "watch") {
67926
68337
  const observation = await params.agentService.observeRun(params.sessionTarget, buildRunObserver({
@@ -67929,19 +68340,19 @@ async function processChannelInteraction(params) {
67929
68340
  durationMs: slashCommand.durationMs
67930
68341
  }));
67931
68342
  await applyRunUpdate(observation.update);
67932
- return;
68343
+ return interactionResult;
67933
68344
  }
67934
68345
  if (slashCommand.name === "stop") {
67935
68346
  const stopped = await params.agentService.interruptSession(params.sessionTarget);
67936
68347
  await params.postText(stopped.interrupted ? `Interrupted agent \`${stopped.agentId}\` session \`${stopped.sessionName}\`.` : `Agent \`${stopped.agentId}\` session \`${stopped.sessionName}\` was not running.`);
67937
68348
  await params.agentService.recordConversationReply(params.sessionTarget);
67938
- return;
68349
+ return interactionResult;
67939
68350
  }
67940
68351
  if (slashCommand.name === "nudge") {
67941
68352
  const nudged = await params.agentService.nudgeSession(params.sessionTarget);
67942
68353
  await params.postText(nudged.nudged ? `Sent one extra Enter to agent \`${nudged.agentId}\` session \`${nudged.sessionName}\`.` : `No active or resumable session to nudge for agent \`${nudged.agentId}\`.`);
67943
68354
  await params.agentService.recordConversationReply(params.sessionTarget);
67944
- return;
68355
+ return interactionResult;
67945
68356
  }
67946
68357
  if (slashCommand.name === "followup") {
67947
68358
  if (slashCommand.action === "status") {
@@ -67960,7 +68371,32 @@ async function processChannelInteraction(params) {
67960
68371
  await params.postText(slashCommand.mode === "paused" ? "Follow-up paused for this conversation until the next explicit mention." : `Follow-up mode set to \`${slashCommand.mode}\` for this conversation.`);
67961
68372
  }
67962
68373
  await params.agentService.recordConversationReply(params.sessionTarget);
67963
- return;
68374
+ return interactionResult;
68375
+ }
68376
+ if (slashCommand.name === "streaming") {
68377
+ if (slashCommand.action === "status") {
68378
+ const persisted = await getConversationStreaming({
68379
+ identity: params.identity
68380
+ });
68381
+ await params.postText(renderStreamingStatusMessage({
68382
+ route: params.route,
68383
+ persisted
68384
+ }));
68385
+ } else if (slashCommand.streaming) {
68386
+ const persisted = await setConversationStreaming({
68387
+ identity: params.identity,
68388
+ streaming: slashCommand.streaming
68389
+ });
68390
+ await params.postText([
68391
+ `Updated streaming mode for \`${persisted.label}\`.`,
68392
+ `config.streaming: \`${persisted.streaming}\``,
68393
+ `config: \`${persisted.configPath}\``,
68394
+ slashCommand.action === "on" ? "`/streaming on` persists as `all` until `latest` and `all` diverge in runtime behavior." : "If config reload is enabled, the new mode should apply automatically shortly."
68395
+ ].join(`
68396
+ `));
68397
+ }
68398
+ await params.agentService.recordConversationReply(params.sessionTarget);
68399
+ return interactionResult;
67964
68400
  }
67965
68401
  if (slashCommand.name === "responsemode") {
67966
68402
  if (slashCommand.action === "status") {
@@ -67985,7 +68421,7 @@ async function processChannelInteraction(params) {
67985
68421
  `));
67986
68422
  }
67987
68423
  await params.agentService.recordConversationReply(params.sessionTarget);
67988
- return;
68424
+ return interactionResult;
67989
68425
  }
67990
68426
  if (slashCommand.name === "additionalmessagemode") {
67991
68427
  if (slashCommand.action === "status") {
@@ -68010,19 +68446,19 @@ async function processChannelInteraction(params) {
68010
68446
  `));
68011
68447
  }
68012
68448
  await params.agentService.recordConversationReply(params.sessionTarget);
68013
- return;
68449
+ return interactionResult;
68014
68450
  }
68015
68451
  if (slashCommand.name === "queue-list") {
68016
68452
  const queuedItems = params.agentService.listQueuedPrompts?.(params.sessionTarget) ?? [];
68017
68453
  await params.postText(renderQueuedMessagesList(queuedItems));
68018
68454
  await params.agentService.recordConversationReply(params.sessionTarget);
68019
- return;
68455
+ return interactionResult;
68020
68456
  }
68021
68457
  if (slashCommand.name === "queue-clear") {
68022
68458
  const clearedCount = params.agentService.clearQueuedPrompts?.(params.sessionTarget) ?? 0;
68023
68459
  await params.postText(clearedCount > 0 ? `Cleared ${clearedCount} queued message${clearedCount === 1 ? "" : "s"}.` : "Queue was already empty.");
68024
68460
  await params.agentService.recordConversationReply(params.sessionTarget);
68025
- return;
68461
+ return interactionResult;
68026
68462
  }
68027
68463
  }
68028
68464
  if (slashCommand?.type === "loop-control") {
@@ -68034,7 +68470,7 @@ async function processChannelInteraction(params) {
68034
68470
  globalLoopCount: params.agentService.getActiveIntervalLoopCount?.() ?? 0
68035
68471
  }));
68036
68472
  await params.agentService.recordConversationReply(params.sessionTarget);
68037
- return;
68473
+ return interactionResult;
68038
68474
  }
68039
68475
  const sessionLoops = params.agentService.listIntervalLoops?.({
68040
68476
  sessionKey: params.sessionTarget.sessionKey
@@ -68043,31 +68479,31 @@ async function processChannelInteraction(params) {
68043
68479
  const cancelled2 = await params.agentService.cancelAllIntervalLoops();
68044
68480
  await params.postText(cancelled2 > 0 ? `Cancelled ${cancelled2} active loop${cancelled2 === 1 ? "" : "s"} across the whole app.` : "No active loops to cancel across the whole app.");
68045
68481
  await params.agentService.recordConversationReply(params.sessionTarget);
68046
- return;
68482
+ return interactionResult;
68047
68483
  }
68048
68484
  if (slashCommand.all) {
68049
68485
  const cancelled2 = await params.agentService.cancelIntervalLoopsForSession(params.sessionTarget);
68050
68486
  await params.postText(cancelled2 > 0 ? `Cancelled ${cancelled2} active loop${cancelled2 === 1 ? "" : "s"} for this session.` : "No active loops to cancel for this session.");
68051
68487
  await params.agentService.recordConversationReply(params.sessionTarget);
68052
- return;
68488
+ return interactionResult;
68053
68489
  }
68054
68490
  const targetLoopId = slashCommand.loopId || (sessionLoops.length === 1 ? sessionLoops[0]?.id : undefined);
68055
68491
  if (!targetLoopId) {
68056
68492
  await params.postText(sessionLoops.length === 0 ? "No active loops to cancel for this session." : "Multiple active loops exist for this session. Use `/loop cancel <id>` or `/loop cancel --all`.");
68057
68493
  await params.agentService.recordConversationReply(params.sessionTarget);
68058
- return;
68494
+ return interactionResult;
68059
68495
  }
68060
68496
  const cancelled = await params.agentService.cancelIntervalLoop(targetLoopId);
68061
68497
  await params.postText(cancelled ? `Cancelled loop \`${targetLoopId}\`.` : `No active loop found with id \`${targetLoopId}\`.`);
68062
68498
  await params.agentService.recordConversationReply(params.sessionTarget);
68063
- return;
68499
+ return interactionResult;
68064
68500
  }
68065
68501
  if (slashCommand?.type === "loop-error") {
68066
68502
  await params.postText(`${slashCommand.message}
68067
68503
 
68068
68504
  ${renderLoopUsage()}`);
68069
68505
  await params.agentService.recordConversationReply(params.sessionTarget);
68070
- return;
68506
+ return interactionResult;
68071
68507
  }
68072
68508
  if (slashCommand?.type === "loop") {
68073
68509
  const loopConfig = params.agentService.getLoopConfig();
@@ -68078,7 +68514,7 @@ ${renderLoopUsage()}`);
68078
68514
 
68079
68515
  ${renderLoopUsage()}`);
68080
68516
  await params.agentService.recordConversationReply(params.sessionTarget);
68081
- return;
68517
+ return interactionResult;
68082
68518
  }
68083
68519
  if (slashCommand.params.mode === "interval") {
68084
68520
  const intervalValidation = validateLoopInterval({
@@ -68090,7 +68526,7 @@ ${renderLoopUsage()}`);
68090
68526
 
68091
68527
  ${renderLoopUsage()}`);
68092
68528
  await params.agentService.recordConversationReply(params.sessionTarget);
68093
- return;
68529
+ return interactionResult;
68094
68530
  }
68095
68531
  }
68096
68532
  let resolvedLoopPrompt;
@@ -68103,7 +68539,7 @@ ${renderLoopUsage()}`);
68103
68539
  } catch (error) {
68104
68540
  await params.postText(String(error));
68105
68541
  await params.agentService.recordConversationReply(params.sessionTarget);
68106
- return;
68542
+ return interactionResult;
68107
68543
  }
68108
68544
  const buildLoopPromptText = (text) => params.agentPromptBuilder ? params.agentPromptBuilder(text) : text;
68109
68545
  if (slashCommand.params.mode === "times") {
@@ -68126,7 +68562,7 @@ ${renderLoopUsage()}`);
68126
68562
  observerId: `${observerId}:loop:${index + 1}`
68127
68563
  });
68128
68564
  }
68129
- return;
68565
+ return interactionResult;
68130
68566
  }
68131
68567
  if (slashCommand.params.mode === "calendar") {
68132
68568
  const effectiveTimezone = resolveEffectiveLoopTimezone({
@@ -68136,8 +68572,10 @@ ${renderLoopUsage()}`);
68136
68572
  const createdLoop2 = await params.agentService.createCalendarLoop({
68137
68573
  target: params.sessionTarget,
68138
68574
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
68575
+ canonicalPromptText: resolvedLoopPrompt.text,
68139
68576
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
68140
68577
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
68578
+ surfaceBinding: buildLoopSurfaceBinding(params.identity),
68141
68579
  cadence: slashCommand.params.cadence,
68142
68580
  dayOfWeek: slashCommand.params.dayOfWeek,
68143
68581
  localTime: slashCommand.params.localTime,
@@ -68145,7 +68583,8 @@ ${renderLoopUsage()}`);
68145
68583
  minute: slashCommand.params.minute,
68146
68584
  timezone: effectiveTimezone,
68147
68585
  maxRuns: maxRunsPerLoop,
68148
- createdBy: params.senderId
68586
+ createdBy: params.senderId,
68587
+ protectedControlMutationRule: params.protectedControlMutationRule
68149
68588
  });
68150
68589
  await params.postText(renderLoopStartedMessage({
68151
68590
  mode: "calendar",
@@ -68165,17 +68604,20 @@ ${renderLoopUsage()}`);
68165
68604
  globalLoopCount: params.agentService.getActiveIntervalLoopCount()
68166
68605
  }));
68167
68606
  await params.agentService.recordConversationReply(params.sessionTarget);
68168
- return;
68607
+ return interactionResult;
68169
68608
  }
68170
68609
  const createdLoop = await params.agentService.createIntervalLoop({
68171
68610
  target: params.sessionTarget,
68172
68611
  promptText: buildLoopPromptText(resolvedLoopPrompt.text),
68612
+ canonicalPromptText: resolvedLoopPrompt.text,
68173
68613
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
68174
68614
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
68615
+ surfaceBinding: buildLoopSurfaceBinding(params.identity),
68175
68616
  intervalMs: effectiveIntervalMs,
68176
68617
  maxRuns: maxRunsPerLoop,
68177
68618
  createdBy: params.senderId,
68178
- force: slashCommand.params.force
68619
+ force: slashCommand.params.force,
68620
+ protectedControlMutationRule: params.protectedControlMutationRule
68179
68621
  });
68180
68622
  await params.postText(renderLoopStartedMessage({
68181
68623
  mode: "interval",
@@ -68193,55 +68635,59 @@ ${renderLoopUsage()}`);
68193
68635
  }).warning
68194
68636
  }));
68195
68637
  await params.agentService.recordConversationReply(params.sessionTarget);
68196
- return;
68638
+ return interactionResult;
68197
68639
  }
68198
68640
  if (slashCommand?.type === "bash") {
68199
68641
  if (!slashCommand.command.trim()) {
68200
68642
  await params.postText("Usage: `/bash <command>` or a configured bash shortcut such as `!<command>`");
68201
- return;
68643
+ return interactionResult;
68202
68644
  }
68203
- const result = await params.agentService.runShellCommand(params.sessionTarget, slashCommand.command);
68645
+ const shellResult = await params.agentService.runShellCommand(params.sessionTarget, slashCommand.command);
68204
68646
  const header = [
68205
- `Bash in \`${result.workspacePath}\``,
68206
- `command: \`${result.command}\``,
68207
- result.timedOut ? "exit: `124` timed out" : `exit: \`${result.exitCode}\``
68647
+ `Bash in \`${shellResult.workspacePath}\``,
68648
+ `command: \`${shellResult.command}\``,
68649
+ shellResult.timedOut ? "exit: `124` timed out" : `exit: \`${shellResult.exitCode}\``
68208
68650
  ].join(`
68209
68651
  `);
68210
- const body = result.output ? `
68652
+ const body = shellResult.output ? `
68211
68653
 
68212
68654
  \`\`\`text
68213
- ${escapeCodeFence(result.output)}
68655
+ ${escapeCodeFence(shellResult.output)}
68214
68656
  \`\`\`` : "\n\n`(no output)`";
68215
68657
  await params.postText(`${header}${body}`);
68216
68658
  await params.agentService.recordConversationReply(params.sessionTarget);
68217
- return;
68659
+ return interactionResult;
68218
68660
  }
68219
68661
  if (slashCommand?.type === "queue" && !explicitQueueMessage) {
68220
68662
  await params.postText("Usage: `/queue <message>` or `\\q <message>`");
68221
68663
  await params.agentService.recordConversationReply(params.sessionTarget);
68222
- return;
68664
+ return interactionResult;
68223
68665
  }
68224
68666
  if (slashCommand?.type === "steer" && !explicitSteerMessage) {
68225
68667
  await params.postText("Usage: `/steer <message>` or `\\s <message>`");
68226
68668
  await params.agentService.recordConversationReply(params.sessionTarget);
68227
- return;
68669
+ return interactionResult;
68228
68670
  }
68229
68671
  if (explicitSteerMessage) {
68230
68672
  const hasActiveRun = params.agentService.hasActiveRun?.(params.sessionTarget) ?? false;
68231
68673
  if (!hasActiveRun) {
68232
68674
  await params.postText("No active run to steer.");
68233
68675
  await params.agentService.recordConversationReply(params.sessionTarget);
68234
- return;
68676
+ return interactionResult;
68235
68677
  }
68236
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage));
68678
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(explicitSteerMessage, params.protectedControlMutationRule));
68237
68679
  await params.postText("Steered.");
68238
68680
  await params.agentService.recordConversationReply(params.sessionTarget);
68239
- return;
68681
+ return {
68682
+ processingIndicatorLifecycle: "active-run"
68683
+ };
68240
68684
  }
68241
68685
  if (!forceQueuedDelivery && params.route.additionalMessageMode === "steer") {
68242
68686
  if (sessionBusy) {
68243
- await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text));
68244
- return;
68687
+ await params.agentService.submitSessionInput(params.sessionTarget, buildSteeringMessage(params.text, params.protectedControlMutationRule));
68688
+ return {
68689
+ processingIndicatorLifecycle: "active-run"
68690
+ };
68245
68691
  }
68246
68692
  }
68247
68693
  await executePromptDelivery({
@@ -68250,13 +68696,14 @@ ${escapeCodeFence(result.output)}
68250
68696
  identity: params.identity,
68251
68697
  route: params.route,
68252
68698
  maxChars: params.maxChars,
68253
- promptText: forceQueuedDelivery ? explicitQueueMessage : params.agentPromptText ?? params.text,
68699
+ promptText: delayedPromptText,
68254
68700
  postText: params.postText,
68255
68701
  reconcileText: params.reconcileText,
68256
68702
  observerId,
68257
68703
  timingContext: params.timingContext,
68258
68704
  forceQueuedDelivery
68259
68705
  });
68706
+ return interactionResult;
68260
68707
  }
68261
68708
 
68262
68709
  // src/channels/pairing/access.ts
@@ -68309,132 +68756,88 @@ function isTelegramSenderAllowed(params) {
68309
68756
  return false;
68310
68757
  }
68311
68758
 
68312
- // src/channels/agent-prompt.ts
68313
- function buildAgentPromptText(params) {
68314
- if (!params.config.enabled) {
68315
- return params.text;
68759
+ // src/auth/resolve.ts
68760
+ function normalizePrincipal(principal) {
68761
+ const trimmed = principal.trim();
68762
+ if (!trimmed) {
68763
+ return "";
68316
68764
  }
68317
- const systemBlock = renderAgentPromptInstruction(params);
68318
- return `<system>
68319
- ${systemBlock}
68320
- </system>
68321
-
68322
- <user>
68323
- ${params.text}
68324
- </user>`;
68325
- }
68326
- function renderAgentPromptInstruction(params) {
68327
- const messageToolMode = (params.responseMode ?? "message-tool") === "message-tool";
68328
- const lines = [
68329
- `[${renderPromptTimestamp()}] ${renderIdentitySummary(params.identity)}`,
68330
- "",
68331
- "You are operating inside clisbot.",
68332
- messageToolMode ? "To send a user-visible progress update or final 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"
68333
- ];
68334
- if (messageToolMode) {
68335
- const replyCommand = buildReplyCommand({
68336
- command: getClisbotPromptCommand(),
68337
- identity: params.identity
68338
- });
68339
- lines.push(replyCommand, "When replying to the user:", "- put the user-facing message inside the --message body of that command", "- use that command to send progress updates and the final reply back to the conversation", `- send at most ${params.config.maxProgressMessages} progress updates`, params.config.requireFinalResponse ? "- send exactly 1 final user-facing response" : "- final response is optional", "- keep progress updates short and meaningful", "- do not send progress updates for trivial internal steps");
68765
+ const [platform, userId] = trimmed.split(":", 2);
68766
+ if (!platform || !userId) {
68767
+ return trimmed;
68340
68768
  }
68341
- return lines.join(`
68342
- `);
68769
+ if (platform === "slack") {
68770
+ return `slack:${userId.trim().toUpperCase()}`;
68771
+ }
68772
+ if (platform === "telegram") {
68773
+ return `telegram:${userId.trim()}`;
68774
+ }
68775
+ return `${platform}:${userId.trim()}`;
68343
68776
  }
68344
- function renderPromptTimestamp() {
68345
- const date = new Date;
68346
- const formatter = new Intl.DateTimeFormat("en-CA", {
68347
- year: "numeric",
68348
- month: "2-digit",
68349
- day: "2-digit",
68350
- hour: "2-digit",
68351
- minute: "2-digit",
68352
- second: "2-digit",
68353
- hour12: false,
68354
- timeZoneName: "shortOffset"
68355
- });
68356
- return formatter.format(date).replace(",", "");
68777
+ function normalizeRoleUsers(users) {
68778
+ return (users ?? []).map(normalizePrincipal).filter(Boolean);
68357
68779
  }
68358
- function renderIdentitySummary(identity) {
68359
- const segments = [renderConversationSummary(identity)];
68360
- const sender = renderSenderSummary(identity);
68361
- if (sender) {
68362
- segments.push(sender);
68780
+ function resolvePrincipal(identity) {
68781
+ const senderId = identity.senderId?.trim();
68782
+ if (!senderId) {
68783
+ return;
68363
68784
  }
68364
- return segments.join(" | ");
68365
- }
68366
- function renderConversationSummary(identity) {
68367
68785
  if (identity.platform === "slack") {
68368
- const scopeLabel = identity.conversationKind === "dm" ? "Slack direct message" : identity.conversationKind === "group" ? "Slack group" : "Slack channel";
68369
- const segments = [scopeLabel];
68370
- const channel = renderLabeledTarget(identity.channelName, identity.channelId, "#");
68371
- if (channel) {
68372
- segments.push(channel);
68373
- }
68374
- if (identity.threadTs) {
68375
- segments.push(`thread ${identity.threadTs}`);
68376
- }
68377
- return segments.join(" ");
68378
- }
68379
- if (identity.conversationKind === "dm") {
68380
- return ["Telegram direct message", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
68381
- }
68382
- if (identity.conversationKind === "topic") {
68383
- const topic = renderNamedValue("topic", identity.topicName, identity.topicId);
68384
- const group = renderNamedValue("in group", identity.chatName, identity.chatId);
68385
- return [topic, group].filter(Boolean).join(" ");
68786
+ return normalizePrincipal(`slack:${senderId}`);
68386
68787
  }
68387
- return ["Telegram group", renderLabeledTarget(identity.chatName, identity.chatId)].filter(Boolean).join(" ");
68388
- }
68389
- function renderSenderSummary(identity) {
68390
- const sender = renderLabeledTarget(identity.senderName, identity.senderId);
68391
- return sender ? `sender ${sender}` : "";
68788
+ return normalizePrincipal(`telegram:${senderId}`);
68392
68789
  }
68393
- function renderLabeledTarget(name, id, namePrefix = "") {
68394
- const normalizedName = name?.trim();
68395
- const normalizedId = id?.trim();
68396
- if (normalizedName && normalizedId) {
68397
- return `${namePrefix}${normalizedName} (${normalizedId})`;
68790
+ function findExplicitRole(roles, principal) {
68791
+ if (!principal || !roles) {
68792
+ return;
68398
68793
  }
68399
- if (normalizedName) {
68400
- return `${namePrefix}${normalizedName}`;
68794
+ for (const [roleName, roleDefinition] of Object.entries(roles)) {
68795
+ if (normalizeRoleUsers(roleDefinition.users).includes(principal)) {
68796
+ return roleName;
68797
+ }
68401
68798
  }
68402
- return normalizedId ?? "";
68403
- }
68404
- function renderNamedValue(label, name, id) {
68405
- const value = renderLabeledTarget(name, id);
68406
- return value ? `${label} ${value}` : "";
68799
+ return;
68407
68800
  }
68408
- function buildReplyCommand(params) {
68409
- const lines = [`${params.command} message send \\`];
68410
- if (params.identity.platform === "slack") {
68411
- lines.push(" --channel slack \\");
68412
- lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
68413
- if (params.identity.threadTs) {
68414
- lines.push(` --thread-id ${params.identity.threadTs} \\`);
68801
+ function getAgentAuth(config, agentId) {
68802
+ const defaults = config.agents.defaults.auth;
68803
+ const entry = config.agents.list.find((item) => item.id === agentId);
68804
+ const override = entry?.auth;
68805
+ return {
68806
+ defaultRole: override?.defaultRole ?? defaults.defaultRole,
68807
+ roles: {
68808
+ ...defaults.roles,
68809
+ ...override?.roles ?? {}
68415
68810
  }
68416
- lines.push(" --final \\");
68417
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
68418
- lines.push("<short progress update>");
68419
- lines.push("__CLISBOT_MESSAGE__");
68420
- lines.push(')" \\');
68421
- lines.push(" [--media /absolute/path/to/file]");
68422
- return lines.join(`
68423
- `);
68424
- }
68425
- lines.push(" --channel telegram \\");
68426
- lines.push(` --target ${params.identity.chatId ?? ""} \\`);
68427
- if (params.identity.topicId) {
68428
- lines.push(` --thread-id ${params.identity.topicId} \\`);
68811
+ };
68812
+ }
68813
+ function getAllowedPermissions(roles, role) {
68814
+ return new Set(roles?.[role]?.allow ?? []);
68815
+ }
68816
+ function hasAppPermission(config, appRole, permission) {
68817
+ if (appRole === "owner") {
68818
+ return true;
68429
68819
  }
68430
- lines.push(" --final \\");
68431
- lines.push(' --message "$(cat <<\\__CLISBOT_MESSAGE__');
68432
- lines.push("<short progress update>");
68433
- lines.push("__CLISBOT_MESSAGE__");
68434
- lines.push(')" \\');
68435
- lines.push(" [--media /absolute/path/to/file]");
68436
- return lines.join(`
68437
- `);
68820
+ return getAllowedPermissions(config.app.auth.roles, appRole).has(permission);
68821
+ }
68822
+ function resolveChannelAuth(params) {
68823
+ const principal = resolvePrincipal(params.identity);
68824
+ const appAuth = params.config.app.auth;
68825
+ const explicitAppRole = findExplicitRole(appAuth.roles, principal);
68826
+ const appRole = explicitAppRole ?? appAuth.defaultRole;
68827
+ const appAdminLike = appRole === "owner" || appRole === "admin";
68828
+ const agentAuth = getAgentAuth(params.config, params.agentId);
68829
+ const explicitAgentRole = findExplicitRole(agentAuth.roles, principal);
68830
+ const agentRole = explicitAgentRole ?? agentAuth.defaultRole;
68831
+ const agentPermissions = getAllowedPermissions(agentAuth.roles, agentRole);
68832
+ const mayManageProtectedResources = appAdminLike || hasAppPermission(params.config, appRole, "configManage") || hasAppPermission(params.config, appRole, "appAuthManage") || hasAppPermission(params.config, appRole, "agentAuthManage") || hasAppPermission(params.config, appRole, "promptGovernanceManage");
68833
+ return {
68834
+ principal,
68835
+ appRole,
68836
+ agentRole,
68837
+ mayBypassPairing: appAdminLike,
68838
+ mayManageProtectedResources,
68839
+ canUseShell: appAdminLike || agentPermissions.has("shellExecute")
68840
+ };
68438
68841
  }
68439
68842
 
68440
68843
  // src/channels/slack/session-routing.ts
@@ -68492,9 +68895,20 @@ function resolveSlackConversationTarget(params) {
68492
68895
  };
68493
68896
  }
68494
68897
 
68898
+ // src/channels/privilege-commands.ts
68899
+ function resolvePrivilegeCommands(rootConfig, override) {
68900
+ return {
68901
+ enabled: override?.enabled ?? rootConfig.enabled,
68902
+ allowUsers: override?.allowUsers ?? rootConfig.allowUsers
68903
+ };
68904
+ }
68905
+
68495
68906
  // src/channels/route-policy.ts
68496
68907
  function buildSharedChannelRoute(params) {
68497
- const privilegeCommands = resolvePrivilegeCommands(params.channelConfig.privilegeCommands, params.route?.privilegeCommands);
68908
+ const privilegeCommands = resolvePrivilegeCommands(params.channelConfig.privilegeCommands ?? {
68909
+ enabled: false,
68910
+ allowUsers: []
68911
+ }, params.route?.privilegeCommands);
68498
68912
  const agentId = params.route?.agentId ?? resolveTopLevelBoundAgentId(params.loadedConfig, {
68499
68913
  channel: params.channel,
68500
68914
  accountId: params.accountId
@@ -68516,6 +68930,7 @@ function buildSharedChannelRoute(params) {
68516
68930
  response: params.route?.response ?? params.channelConfig.response,
68517
68931
  responseMode: params.route?.responseMode ?? agentEntry?.responseMode ?? params.channelConfig.responseMode,
68518
68932
  additionalMessageMode: params.route?.additionalMessageMode ?? agentEntry?.additionalMessageMode ?? params.channelConfig.additionalMessageMode,
68933
+ verbose: params.route?.verbose ?? params.channelConfig.verbose,
68519
68934
  followUp: {
68520
68935
  mode: params.route?.followUp?.mode ?? params.channelConfig.followUp.mode,
68521
68936
  participationTtlMs: resolveConfigDurationMs({
@@ -68696,6 +69111,178 @@ async function clearSlackAssistantThreadStatus(client, target) {
68696
69111
  }
68697
69112
  }
68698
69113
 
69114
+ // src/channels/processing-indicator.ts
69115
+ function shouldResolveIndicatorWait(update) {
69116
+ return isTerminalRunStatus(update.status) || update.status === "detached";
69117
+ }
69118
+ async function waitForProcessingIndicatorLifecycle(params) {
69119
+ if (params.lifecycle !== "active-run") {
69120
+ return;
69121
+ }
69122
+ if (!params.agentService.hasActiveRun(params.sessionTarget)) {
69123
+ return;
69124
+ }
69125
+ let settled = false;
69126
+ const settle = () => {
69127
+ if (settled) {
69128
+ return;
69129
+ }
69130
+ settled = true;
69131
+ };
69132
+ try {
69133
+ await new Promise(async (resolve, reject) => {
69134
+ const resolveOnce = () => {
69135
+ if (settled) {
69136
+ return;
69137
+ }
69138
+ settled = true;
69139
+ resolve();
69140
+ };
69141
+ try {
69142
+ const observation = await params.agentService.observeRun(params.sessionTarget, {
69143
+ id: params.observerId,
69144
+ mode: "live",
69145
+ onUpdate: async (update) => {
69146
+ if (shouldResolveIndicatorWait(update)) {
69147
+ resolveOnce();
69148
+ }
69149
+ }
69150
+ });
69151
+ if (!observation.active || shouldResolveIndicatorWait(observation.update)) {
69152
+ resolveOnce();
69153
+ }
69154
+ } catch (error) {
69155
+ reject(error);
69156
+ }
69157
+ });
69158
+ } finally {
69159
+ settle();
69160
+ await params.agentService.detachRunObserver(params.sessionTarget, params.observerId).catch(() => {
69161
+ return;
69162
+ });
69163
+ }
69164
+ }
69165
+
69166
+ class ConversationProcessingIndicatorCoordinator {
69167
+ entries = new Map;
69168
+ async acquire(params) {
69169
+ let entry = this.entries.get(params.key);
69170
+ if (!entry) {
69171
+ entry = {
69172
+ activeRunHold: false,
69173
+ indicatorActive: false,
69174
+ key: params.key,
69175
+ operationChain: Promise.resolve(),
69176
+ refCount: 0
69177
+ };
69178
+ this.entries.set(params.key, entry);
69179
+ }
69180
+ entry.refCount += 1;
69181
+ await this.ensureIndicatorActive(entry, params.activate, params.onError);
69182
+ let released = false;
69183
+ return {
69184
+ setLifecycle: async (lifecycleParams) => {
69185
+ if (released || lifecycleParams.lifecycle !== "active-run" || entry.activeRunHold) {
69186
+ return;
69187
+ }
69188
+ entry.activeRunHold = true;
69189
+ entry.activeRunWait = waitForProcessingIndicatorLifecycle(lifecycleParams).catch((error) => {
69190
+ params.onError?.("active-run", error);
69191
+ }).finally(() => {
69192
+ entry.activeRunHold = false;
69193
+ entry.activeRunWait = undefined;
69194
+ this.maybeDeactivate(entry, params.onError);
69195
+ });
69196
+ },
69197
+ release: async () => {
69198
+ if (released) {
69199
+ return;
69200
+ }
69201
+ released = true;
69202
+ entry.refCount = Math.max(0, entry.refCount - 1);
69203
+ await this.maybeDeactivate(entry, params.onError);
69204
+ }
69205
+ };
69206
+ }
69207
+ async ensureIndicatorActive(entry, activate, onError) {
69208
+ entry.operationChain = entry.operationChain.then(async () => {
69209
+ if (entry.indicatorActive) {
69210
+ return;
69211
+ }
69212
+ try {
69213
+ const cleanup = await activate();
69214
+ entry.cleanup = typeof cleanup === "function" ? cleanup : undefined;
69215
+ entry.indicatorActive = true;
69216
+ } catch (error) {
69217
+ onError?.("activate", error);
69218
+ }
69219
+ });
69220
+ await entry.operationChain;
69221
+ }
69222
+ async maybeDeactivate(entry, onError) {
69223
+ if (entry.refCount > 0 || entry.activeRunHold) {
69224
+ return;
69225
+ }
69226
+ entry.operationChain = entry.operationChain.then(async () => {
69227
+ if (entry.refCount > 0 || entry.activeRunHold || !entry.indicatorActive) {
69228
+ return;
69229
+ }
69230
+ try {
69231
+ await entry.cleanup?.();
69232
+ } catch (error) {
69233
+ onError?.("deactivate", error);
69234
+ } finally {
69235
+ entry.cleanup = undefined;
69236
+ entry.indicatorActive = false;
69237
+ if (entry.refCount === 0 && !entry.activeRunHold) {
69238
+ this.entries.delete(entry.key);
69239
+ }
69240
+ }
69241
+ });
69242
+ await entry.operationChain;
69243
+ }
69244
+ }
69245
+
69246
+ // src/channels/slack/processing-decoration.ts
69247
+ async function activateSlackProcessingDecoration(params) {
69248
+ const [reactionResult, statusResult] = await Promise.allSettled([
69249
+ params.addReaction(),
69250
+ params.setStatus()
69251
+ ]);
69252
+ const reactionApplied = reactionResult.status === "fulfilled" ? reactionResult.value === true : false;
69253
+ const statusApplied = statusResult.status === "fulfilled" ? statusResult.value === true : false;
69254
+ if (reactionResult.status === "rejected") {
69255
+ params.onUnexpectedError?.("add-reaction", reactionResult.reason);
69256
+ }
69257
+ if (statusResult.status === "rejected") {
69258
+ params.onUnexpectedError?.("set-status", statusResult.reason);
69259
+ }
69260
+ if (!reactionApplied && !statusApplied) {
69261
+ if (reactionResult.status === "rejected") {
69262
+ throw reactionResult.reason;
69263
+ }
69264
+ if (statusResult.status === "rejected") {
69265
+ throw statusResult.reason;
69266
+ }
69267
+ }
69268
+ return async () => {
69269
+ if (reactionApplied) {
69270
+ try {
69271
+ await params.removeReaction();
69272
+ } catch (error) {
69273
+ params.onUnexpectedError?.("remove-reaction", error);
69274
+ }
69275
+ }
69276
+ if (statusApplied) {
69277
+ try {
69278
+ await params.clearStatus();
69279
+ } catch (error) {
69280
+ params.onUnexpectedError?.("clear-status", error);
69281
+ }
69282
+ }
69283
+ };
69284
+ }
69285
+
68699
69286
  // src/channels/slack/bolt-compat.ts
68700
69287
  var SlackBolt = __toESM(require_dist7(), 1);
68701
69288
  var { App } = SlackBolt;
@@ -68863,7 +69450,7 @@ async function downloadRemoteBuffer(params) {
68863
69450
 
68864
69451
  // src/agents/attachments/storage.ts
68865
69452
  import { access as access2 } from "node:fs/promises";
68866
- import { extname, join as join8 } from "node:path";
69453
+ import { extname, join as join9 } from "node:path";
68867
69454
  var CONTENT_TYPE_EXTENSION_MAP = {
68868
69455
  "application/json": ".json",
68869
69456
  "application/pdf": ".pdf",
@@ -68896,12 +69483,12 @@ function buildAttachmentFilename(params) {
68896
69483
  async function resolveUniquePath(directoryPath, fileName) {
68897
69484
  const extension = extname(fileName);
68898
69485
  const baseName = extension ? fileName.slice(0, -extension.length) : fileName;
68899
- let candidatePath = join8(directoryPath, fileName);
69486
+ let candidatePath = join9(directoryPath, fileName);
68900
69487
  let index = 2;
68901
69488
  while (true) {
68902
69489
  try {
68903
69490
  await access2(candidatePath);
68904
- candidatePath = join8(directoryPath, `${baseName}-${index}${extension}`);
69491
+ candidatePath = join9(directoryPath, `${baseName}-${index}${extension}`);
68905
69492
  index += 1;
68906
69493
  } catch {
68907
69494
  return candidatePath;
@@ -68909,7 +69496,7 @@ async function resolveUniquePath(directoryPath, fileName) {
68909
69496
  }
68910
69497
  }
68911
69498
  async function saveWorkspaceAttachment(params) {
68912
- const attachmentDir = join8(params.workspacePath, ".attachments", sanitizeSessionName(params.sessionKey), sanitizeSessionName(params.messageId));
69499
+ const attachmentDir = join9(params.workspacePath, ".attachments", sanitizeSessionName(params.sessionKey), sanitizeSessionName(params.messageId));
68913
69500
  await ensureDir2(attachmentDir);
68914
69501
  const fileName = buildAttachmentFilename({
68915
69502
  originalFilename: params.originalFilename,
@@ -69299,6 +69886,7 @@ class SlackSocketService {
69299
69886
  accountId;
69300
69887
  accountConfig;
69301
69888
  app;
69889
+ processingIndicators = new ConversationProcessingIndicatorCoordinator;
69302
69890
  botUserId = "";
69303
69891
  botLabel = "";
69304
69892
  teamId = "";
@@ -69442,12 +70030,22 @@ class SlackSocketService {
69442
70030
  if (params.conversationKind === "dm") {
69443
70031
  const directUserId = typeof event.user === "string" ? event.user.trim() : "";
69444
70032
  const dmConfig = this.loadedConfig.raw.channels.slack.directMessages;
70033
+ const auth2 = resolveChannelAuth({
70034
+ config: this.loadedConfig.raw,
70035
+ agentId: params.route.agentId,
70036
+ identity: {
70037
+ platform: "slack",
70038
+ conversationKind: params.conversationKind,
70039
+ senderId: directUserId || undefined,
70040
+ channelId
70041
+ }
70042
+ });
69445
70043
  if (!directUserId || dmConfig.policy === "disabled") {
69446
70044
  debugSlackEvent("drop-dm-disabled", { eventId, directUserId });
69447
70045
  await this.processedEventsStore.markCompleted(eventId);
69448
70046
  return;
69449
70047
  }
69450
- if (dmConfig.policy !== "open") {
70048
+ if (dmConfig.policy !== "open" && !auth2.mayBypassPairing) {
69451
70049
  const storedAllowFrom = await readChannelAllowFromStore("slack");
69452
70050
  const allowed = isSlackSenderAllowed({
69453
70051
  allowFrom: [...dmConfig.allowFrom, ...storedAllowFrom],
@@ -69559,18 +70157,27 @@ class SlackSocketService {
69559
70157
  };
69560
70158
  let responseChunks = [];
69561
70159
  const cliTool = getAgentEntry(this.loadedConfig, params.route.agentId)?.cliTool;
70160
+ const identity = {
70161
+ platform: "slack",
70162
+ conversationKind: params.conversationKind,
70163
+ senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
70164
+ channelId,
70165
+ threadTs
70166
+ };
70167
+ const auth = resolveChannelAuth({
70168
+ config: this.loadedConfig.raw,
70169
+ agentId: params.route.agentId,
70170
+ identity
70171
+ });
70172
+ const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
69562
70173
  const agentPromptText = buildAgentPromptText({
69563
70174
  text,
69564
- identity: {
69565
- platform: "slack",
69566
- conversationKind: params.conversationKind,
69567
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69568
- channelId,
69569
- threadTs
69570
- },
70175
+ identity,
69571
70176
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69572
70177
  cliTool,
69573
- responseMode: params.route.responseMode
70178
+ responseMode: params.route.responseMode,
70179
+ streaming: params.route.streaming,
70180
+ protectedControlMutationRule
69574
70181
  });
69575
70182
  const timingContext = {
69576
70183
  platform: "slack",
@@ -69586,40 +70193,46 @@ class SlackSocketService {
69586
70193
  accountId: this.accountId
69587
70194
  });
69588
70195
  const ackReactionTask = waitForBackgroundSlackTask(addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.ackReaction, reactionTarget));
69589
- const processingDecorationTask = waitForBackgroundSlackTask(Promise.all([
69590
- addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
69591
- setSlackAssistantThreadStatus(this.app.client, this.loadedConfig.raw.channels.slack.processingStatus, {
69592
- channel: channelId,
69593
- threadTs
69594
- })
69595
- ]));
70196
+ const processingLease = await this.processingIndicators.acquire({
70197
+ key: `slack:${this.accountId}:${channelId}:${threadTs}`,
70198
+ activate: async () => activateSlackProcessingDecoration({
70199
+ addReaction: () => addConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
70200
+ removeReaction: () => removeConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget),
70201
+ setStatus: () => setSlackAssistantThreadStatus(this.app.client, this.loadedConfig.raw.channels.slack.processingStatus, {
70202
+ channel: channelId,
70203
+ threadTs
70204
+ }),
70205
+ clearStatus: () => clearSlackAssistantThreadStatus(this.app.client, {
70206
+ channel: channelId,
70207
+ threadTs
70208
+ }),
70209
+ onUnexpectedError: (phase, error) => {
70210
+ console.error(`slack processing indicator ${phase} failed`, error);
70211
+ }
70212
+ }),
70213
+ onError: (phase, error) => {
70214
+ console.error(`slack processing indicator ${phase} failed`, error);
70215
+ }
70216
+ });
69596
70217
  try {
69597
- await processChannelInteraction({
70218
+ const interaction = await processChannelInteraction({
69598
70219
  agentService: this.agentService,
69599
70220
  sessionTarget,
69600
- identity: {
69601
- platform: "slack",
69602
- conversationKind: params.conversationKind,
69603
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69604
- channelId,
69605
- threadTs
69606
- },
70221
+ identity,
70222
+ auth,
69607
70223
  senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69608
70224
  text,
69609
70225
  agentPromptText,
69610
70226
  agentPromptBuilder: (nextText) => buildAgentPromptText({
69611
70227
  text: nextText,
69612
- identity: {
69613
- platform: "slack",
69614
- conversationKind: params.conversationKind,
69615
- senderId: typeof event.user === "string" ? event.user.trim().toUpperCase() : undefined,
69616
- channelId,
69617
- threadTs
69618
- },
70228
+ identity,
69619
70229
  config: this.loadedConfig.raw.channels.slack.agentPrompt,
69620
70230
  cliTool,
69621
- responseMode: params.route.responseMode
70231
+ responseMode: params.route.responseMode,
70232
+ streaming: params.route.streaming,
70233
+ protectedControlMutationRule
69622
70234
  }),
70235
+ protectedControlMutationRule,
69623
70236
  route: params.route,
69624
70237
  maxChars: this.getSlackMaxChars(params.route.agentId),
69625
70238
  timingContext,
@@ -69641,6 +70254,12 @@ class SlackSocketService {
69641
70254
  return responseChunks;
69642
70255
  }
69643
70256
  });
70257
+ await processingLease.setLifecycle({
70258
+ agentService: this.agentService,
70259
+ sessionTarget,
70260
+ observerId: `slack-processing:${channelId}:${threadTs}`,
70261
+ lifecycle: interaction.processingIndicatorLifecycle
70262
+ });
69644
70263
  await this.processedEventsStore.markCompleted(eventId);
69645
70264
  } catch (error) {
69646
70265
  console.error("slack handler error", error);
@@ -69648,12 +70267,7 @@ class SlackSocketService {
69648
70267
  return;
69649
70268
  } finally {
69650
70269
  await ackReactionTask;
69651
- await processingDecorationTask;
69652
- await removeConfiguredReaction(this.app.client, this.loadedConfig.raw.channels.slack.typingReaction, reactionTarget);
69653
- await clearSlackAssistantThreadStatus(this.app.client, {
69654
- channel: channelId,
69655
- threadTs
69656
- });
70270
+ await processingLease.release();
69657
70271
  }
69658
70272
  }
69659
70273
  registerEvents() {
@@ -70613,22 +71227,17 @@ function startTelegramTypingHeartbeat(params) {
70613
71227
  }
70614
71228
  };
70615
71229
  }
70616
- async function runWithTelegramTypingHeartbeat(params) {
71230
+ async function beginTelegramTypingHeartbeat(params) {
70617
71231
  try {
70618
71232
  await params.sendTyping();
70619
71233
  } catch (error) {
70620
71234
  logTelegramTypingError(params.onError, error);
70621
71235
  }
70622
- const stopHeartbeat = startTelegramTypingHeartbeat({
71236
+ return startTelegramTypingHeartbeat({
70623
71237
  sendTyping: params.sendTyping,
70624
71238
  intervalMs: params.intervalMs ?? TELEGRAM_TYPING_HEARTBEAT_MS,
70625
71239
  onError: params.onError
70626
71240
  });
70627
- try {
70628
- return await params.run();
70629
- } finally {
70630
- stopHeartbeat();
70631
- }
70632
71241
  }
70633
71242
 
70634
71243
  // src/channels/telegram/service.ts
@@ -70647,6 +71256,7 @@ var TELEGRAM_FULL_COMMANDS = [
70647
71256
  { command: "stop", description: "Interrupt current run" },
70648
71257
  { command: "nudge", description: "Send one extra Enter to the session" },
70649
71258
  { command: "followup", description: "Show or change follow-up mode" },
71259
+ { command: "streaming", description: "Show or change streaming mode" },
70650
71260
  { command: "responsemode", description: "Show or change response mode" },
70651
71261
  { command: "queue", description: "Queue a later message behind the active run" },
70652
71262
  { command: "steer", description: "Steer the active run immediately" },
@@ -70734,6 +71344,7 @@ class TelegramPollingService {
70734
71344
  loopPromise;
70735
71345
  activePollController;
70736
71346
  inFlightUpdates = new Set;
71347
+ processingIndicators = new ConversationProcessingIndicatorCoordinator;
70737
71348
  constructor(loadedConfig, agentService, processedEventsStore, activityStore, accountId = "default", accountConfig) {
70738
71349
  this.loadedConfig = loadedConfig;
70739
71350
  this.agentService = agentService;
@@ -70874,11 +71485,21 @@ class TelegramPollingService {
70874
71485
  const directMessages = this.loadedConfig.raw.channels.telegram.directMessages;
70875
71486
  const senderId = message.from?.id != null ? String(message.from.id) : "";
70876
71487
  const senderUsername = message.from?.username;
71488
+ const auth = resolveChannelAuth({
71489
+ config: this.loadedConfig.raw,
71490
+ agentId: routeInfo.route.agentId,
71491
+ identity: {
71492
+ platform: "telegram",
71493
+ conversationKind: routeInfo.conversationKind,
71494
+ senderId: senderId || undefined,
71495
+ chatId: String(message.chat.id)
71496
+ }
71497
+ });
70877
71498
  if (!senderId || directMessages.policy === "disabled") {
70878
71499
  await this.processedEventsStore.markCompleted(eventId);
70879
71500
  return;
70880
71501
  }
70881
- if (directMessages.policy !== "open") {
71502
+ if (directMessages.policy !== "open" && !auth.mayBypassPairing) {
70882
71503
  const storedAllowFrom = await readChannelAllowFromStore("telegram");
70883
71504
  const allowed = isTelegramSenderAllowed({
70884
71505
  allowFrom: [...directMessages.allowFrom, ...storedAllowFrom],
@@ -70968,12 +71589,20 @@ class TelegramPollingService {
70968
71589
  topicId: routeInfo.topicId != null ? String(routeInfo.topicId) : undefined
70969
71590
  };
70970
71591
  const cliTool = getAgentEntry(this.loadedConfig, routeInfo.route.agentId)?.cliTool;
71592
+ const auth = resolveChannelAuth({
71593
+ config: this.loadedConfig.raw,
71594
+ agentId: routeInfo.route.agentId,
71595
+ identity
71596
+ });
71597
+ const protectedControlMutationRule = auth.mayManageProtectedResources ? undefined : DEFAULT_PROTECTED_CONTROL_RULE;
70971
71598
  const agentPromptText = buildAgentPromptText({
70972
71599
  text,
70973
71600
  identity,
70974
71601
  config: this.loadedConfig.raw.channels.telegram.agentPrompt,
70975
71602
  cliTool,
70976
- responseMode: routeInfo.route.responseMode
71603
+ responseMode: routeInfo.route.responseMode,
71604
+ streaming: routeInfo.route.streaming,
71605
+ protectedControlMutationRule
70977
71606
  });
70978
71607
  const timingContext = {
70979
71608
  platform: "telegram",
@@ -70988,53 +71617,71 @@ class TelegramPollingService {
70988
71617
  responseMode: routeInfo.route.responseMode,
70989
71618
  accountId: this.accountId
70990
71619
  });
70991
- await runWithTelegramTypingHeartbeat({
70992
- sendTyping: () => this.sendTyping(message.chat.id, routeInfo.topicId),
70993
- onError: (error) => {
70994
- console.error("telegram typing failed", error);
70995
- },
70996
- run: async () => {
70997
- await processChannelInteraction({
70998
- agentService: this.agentService,
70999
- sessionTarget: routeInfo.sessionTarget,
71000
- identity,
71001
- senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
71002
- text,
71003
- agentPromptText,
71004
- agentPromptBuilder: (nextText) => buildAgentPromptText({
71005
- text: nextText,
71006
- identity,
71007
- config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71008
- cliTool,
71009
- responseMode: routeInfo.route.responseMode
71010
- }),
71011
- route: routeInfo.route,
71012
- maxChars: this.getTelegramMaxChars(routeInfo.route.agentId),
71013
- timingContext,
71014
- postText: async (nextText) => {
71015
- responseChunks = await postTelegramText({
71016
- token: this.accountConfig.botToken,
71017
- chatId: message.chat.id,
71018
- text: nextText,
71019
- topicId: routeInfo.topicId,
71020
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71021
- });
71022
- return responseChunks;
71023
- },
71024
- reconcileText: async (chunks, nextText) => {
71025
- responseChunks = await reconcileTelegramText({
71026
- token: this.accountConfig.botToken,
71027
- chatId: message.chat.id,
71028
- chunks,
71029
- text: nextText,
71030
- topicId: routeInfo.topicId,
71031
- omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71032
- });
71033
- return responseChunks;
71034
- }
71035
- });
71620
+ const processingLease = await this.processingIndicators.acquire({
71621
+ key: `telegram:${this.accountId}:${message.chat.id}:${routeInfo.topicId ?? "root"}`,
71622
+ activate: async () => beginTelegramTypingHeartbeat({
71623
+ sendTyping: () => this.sendTyping(message.chat.id, routeInfo.topicId),
71624
+ onError: (error) => {
71625
+ console.error("telegram typing failed", error);
71626
+ }
71627
+ }),
71628
+ onError: (_phase, error) => {
71629
+ console.error("telegram processing indicator failed", error);
71036
71630
  }
71037
71631
  });
71632
+ try {
71633
+ const interaction = await processChannelInteraction({
71634
+ agentService: this.agentService,
71635
+ sessionTarget: routeInfo.sessionTarget,
71636
+ identity,
71637
+ auth,
71638
+ senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
71639
+ text,
71640
+ agentPromptText,
71641
+ agentPromptBuilder: (nextText) => buildAgentPromptText({
71642
+ text: nextText,
71643
+ identity,
71644
+ config: this.loadedConfig.raw.channels.telegram.agentPrompt,
71645
+ cliTool,
71646
+ responseMode: routeInfo.route.responseMode,
71647
+ streaming: routeInfo.route.streaming,
71648
+ protectedControlMutationRule
71649
+ }),
71650
+ protectedControlMutationRule,
71651
+ route: routeInfo.route,
71652
+ maxChars: this.getTelegramMaxChars(routeInfo.route.agentId),
71653
+ timingContext,
71654
+ postText: async (nextText) => {
71655
+ responseChunks = await postTelegramText({
71656
+ token: this.accountConfig.botToken,
71657
+ chatId: message.chat.id,
71658
+ text: nextText,
71659
+ topicId: routeInfo.topicId,
71660
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71661
+ });
71662
+ return responseChunks;
71663
+ },
71664
+ reconcileText: async (chunks, nextText) => {
71665
+ responseChunks = await reconcileTelegramText({
71666
+ token: this.accountConfig.botToken,
71667
+ chatId: message.chat.id,
71668
+ chunks,
71669
+ text: nextText,
71670
+ topicId: routeInfo.topicId,
71671
+ omitThreadId: shouldOmitTelegramThreadId(routeInfo.topicId)
71672
+ });
71673
+ return responseChunks;
71674
+ }
71675
+ });
71676
+ await processingLease.setLifecycle({
71677
+ agentService: this.agentService,
71678
+ sessionTarget: routeInfo.sessionTarget,
71679
+ observerId: `telegram-processing:${message.chat.id}:${routeInfo.topicId ?? "root"}`,
71680
+ lifecycle: interaction.processingIndicatorLifecycle
71681
+ });
71682
+ } finally {
71683
+ await processingLease.release();
71684
+ }
71038
71685
  await this.processedEventsStore.markCompleted(eventId);
71039
71686
  } catch (error) {
71040
71687
  console.error("telegram handler error", error);
@@ -71435,9 +72082,9 @@ var defaultMessageCliDependencies = {
71435
72082
  loadConfig,
71436
72083
  plugins: listChannelPlugins(),
71437
72084
  print: (text) => console.log(text),
71438
- recordConversationReply: async ({ loadedConfig, target, kind }) => {
72085
+ recordConversationReply: async ({ loadedConfig, target, kind, source }) => {
71439
72086
  const agentService = new AgentService(loadedConfig);
71440
- await agentService.recordConversationReply(target, kind);
72087
+ await agentService.recordConversationReply(target, kind, source);
71441
72088
  }
71442
72089
  };
71443
72090
  function parseRepeatedOption2(args, name) {
@@ -71454,12 +72101,12 @@ function parseRepeatedOption2(args, name) {
71454
72101
  }
71455
72102
  return values;
71456
72103
  }
71457
- function parseOptionValue5(args, name) {
72104
+ function parseOptionValue4(args, name) {
71458
72105
  const values = parseRepeatedOption2(args, name);
71459
72106
  return values.length > 0 ? values.at(-1) : undefined;
71460
72107
  }
71461
72108
  function parseIntegerOption(args, name) {
71462
- const raw = parseOptionValue5(args, name);
72109
+ const raw = parseOptionValue4(args, name);
71463
72110
  if (!raw) {
71464
72111
  return;
71465
72112
  }
@@ -71488,25 +72135,25 @@ function parseMessageCommand(args) {
71488
72135
  }
71489
72136
  const action = rawAction;
71490
72137
  const rest = args.slice(1);
71491
- const channel = parseOptionValue5(rest, "--channel");
72138
+ const channel = parseOptionValue4(rest, "--channel");
71492
72139
  if (channel !== "slack" && channel !== "telegram") {
71493
72140
  throw new Error("--channel <slack|telegram> is required");
71494
72141
  }
71495
72142
  return {
71496
72143
  action,
71497
72144
  channel,
71498
- account: parseOptionValue5(rest, "--account"),
71499
- target: parseOptionValue5(rest, "--target"),
71500
- message: parseOptionValue5(rest, "--message") ?? parseOptionValue5(rest, "-m"),
71501
- media: parseOptionValue5(rest, "--media"),
71502
- messageId: parseOptionValue5(rest, "--message-id"),
71503
- emoji: parseOptionValue5(rest, "--emoji"),
72145
+ account: parseOptionValue4(rest, "--account"),
72146
+ target: parseOptionValue4(rest, "--target"),
72147
+ message: parseOptionValue4(rest, "--message") ?? parseOptionValue4(rest, "-m"),
72148
+ media: parseOptionValue4(rest, "--media"),
72149
+ messageId: parseOptionValue4(rest, "--message-id"),
72150
+ emoji: parseOptionValue4(rest, "--emoji"),
71504
72151
  remove: hasFlag3(rest, "--remove"),
71505
- threadId: parseOptionValue5(rest, "--thread-id"),
71506
- replyTo: parseOptionValue5(rest, "--reply-to"),
72152
+ threadId: parseOptionValue4(rest, "--thread-id"),
72153
+ replyTo: parseOptionValue4(rest, "--reply-to"),
71507
72154
  limit: parseIntegerOption(rest, "--limit"),
71508
- query: parseOptionValue5(rest, "--query"),
71509
- pollQuestion: parseOptionValue5(rest, "--poll-question"),
72155
+ query: parseOptionValue4(rest, "--query"),
72156
+ pollQuestion: parseOptionValue4(rest, "--poll-question"),
71510
72157
  pollOptions: parseRepeatedOption2(rest, "--poll-option"),
71511
72158
  forceDocument: hasFlag3(rest, "--force-document"),
71512
72159
  silent: hasFlag3(rest, "--silent"),
@@ -71549,7 +72196,9 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71549
72196
  throw new Error("--progress and --final cannot be used together");
71550
72197
  }
71551
72198
  assertTarget(command);
71552
- const loadedConfig = await dependencies.loadConfig(getConfigPath());
72199
+ const loadedConfig = await dependencies.loadConfig(getConfigPath(), {
72200
+ materializeChannels: [command.channel]
72201
+ });
71553
72202
  const plugin = dependencies.plugins.find((entry) => entry.id === command.channel);
71554
72203
  if (!plugin) {
71555
72204
  throw new Error(`Unsupported message channel: ${command.channel}`);
@@ -71564,7 +72213,8 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71564
72213
  await dependencies.recordConversationReply({
71565
72214
  loadedConfig,
71566
72215
  target: replyTarget,
71567
- kind: resolveReplyKind(command)
72216
+ kind: resolveReplyKind(command),
72217
+ source: "message-tool"
71568
72218
  });
71569
72219
  }
71570
72220
  if (command.json) {
@@ -71576,7 +72226,7 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
71576
72226
 
71577
72227
  // src/control/runtime-supervisor.ts
71578
72228
  import { statSync as statSync4, watch } from "node:fs";
71579
- import { basename as basename4, dirname as dirname13 } from "node:path";
72229
+ import { basename as basename4, dirname as dirname14 } from "node:path";
71580
72230
 
71581
72231
  // src/channels/processed-events-store.ts
71582
72232
  import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
@@ -71654,7 +72304,7 @@ class ProcessedEventsStore {
71654
72304
  }
71655
72305
 
71656
72306
  // src/control/activity-store.ts
71657
- import { dirname as dirname12 } from "node:path";
72307
+ import { dirname as dirname13 } from "node:path";
71658
72308
  class ActivityStore {
71659
72309
  filePath;
71660
72310
  constructor(filePath = getDefaultActivityStorePath()) {
@@ -71693,7 +72343,7 @@ class ActivityStore {
71693
72343
  };
71694
72344
  }
71695
72345
  async write(document2) {
71696
- await ensureDir2(dirname12(this.filePath));
72346
+ await ensureDir2(dirname13(this.filePath));
71697
72347
  await writeTextFile(this.filePath, `${JSON.stringify(document2, null, 2)}
71698
72348
  `);
71699
72349
  }
@@ -71935,7 +72585,7 @@ class RuntimeSupervisor {
71935
72585
  if (this.configWatcher) {
71936
72586
  return;
71937
72587
  }
71938
- const watchedDir = dirname13(loadedConfig.configPath);
72588
+ const watchedDir = dirname14(loadedConfig.configPath);
71939
72589
  const watchedFile = basename4(loadedConfig.configPath);
71940
72590
  this.configWatcher = watch(watchedDir, (_eventType, filename) => {
71941
72591
  if (filename && filename.toString() !== watchedFile) {