clisbot 0.1.46-beta.0 → 0.1.50

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
@@ -32451,7 +32451,7 @@ var require_websocket = __commonJS((exports, module) => {
32451
32451
  var http = __require("http");
32452
32452
  var net = __require("net");
32453
32453
  var tls = __require("tls");
32454
- var { randomBytes, createHash } = __require("crypto");
32454
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
32455
32455
  var { Duplex, Readable } = __require("stream");
32456
32456
  var { URL: URL2 } = __require("url");
32457
32457
  var PerMessageDeflate = require_permessage_deflate();
@@ -32990,7 +32990,7 @@ var require_websocket = __commonJS((exports, module) => {
32990
32990
  abortHandshake(websocket, socket, "Invalid Upgrade header");
32991
32991
  return;
32992
32992
  }
32993
- const digest = createHash("sha1").update(key + GUID).digest("base64");
32993
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
32994
32994
  if (res.headers["sec-websocket-accept"] !== digest) {
32995
32995
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
32996
32996
  return;
@@ -33363,7 +33363,7 @@ var require_websocket_server = __commonJS((exports, module) => {
33363
33363
  var EventEmitter = __require("events");
33364
33364
  var http = __require("http");
33365
33365
  var { Duplex } = __require("stream");
33366
- var { createHash } = __require("crypto");
33366
+ var { createHash: createHash2 } = __require("crypto");
33367
33367
  var extension = require_extension();
33368
33368
  var PerMessageDeflate = require_permessage_deflate();
33369
33369
  var subprotocol = require_subprotocol();
@@ -33576,7 +33576,7 @@ var require_websocket_server = __commonJS((exports, module) => {
33576
33576
  }
33577
33577
  if (this._state > RUNNING)
33578
33578
  return abortHandshake(socket, 503);
33579
- const digest = createHash("sha1").update(key + GUID).digest("base64");
33579
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
33580
33580
  const headers = [
33581
33581
  "HTTP/1.1 101 Switching Protocols",
33582
33582
  "Upgrade: websocket",
@@ -54755,6 +54755,11 @@ function renderCliHelp() {
54755
54755
  " personal One human gets one dedicated long-lived assistant workspace and session path.",
54756
54756
  " team One shared channel or group routes into one shared assistant workspace and session path.",
54757
54757
  "",
54758
+ "Bootstrap files:",
54759
+ " `--bot-type` seeds workspace guidance files for a fresh agent. It is optional when you already have a workspace.",
54760
+ " Canonical workspace instructions live in `AGENTS.md`.",
54761
+ " Claude and Gemini bootstraps also add `CLAUDE.md` or `GEMINI.md` as symlinks to `AGENTS.md` for CLI discovery.",
54762
+ "",
54758
54763
  "Credential input rules:",
54759
54764
  " Pass ENV_NAME or ${ENV_NAME} to keep the selected bot env-backed.",
54760
54765
  " Pass a raw or shell-expanded token value to use credentialType=mem for the current runtime only.",
@@ -54803,7 +54808,7 @@ function renderCliHelp() {
54803
54808
  " status Show runtime process, config, log, tmux socket status, and recent runner sessions.",
54804
54809
  " version Show the installed clisbot version.",
54805
54810
  " logs Print the most recent clisbot log lines.",
54806
- " update Print the AI-readable package update guide and release/migration doc links.",
54811
+ " update Print the update guide and release/migration doc links.",
54807
54812
  ` See ${renderCliCommand("update --help", { inline: true })} before asking an agent to update clisbot.`,
54808
54813
  " timezone Manage the app-wide wall-clock timezone used by schedules and loops.",
54809
54814
  ` See ${renderCliCommand("timezone --help", { inline: true })} for override guidance.`,
@@ -58902,10 +58907,12 @@ function parseCommandDurationMs(raw) {
58902
58907
  // src/agents/loop-command.ts
58903
58908
  var DEFAULT_LOOP_MAX_TIMES = 50;
58904
58909
  var LOOP_FORCE_FLAG = "--force";
58910
+ var LOOP_START_FLAG = "--loop-start";
58905
58911
  var LOOP_ALL_FLAG = "--all";
58906
58912
  var LOOP_APP_FLAG = "--app";
58907
58913
  var MIN_LOOP_INTERVAL_MS = 60000;
58908
58914
  var FORCE_LOOP_INTERVAL_MS = 5 * 60000;
58915
+ var LOOP_START_MODES = ["none", "brief", "full"];
58909
58916
  var LOOP_WEEKDAY_LABELS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
58910
58917
  var LOOP_WEEKDAY_ALIASES = {
58911
58918
  sun: 0,
@@ -58934,7 +58941,12 @@ function parseLoopSlashCommand(raw) {
58934
58941
  error: "Loop requires an interval, count, or schedule. Try `/loop 5m check CI`, `/loop 3 check CI`, `/loop every day at 07:00 check CI`, or `/loop 3` for maintenance mode."
58935
58942
  };
58936
58943
  }
58937
- const tokens = trimmed.split(/\s+/).filter(Boolean);
58944
+ const modifier = extractLoopStartModifier(trimmed);
58945
+ if ("error" in modifier) {
58946
+ return modifier;
58947
+ }
58948
+ const normalizedText = modifier.normalizedText;
58949
+ const tokens = modifier.normalizedText.split(/\s+/).filter(Boolean);
58938
58950
  const forceTokenIndexes = tokens.map((token, index) => token.toLowerCase() === LOOP_FORCE_FLAG ? index : -1).filter((index) => index >= 0);
58939
58951
  if (forceTokenIndexes.length > 1) {
58940
58952
  return {
@@ -58943,7 +58955,7 @@ function parseLoopSlashCommand(raw) {
58943
58955
  }
58944
58956
  const forceTokenIndex = forceTokenIndexes[0];
58945
58957
  const leadingToken = tokens[0] ?? "";
58946
- const everyDayMatch = trimmed.match(/^every\s+day\s+at\s+(\S+)(?:\s+(.*))?$/i);
58958
+ const everyDayMatch = normalizedText.match(/^every\s+day\s+at\s+(\S+)(?:\s+(.*))?$/i);
58947
58959
  if (everyDayMatch) {
58948
58960
  if (forceTokenIndex !== undefined) {
58949
58961
  return {
@@ -58956,7 +58968,7 @@ function parseLoopSlashCommand(raw) {
58956
58968
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
58957
58969
  };
58958
58970
  }
58959
- return {
58971
+ const parsed = {
58960
58972
  mode: "calendar",
58961
58973
  cadence: "daily",
58962
58974
  localTime: parsedTime.localTime,
@@ -58966,8 +58978,18 @@ function parseLoopSlashCommand(raw) {
58966
58978
  force: false,
58967
58979
  syntax: "calendar-at"
58968
58980
  };
58981
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
58982
+ if (validationError) {
58983
+ return {
58984
+ error: validationError
58985
+ };
58986
+ }
58987
+ return {
58988
+ ...parsed,
58989
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
58990
+ };
58969
58991
  }
58970
- const everyWeekdayMatch = trimmed.match(/^every\s+weekday\s+at\s+(\S+)(?:\s+(.*))?$/i);
58992
+ const everyWeekdayMatch = normalizedText.match(/^every\s+weekday\s+at\s+(\S+)(?:\s+(.*))?$/i);
58971
58993
  if (everyWeekdayMatch) {
58972
58994
  if (forceTokenIndex !== undefined) {
58973
58995
  return {
@@ -58980,7 +59002,7 @@ function parseLoopSlashCommand(raw) {
58980
59002
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
58981
59003
  };
58982
59004
  }
58983
- return {
59005
+ const parsed = {
58984
59006
  mode: "calendar",
58985
59007
  cadence: "weekday",
58986
59008
  localTime: parsedTime.localTime,
@@ -58990,8 +59012,18 @@ function parseLoopSlashCommand(raw) {
58990
59012
  force: false,
58991
59013
  syntax: "calendar-at"
58992
59014
  };
59015
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59016
+ if (validationError) {
59017
+ return {
59018
+ error: validationError
59019
+ };
59020
+ }
59021
+ return {
59022
+ ...parsed,
59023
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59024
+ };
58993
59025
  }
58994
- const everyDayOfWeekMatch = trimmed.match(/^every\s+([a-z]+)\s+at\s+(\S+)(?:\s+(.*))?$/i);
59026
+ const everyDayOfWeekMatch = normalizedText.match(/^every\s+([a-z]+)\s+at\s+(\S+)(?:\s+(.*))?$/i);
58995
59027
  if (everyDayOfWeekMatch) {
58996
59028
  const dayOfWeek = resolveLoopDayOfWeek(everyDayOfWeekMatch[1] ?? "");
58997
59029
  if (dayOfWeek != null) {
@@ -59006,7 +59038,7 @@ function parseLoopSlashCommand(raw) {
59006
59038
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
59007
59039
  };
59008
59040
  }
59009
- return {
59041
+ const parsed = {
59010
59042
  mode: "calendar",
59011
59043
  cadence: "day-of-week",
59012
59044
  dayOfWeek,
@@ -59017,6 +59049,16 @@ function parseLoopSlashCommand(raw) {
59017
59049
  force: false,
59018
59050
  syntax: "calendar-at"
59019
59051
  };
59052
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59053
+ if (validationError) {
59054
+ return {
59055
+ error: validationError
59056
+ };
59057
+ }
59058
+ return {
59059
+ ...parsed,
59060
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59061
+ };
59020
59062
  }
59021
59063
  }
59022
59064
  const leadingIntervalMs = parseCommandDurationMs(leadingToken);
@@ -59028,15 +59070,30 @@ function parseLoopSlashCommand(raw) {
59028
59070
  }
59029
59071
  const promptTokens = tokens.slice(forceTokenIndex === 1 ? 2 : 1);
59030
59072
  const promptText = promptTokens.join(" ").trim() || undefined;
59031
- return {
59073
+ const parsed = {
59032
59074
  mode: "interval",
59033
59075
  intervalMs: leadingIntervalMs,
59034
59076
  promptText,
59035
59077
  force: forceTokenIndex === 1,
59036
59078
  syntax: "leading-interval"
59037
59079
  };
59080
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59081
+ if (validationError) {
59082
+ return {
59083
+ error: validationError
59084
+ };
59085
+ }
59086
+ return {
59087
+ ...parsed,
59088
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59089
+ };
59038
59090
  }
59039
59091
  if (/^-?\d+$/.test(leadingToken)) {
59092
+ if (modifier.loopStart) {
59093
+ return {
59094
+ error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
59095
+ };
59096
+ }
59040
59097
  if (forceTokenIndex !== undefined) {
59041
59098
  return {
59042
59099
  error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
@@ -59057,8 +59114,13 @@ function parseLoopSlashCommand(raw) {
59057
59114
  syntax: "leading-count"
59058
59115
  };
59059
59116
  }
59060
- const trailingTimes = trimmed.match(/^(.*?)(?:\s+)?(-?\d+)\s+times$/i);
59117
+ const trailingTimes = normalizedText.match(/^(.*?)(?:\s+)?(-?\d+)\s+times$/i);
59061
59118
  if (trailingTimes) {
59119
+ if (modifier.loopStart) {
59120
+ return {
59121
+ error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
59122
+ };
59123
+ }
59062
59124
  if (forceTokenIndex !== undefined) {
59063
59125
  return {
59064
59126
  error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
@@ -59079,7 +59141,7 @@ function parseLoopSlashCommand(raw) {
59079
59141
  syntax: "trailing-times"
59080
59142
  };
59081
59143
  }
59082
- const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") : trimmed;
59144
+ const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") : normalizedText;
59083
59145
  const normalizedEveryInput = withoutTrailingForce.trim();
59084
59146
  const hasTrailingForce = forceTokenIndex === tokens.length - 1;
59085
59147
  if (forceTokenIndex !== undefined && !hasTrailingForce) {
@@ -59096,13 +59158,23 @@ function parseLoopSlashCommand(raw) {
59096
59158
  };
59097
59159
  }
59098
59160
  const promptText = everyCompactClause[1]?.trim() || undefined;
59099
- return {
59161
+ const parsed = {
59100
59162
  mode: "interval",
59101
59163
  intervalMs,
59102
59164
  promptText,
59103
59165
  force: hasTrailingForce,
59104
59166
  syntax: "every-clause"
59105
59167
  };
59168
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59169
+ if (validationError) {
59170
+ return {
59171
+ error: validationError
59172
+ };
59173
+ }
59174
+ return {
59175
+ ...parsed,
59176
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59177
+ };
59106
59178
  }
59107
59179
  const everyClause = normalizedEveryInput.match(/^(.*?)(?:\s+)?every\s+(-?\d+)\s+([a-z]+)$/i);
59108
59180
  if (everyClause) {
@@ -59120,18 +59192,84 @@ function parseLoopSlashCommand(raw) {
59120
59192
  };
59121
59193
  }
59122
59194
  const promptText = everyClause[1]?.trim() || undefined;
59123
- return {
59195
+ const parsed = {
59124
59196
  mode: "interval",
59125
59197
  intervalMs,
59126
59198
  promptText,
59127
59199
  force: hasTrailingForce,
59128
59200
  syntax: "every-clause"
59129
59201
  };
59202
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59203
+ if (validationError) {
59204
+ return {
59205
+ error: validationError
59206
+ };
59207
+ }
59208
+ return {
59209
+ ...parsed,
59210
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59211
+ };
59130
59212
  }
59131
59213
  return {
59132
59214
  error: "Loop requires an interval, count, or schedule. Try `/loop 5m check CI`, `/loop 3 check CI`, `/loop every day at 07:00 check CI`, or `/loop 3` for maintenance mode."
59133
59215
  };
59134
59216
  }
59217
+ function extractLoopStartModifier(raw) {
59218
+ const tokens = raw.split(/\s+/).filter(Boolean);
59219
+ const indexes = tokens.map((token, index) => token.trim().toLowerCase() === LOOP_START_FLAG ? index : -1).filter((index) => index >= 0);
59220
+ if (indexes.length > 1) {
59221
+ return {
59222
+ error: `Loop accepts at most one \`${LOOP_START_FLAG}\` flag.`
59223
+ };
59224
+ }
59225
+ const flagIndex = indexes[0];
59226
+ if (flagIndex == null) {
59227
+ return {
59228
+ normalizedText: raw,
59229
+ tokens
59230
+ };
59231
+ }
59232
+ const rawMode = tokens[flagIndex + 1]?.trim().toLowerCase();
59233
+ if (!rawMode) {
59234
+ return {
59235
+ error: `Loop requires \`${LOOP_START_FLAG} <none|brief|full>\`.`
59236
+ };
59237
+ }
59238
+ if (!LOOP_START_MODES.includes(rawMode)) {
59239
+ return {
59240
+ error: `\`${LOOP_START_FLAG}\` must be one of \`none\`, \`brief\`, or \`full\`.`
59241
+ };
59242
+ }
59243
+ const strippedTokens = tokens.filter((_, index) => index !== flagIndex && index !== flagIndex + 1);
59244
+ return {
59245
+ normalizedText: strippedTokens.join(" "),
59246
+ tokens,
59247
+ flagIndex,
59248
+ loopStart: rawMode
59249
+ };
59250
+ }
59251
+ function validateLoopStartModifierPlacement(parsed, modifier, tokenCount) {
59252
+ if (!modifier.loopStart || modifier.flagIndex == null) {
59253
+ return;
59254
+ }
59255
+ if (parsed.mode === "calendar") {
59256
+ if (modifier.flagIndex === 4) {
59257
+ return;
59258
+ }
59259
+ return `For wall-clock loops, \`${LOOP_START_FLAG}\` must appear immediately after the \`at HH:MM\` clause, for example \`/loop every day at 07:00 --loop-start none morning brief\`.`;
59260
+ }
59261
+ if (parsed.syntax === "leading-interval") {
59262
+ const expectedIndex = parsed.force ? 2 : 1;
59263
+ if (modifier.flagIndex === expectedIndex) {
59264
+ return;
59265
+ }
59266
+ return `For leading interval loops, \`${LOOP_START_FLAG}\` must appear after the interval and optional \`${LOOP_FORCE_FLAG}\`, before the prompt, for example \`/loop 5m --loop-start none check CI\`.`;
59267
+ }
59268
+ if (modifier.flagIndex === tokenCount - 2) {
59269
+ return;
59270
+ }
59271
+ return `For \`every ...\` interval loops, \`${LOOP_START_FLAG}\` must appear at the end of the loop schedule, for example \`/loop check deploy every 2h --loop-start none\`.`;
59272
+ }
59135
59273
  function formatLoopIntervalShort(intervalMs) {
59136
59274
  if (intervalMs % (60 * 60000) === 0) {
59137
59275
  return `${intervalMs / (60 * 60000)}h`;
@@ -59161,7 +59299,8 @@ function renderLoopHelpLines() {
59161
59299
  "- `/loop /codereview 3 times`: run the slash command 3 times",
59162
59300
  "- `/loop status`: show active loops for this session",
59163
59301
  `- \`/loop cancel\`, \`/loop cancel <id>\`, \`/loop cancel --all\`, \`/loop cancel --all ${LOOP_APP_FLAG}\`: cancel active loops`,
59164
- `- intervals must be at least \`1m\`; intervals below \`5m\` require \`${LOOP_FORCE_FLAG}\` right after the interval clause; wall-clock schedules use \`every ... at HH:MM\`; the first wall-clock loop created with \`clisbot loops create\` requires \`--confirm\`; timezone resolves from route/topic, agent, bot, app timezone, then legacy defaults and host; bare numbers mean times, compact durations such as \`5m\` mean intervals, and the historical default loop cap was \`${DEFAULT_LOOP_MAX_TIMES}\``
59302
+ `- intervals must be at least \`1m\`; intervals below \`5m\` require \`${LOOP_FORCE_FLAG}\` right after the interval clause; wall-clock schedules use \`every ... at HH:MM\`; the first wall-clock loop created with \`clisbot loops create\` requires \`--confirm\`; timezone resolves from route/topic, agent, bot, app timezone, then legacy defaults and host; bare numbers mean times, compact durations such as \`5m\` mean intervals, and the historical default loop cap was \`${DEFAULT_LOOP_MAX_TIMES}\``,
59303
+ `- Advanced: loop creation also accepts \`${LOOP_START_FLAG} <none|brief|full>\` to override the default start notification behavior for that loop. Example: \`/loop every day at 07:00 --loop-start none morning brief\``
59165
59304
  ];
59166
59305
  }
59167
59306
  function hasLoopFlag(raw, flag) {
@@ -60287,8 +60426,8 @@ function migrateLegacyConfigShape(config) {
60287
60426
  }
60288
60427
 
60289
60428
  // src/config/config-migration.ts
60290
- var CURRENT_SCHEMA_VERSION = "0.1.45";
60291
- var CONFIG_UPGRADE_MAX_SCHEMA_VERSION = "0.1.44";
60429
+ var CURRENT_SCHEMA_VERSION = "0.1.50";
60430
+ var LEGACY_CONFIG_UPGRADE_MAX_SCHEMA_VERSION = "0.1.44";
60292
60431
  function isRecord3(value) {
60293
60432
  return typeof value === "object" && value !== null && !Array.isArray(value);
60294
60433
  }
@@ -60322,6 +60461,22 @@ function isAtMostVersion(schemaVersion, maxVersion) {
60322
60461
  }
60323
60462
  return true;
60324
60463
  }
60464
+ function isBeforeVersion(schemaVersion, targetVersion) {
60465
+ const current = parseVersionParts(schemaVersion);
60466
+ const target = parseVersionParts(targetVersion);
60467
+ if (!current || !target) {
60468
+ return !schemaVersion;
60469
+ }
60470
+ for (let index = 0;index < current.length; index += 1) {
60471
+ if (current[index] < target[index]) {
60472
+ return true;
60473
+ }
60474
+ if (current[index] > target[index]) {
60475
+ return false;
60476
+ }
60477
+ }
60478
+ return false;
60479
+ }
60325
60480
  function readTimezone(value) {
60326
60481
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
60327
60482
  }
@@ -60572,7 +60727,10 @@ function updateSchemaVersion(config) {
60572
60727
  };
60573
60728
  }
60574
60729
  function shouldUpgradeConfigSchema(schemaVersion) {
60575
- return isAtMostVersion(schemaVersion, CONFIG_UPGRADE_MAX_SCHEMA_VERSION);
60730
+ return isBeforeVersion(schemaVersion, CURRENT_SCHEMA_VERSION);
60731
+ }
60732
+ function shouldApplyLegacyConfigMigration(schemaVersion) {
60733
+ return isAtMostVersion(schemaVersion, LEGACY_CONFIG_UPGRADE_MAX_SCHEMA_VERSION);
60576
60734
  }
60577
60735
  function normalizeConfigDocumentShape(input) {
60578
60736
  if (!isRecord3(input)) {
@@ -60581,7 +60739,7 @@ function normalizeConfigDocumentShape(input) {
60581
60739
  const config = { ...input };
60582
60740
  migrateLegacyConfigShape(config);
60583
60741
  const schemaVersion = isRecord3(config.meta) && typeof config.meta.schemaVersion === "string" ? config.meta.schemaVersion : undefined;
60584
- const legacyExactAdmission = shouldUpgradeConfigSchema(schemaVersion);
60742
+ const legacyExactAdmission = shouldApplyLegacyConfigMigration(schemaVersion);
60585
60743
  const bots = cloneRecord2(config.bots);
60586
60744
  const slack = cloneRecord2(bots.slack);
60587
60745
  const telegram = cloneRecord2(bots.telegram);
@@ -62066,7 +62224,7 @@ import { basename, dirname as dirname4, join as join5 } from "node:path";
62066
62224
 
62067
62225
  // src/config/persisted-config.ts
62068
62226
  var defaultOwnedRunnerFields = {
62069
- codex: ["startupReadyPattern"],
62227
+ codex: ["startupDelayMs", "startupReadyPattern"],
62070
62228
  gemini: [
62071
62229
  "startupDelayMs",
62072
62230
  "startupRetryCount",
@@ -62228,8 +62386,17 @@ function pruneCurrentSchemaStartupDefaults(config) {
62228
62386
  const defaults = isRecord6(agents?.defaults) ? agents.defaults : undefined;
62229
62387
  const runner = isRecord6(defaults?.runner) ? defaults.runner : undefined;
62230
62388
  const runnerDefaults = isRecord6(runner?.defaults) ? runner.defaults : undefined;
62389
+ const codexRunner = isRecord6(runner?.codex) ? runner.codex : undefined;
62390
+ let pruned = false;
62231
62391
  if (runnerDefaults?.startupDelayMs === 3000) {
62232
62392
  delete runnerDefaults.startupDelayMs;
62393
+ pruned = true;
62394
+ }
62395
+ if (codexRunner?.startupDelayMs === 3000) {
62396
+ delete codexRunner.startupDelayMs;
62397
+ pruned = true;
62398
+ }
62399
+ if (pruned) {
62233
62400
  nextConfig.meta = {
62234
62401
  ...meta,
62235
62402
  schemaVersion: CURRENT_SCHEMA_VERSION,
@@ -63254,7 +63421,7 @@ async function runPairingCli(args, writer = console) {
63254
63421
 
63255
63422
  // src/agents/bootstrap.ts
63256
63423
  import { fileURLToPath as fileURLToPath2 } from "node:url";
63257
- import { cpSync, existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
63424
+ import { cpSync, existsSync as existsSync4, lstatSync, readdirSync, rmSync as rmSync2, statSync as statSync2, symlinkSync } from "node:fs";
63258
63425
  import { dirname as dirname6, join as join6 } from "node:path";
63259
63426
  function resolveTemplateRoot(moduleDir) {
63260
63427
  const candidates = [
@@ -63272,35 +63439,40 @@ var TEMPLATE_ROOT = resolveTemplateRoot(dirname6(fileURLToPath2(import.meta.url)
63272
63439
  var DEFAULT_TEMPLATE_DIR = join6(TEMPLATE_ROOT, "default");
63273
63440
  var CUSTOMIZED_TEMPLATE_DIR = join6(TEMPLATE_ROOT, "customized");
63274
63441
  var CUSTOMIZED_DEFAULT_TEMPLATE_DIR = join6(CUSTOMIZED_TEMPLATE_DIR, "default");
63275
- var TOOL_BOOTSTRAP_FILE = {
63276
- codex: "AGENTS.md",
63442
+ var CANONICAL_BOOTSTRAP_FILE = "AGENTS.md";
63443
+ var TOOL_DISCOVERY_FILE = {
63277
63444
  claude: "CLAUDE.md",
63278
63445
  gemini: "GEMINI.md"
63279
63446
  };
63280
- function shouldIncludeTemplateFile(toolId, relativePath) {
63281
- const normalized = relativePath.replaceAll("\\", "/");
63282
- if (normalized.endsWith("AGENTS.md")) {
63283
- return toolId === "codex";
63447
+ function pathExists(path2) {
63448
+ try {
63449
+ lstatSync(path2);
63450
+ return true;
63451
+ } catch {
63452
+ return false;
63284
63453
  }
63454
+ }
63455
+ function shouldIncludeTemplateFile(relativePath) {
63456
+ const normalized = relativePath.replaceAll("\\", "/");
63285
63457
  if (normalized.endsWith("CLAUDE.md")) {
63286
- return toolId === "claude";
63458
+ return false;
63287
63459
  }
63288
63460
  if (normalized.endsWith("GEMINI.md")) {
63289
- return toolId === "gemini";
63461
+ return false;
63290
63462
  }
63291
63463
  return true;
63292
63464
  }
63293
- function collectTemplateFiles(rootDir, toolId, prefix = "") {
63465
+ function collectTemplateFiles(rootDir, prefix = "") {
63294
63466
  const files = [];
63295
63467
  for (const entry of readdirSync(rootDir)) {
63296
63468
  const sourcePath = join6(rootDir, entry);
63297
63469
  const relativePath = prefix ? join6(prefix, entry) : entry;
63298
63470
  const sourceStat = statSync2(sourcePath);
63299
63471
  if (sourceStat.isDirectory()) {
63300
- files.push(...collectTemplateFiles(sourcePath, toolId, relativePath));
63472
+ files.push(...collectTemplateFiles(sourcePath, relativePath));
63301
63473
  continue;
63302
63474
  }
63303
- if (!shouldIncludeTemplateFile(toolId, relativePath)) {
63475
+ if (!shouldIncludeTemplateFile(relativePath)) {
63304
63476
  continue;
63305
63477
  }
63306
63478
  files.push({
@@ -63313,22 +63485,44 @@ function collectTemplateFiles(rootDir, toolId, prefix = "") {
63313
63485
  }
63314
63486
  function getTemplateFiles(toolId, mode) {
63315
63487
  return [
63316
- ...collectTemplateFiles(DEFAULT_TEMPLATE_DIR, toolId),
63317
- ...collectTemplateFiles(CUSTOMIZED_DEFAULT_TEMPLATE_DIR, toolId).map((file) => ({
63488
+ ...collectTemplateFiles(DEFAULT_TEMPLATE_DIR),
63489
+ ...collectTemplateFiles(CUSTOMIZED_DEFAULT_TEMPLATE_DIR).map((file) => ({
63318
63490
  ...file,
63319
63491
  customized: true
63320
63492
  })),
63321
- ...collectTemplateFiles(join6(CUSTOMIZED_TEMPLATE_DIR, mode), toolId).map((file) => ({
63493
+ ...collectTemplateFiles(join6(CUSTOMIZED_TEMPLATE_DIR, mode)).map((file) => ({
63322
63494
  ...file,
63323
63495
  customized: true
63324
63496
  }))
63325
63497
  ];
63326
63498
  }
63499
+ function getBootstrapManagedPaths(toolId, mode) {
63500
+ const paths = new Set(getTemplateFiles(toolId, mode).map((file) => file.relativePath));
63501
+ const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
63502
+ if (discoveryFile) {
63503
+ paths.add(discoveryFile);
63504
+ }
63505
+ return [...paths];
63506
+ }
63327
63507
  function getBootstrapTemplateConflicts(workspacePath, toolId, mode) {
63328
63508
  if (!existsSync4(workspacePath)) {
63329
63509
  return [];
63330
63510
  }
63331
- return getTemplateFiles(toolId, mode).map((file) => file.relativePath).filter((relativePath) => existsSync4(join6(workspacePath, relativePath)));
63511
+ return getBootstrapManagedPaths(toolId, mode).filter((relativePath) => pathExists(join6(workspacePath, relativePath)));
63512
+ }
63513
+ function writeToolDiscoverySymlink(workspacePath, toolId, options) {
63514
+ const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
63515
+ if (!discoveryFile) {
63516
+ return;
63517
+ }
63518
+ const destinationPath = join6(workspacePath, discoveryFile);
63519
+ if (options?.force) {
63520
+ rmSync2(destinationPath, {
63521
+ force: true,
63522
+ recursive: false
63523
+ });
63524
+ }
63525
+ symlinkSync(CANONICAL_BOOTSTRAP_FILE, destinationPath);
63332
63526
  }
63333
63527
  async function applyBootstrapTemplate(workspacePath, mode, toolId, options) {
63334
63528
  const force = options?.force === true;
@@ -63344,6 +63538,9 @@ async function applyBootstrapTemplate(workspacePath, mode, toolId, options) {
63344
63538
  force: force || file.customized
63345
63539
  });
63346
63540
  }
63541
+ writeToolDiscoverySymlink(workspacePath, toolId, {
63542
+ force
63543
+ });
63347
63544
  }
63348
63545
  function getBootstrapWorkspaceState(workspacePath, mode, toolId) {
63349
63546
  if (!mode) {
@@ -63352,7 +63549,11 @@ function getBootstrapWorkspaceState(workspacePath, mode, toolId) {
63352
63549
  if (!toolId || !existsSync4(workspacePath)) {
63353
63550
  return "missing";
63354
63551
  }
63355
- if (!existsSync4(join6(workspacePath, TOOL_BOOTSTRAP_FILE[toolId])) || !existsSync4(join6(workspacePath, "IDENTITY.md"))) {
63552
+ if (!existsSync4(join6(workspacePath, CANONICAL_BOOTSTRAP_FILE)) || !existsSync4(join6(workspacePath, "IDENTITY.md"))) {
63553
+ return "missing";
63554
+ }
63555
+ const discoveryFile = TOOL_DISCOVERY_FILE[toolId];
63556
+ if (discoveryFile && !existsSync4(join6(workspacePath, discoveryFile))) {
63356
63557
  return "missing";
63357
63558
  }
63358
63559
  if (existsSync4(join6(workspacePath, "BOOTSTRAP.md"))) {
@@ -63559,6 +63760,9 @@ function renderAgentsHelp() {
63559
63760
  "Notes:",
63560
63761
  ` - \`agents add\` is the lower-level manual surface; first-run ${renderCliCommand("start", { inline: true })} and ${renderCliCommand("init", { inline: true })} can bootstrap the first \`default\` agent for you`,
63561
63762
  " - `--cli` is required on `agents add`; supported tools are `codex`, `claude`, and `gemini`",
63763
+ " - `agents add` without `--bot-type` is valid and does not seed any bootstrap files",
63764
+ " - `--bot-type` on `agents add` or `agents bootstrap` seeds a fresh workspace template; use it when you want clisbot to create guidance files for you",
63765
+ " - canonical workspace instructions live in `AGENTS.md`; Claude and Gemini add `CLAUDE.md` or `GEMINI.md` as symlinks to that same file",
63562
63766
  " - omit `--startup-option` to inherit the built-in startup args for the selected CLI tool",
63563
63767
  " - `response-mode` and `additional-message-mode` mutate per-agent overrides under `agents.list[]`",
63564
63768
  " - use agent timezone only when one workspace/assistant should run wall-clock loops in a different timezone than app default"
@@ -64527,7 +64731,7 @@ class RuntimeHealthStore {
64527
64731
 
64528
64732
  // src/control/runtime-process.ts
64529
64733
  import { execFileSync, spawn as spawn3 } from "node:child_process";
64530
- import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
64734
+ import { closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync3, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
64531
64735
  import { dirname as dirname15, join as join13 } from "node:path";
64532
64736
  import { kill as kill2 } from "node:process";
64533
64737
 
@@ -65133,14 +65337,20 @@ class TmuxClient {
65133
65337
  }
65134
65338
  return result.stdout;
65135
65339
  }
65340
+ sessionTarget(sessionName) {
65341
+ return `=${sessionName}`;
65342
+ }
65136
65343
  target(sessionName) {
65137
- return `${sessionName}:${MAIN_WINDOW_NAME}`;
65344
+ return `${this.sessionTarget(sessionName)}:${MAIN_WINDOW_NAME}`;
65345
+ }
65346
+ windowTarget(sessionName, windowName) {
65347
+ return `${this.sessionTarget(sessionName)}:${windowName}`;
65138
65348
  }
65139
65349
  rawTarget(target) {
65140
65350
  return target;
65141
65351
  }
65142
65352
  async hasSession(sessionName) {
65143
- const result = await this.exec(["has-session", "-t", sessionName]);
65353
+ const result = await this.exec(["has-session", "-t", this.sessionTarget(sessionName)]);
65144
65354
  return result.exitCode === 0;
65145
65355
  }
65146
65356
  async listSessions() {
@@ -65236,14 +65446,14 @@ ${result.stdout}`.trim();
65236
65446
  "-F",
65237
65447
  "#{pane_id}",
65238
65448
  "-t",
65239
- params.sessionName,
65449
+ this.sessionTarget(params.sessionName),
65240
65450
  "-n",
65241
65451
  params.name,
65242
65452
  "-c",
65243
65453
  params.cwd,
65244
65454
  params.command
65245
65455
  ]);
65246
- await this.freezeWindowName(`${params.sessionName}:${params.name}`);
65456
+ await this.freezeWindowName(this.windowTarget(params.sessionName, params.name));
65247
65457
  return paneId.trim();
65248
65458
  }
65249
65459
  async freezeWindowName(target) {
@@ -65254,7 +65464,7 @@ ${result.stdout}`.trim();
65254
65464
  const output = await this.execOrThrow([
65255
65465
  "list-windows",
65256
65466
  "-t",
65257
- sessionName,
65467
+ this.sessionTarget(sessionName),
65258
65468
  "-F",
65259
65469
  "#{window_name}\t#{pane_id}"
65260
65470
  ]);
@@ -65330,7 +65540,7 @@ ${result.stdout}`.trim();
65330
65540
  };
65331
65541
  }
65332
65542
  async killSession(sessionName) {
65333
- await this.exec(["kill-session", "-t", sessionName]);
65543
+ await this.exec(["kill-session", "-t", this.sessionTarget(sessionName)]);
65334
65544
  }
65335
65545
  async killPane(target) {
65336
65546
  await this.exec(["kill-pane", "-t", this.rawTarget(target)]);
@@ -65360,7 +65570,7 @@ ${result.stdout}`.trim();
65360
65570
 
65361
65571
  // src/control/runtime-monitor.ts
65362
65572
  import { spawn as spawn2 } from "node:child_process";
65363
- import { existsSync as existsSync6, rmSync as rmSync2 } from "node:fs";
65573
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "node:fs";
65364
65574
  import { dirname as dirname14 } from "node:path";
65365
65575
  import { kill } from "node:process";
65366
65576
  import { once } from "node:events";
@@ -65882,12 +66092,19 @@ function parseAgentCommand(text, options = {}) {
65882
66092
  if (lowered === "loop") {
65883
66093
  const loopText = withoutSlash.slice(command.length).trim();
65884
66094
  const loweredLoopText = loopText.toLowerCase();
66095
+ const hasLoopStartOverride = hasLoopFlag(loopText, LOOP_START_FLAG);
65885
66096
  if (!loweredLoopText || loweredLoopText === "help") {
65886
66097
  return {
65887
66098
  type: "control",
65888
66099
  name: "loop-help"
65889
66100
  };
65890
66101
  }
66102
+ if (hasLoopStartOverride && (loweredLoopText.startsWith("help ") || loweredLoopText === "status" || loweredLoopText.startsWith("status "))) {
66103
+ return {
66104
+ type: "loop-error",
66105
+ message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
66106
+ };
66107
+ }
65891
66108
  if (loweredLoopText === "status") {
65892
66109
  return {
65893
66110
  type: "loop-control",
@@ -65896,6 +66113,12 @@ function parseAgentCommand(text, options = {}) {
65896
66113
  }
65897
66114
  if (loweredLoopText === "cancel" || loweredLoopText.startsWith("cancel ")) {
65898
66115
  const cancelArgs = loopText.slice("cancel".length).trim();
66116
+ if (hasLoopStartOverride) {
66117
+ return {
66118
+ type: "loop-error",
66119
+ message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
66120
+ };
66121
+ }
65899
66122
  if (hasLoopFlag(cancelArgs, LOOP_FORCE_FLAG)) {
65900
66123
  return {
65901
66124
  type: "loop-error",
@@ -66004,14 +66227,14 @@ function renderAgentControlSlashHelp() {
66004
66227
  "- `/start`: show onboarding help for the current surface",
66005
66228
  "- `/status`: show the current route status and operator setup commands",
66006
66229
  "- `/help`: show available control slash commands",
66007
- "- `/whoami`: show the current platform, route, and sender identity details",
66230
+ "- `/whoami`: show the current platform, route, sender identity, and saved session id",
66008
66231
  "- `/transcript`: show a short recent session snapshot when the route verbose policy allows it",
66009
66232
  "- `/transcript full`: show a longer session snapshot when you need the full pane context",
66010
66233
  "- `/attach`: attach this thread to the active run and resume live updates when it is still processing",
66011
66234
  "- `/detach`: stop live updates for this thread while still posting the final result here",
66012
66235
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
66013
66236
  "- `/stop`: send Escape to interrupt the current conversation session",
66014
- "- `/new`: trigger a new runner conversation for this routed session and store the new session id",
66237
+ "- `/new`: start a new session for this routed conversation and save the new session id",
66015
66238
  "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
66016
66239
  "- `/followup status`: show the current conversation follow-up policy",
66017
66240
  "- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
@@ -66171,27 +66394,6 @@ class SessionStore {
66171
66394
  return next;
66172
66395
  });
66173
66396
  }
66174
- async touch(params) {
66175
- const existing = await this.get(params.sessionKey);
66176
- const sessionId = params.sessionId?.trim() || existing?.sessionId;
66177
- if (!sessionId) {
66178
- return null;
66179
- }
66180
- return this.put({
66181
- agentId: params.agentId,
66182
- sessionKey: params.sessionKey,
66183
- sessionId,
66184
- workspacePath: params.workspacePath,
66185
- runnerCommand: params.runnerCommand,
66186
- lastAdmittedPromptAt: existing?.lastAdmittedPromptAt,
66187
- followUp: existing?.followUp,
66188
- runtime: existing?.runtime,
66189
- loops: existing?.loops ?? existing?.intervalLoops,
66190
- queues: existing?.queues,
66191
- recentConversation: existing?.recentConversation,
66192
- updatedAt: Date.now()
66193
- });
66194
- }
66195
66397
  async withPathLock(work) {
66196
66398
  const previous = SessionStore.pathLocks.get(this.storePath) ?? Promise.resolve();
66197
66399
  let release;
@@ -66348,9 +66550,7 @@ class AgentSessionState {
66348
66550
  lastAdmittedPromptAt: existing?.lastAdmittedPromptAt,
66349
66551
  followUp: existing?.followUp,
66350
66552
  runnerCommand: params.runnerCommand ?? existing?.runnerCommand ?? resolved.runner.command,
66351
- runtime: {
66352
- state: "idle"
66353
- },
66553
+ runtime: params.preserveRuntime ? existing?.runtime : { state: "idle" },
66354
66554
  loops: getStoredLoops(existing),
66355
66555
  recentConversation: existing?.recentConversation
66356
66556
  }));
@@ -66394,19 +66594,6 @@ class AgentSessionState {
66394
66594
  agentId: target.agentId
66395
66595
  };
66396
66596
  }
66397
- async listActiveSessionRuntimes() {
66398
- const entries = await this.sessionStore.list();
66399
- return entries.filter(hasActiveRuntime).map((entry) => ({
66400
- state: entry.runtime.state,
66401
- startedAt: entry.runtime.startedAt,
66402
- detachedAt: entry.runtime.detachedAt,
66403
- finalReplyAt: entry.runtime.finalReplyAt,
66404
- lastMessageToolReplyAt: entry.runtime.lastMessageToolReplyAt,
66405
- messageToolFinalReplyAt: entry.runtime.messageToolFinalReplyAt,
66406
- sessionKey: entry.sessionKey,
66407
- agentId: entry.agentId
66408
- }));
66409
- }
66410
66597
  async listIntervalLoops(params) {
66411
66598
  const entries = await this.sessionStore.list();
66412
66599
  return entries.flatMap((entry) => getStoredLoops(entry).filter((loop) => !params?.sessionKey || entry.sessionKey === params.sessionKey).map((loop) => ({
@@ -66664,6 +66851,10 @@ class AgentSessionState {
66664
66851
  const entry = await this.sessionStore.get(sessionKey);
66665
66852
  return getStoredQueues(entry).some((item) => item.status === "pending" || item.status === "running");
66666
66853
  }
66854
+ async hasQueuedItem(sessionKey, queueId) {
66855
+ const entry = await this.sessionStore.get(sessionKey);
66856
+ return getStoredQueues(entry).some((item) => item.id === queueId);
66857
+ }
66667
66858
  async resetStaleRunningQueuedItems(activeSessionKeys) {
66668
66859
  const entries = await this.sessionStore.list();
66669
66860
  let reset = 0;
@@ -66744,11 +66935,74 @@ function getStoredLoops(entry) {
66744
66935
  function getStoredQueues(entry) {
66745
66936
  return entry?.queues ?? [];
66746
66937
  }
66747
- function hasActiveRuntime(entry) {
66748
- return entry.runtime?.state === "running" || entry.runtime?.state === "detached";
66938
+
66939
+ // src/agents/session-identity.ts
66940
+ import { randomUUID as randomUUID3 } from "node:crypto";
66941
+ function createSessionId() {
66942
+ return randomUUID3();
66943
+ }
66944
+ function parseRunnerSessionId(snapshot, pattern) {
66945
+ const regex = new RegExp(pattern, "ig");
66946
+ let lastMatch = null;
66947
+ for (;; ) {
66948
+ const match = regex.exec(snapshot);
66949
+ if (!match) {
66950
+ break;
66951
+ }
66952
+ if (!match[0]) {
66953
+ break;
66954
+ }
66955
+ lastMatch = match;
66956
+ }
66957
+ return (lastMatch?.[1] ?? lastMatch?.[0] ?? "").trim() || null;
66958
+ }
66959
+
66960
+ // src/agents/session-mapping.ts
66961
+ class SessionMapping {
66962
+ sessionState;
66963
+ constructor(sessionState) {
66964
+ this.sessionState = sessionState;
66965
+ }
66966
+ async get(sessionKey) {
66967
+ return this.sessionState.getEntry(sessionKey);
66968
+ }
66969
+ async prepareStartup(resolved) {
66970
+ const entry = await this.sessionState.getEntry(resolved.sessionKey);
66971
+ const storedSessionId = entry?.sessionId?.trim() || undefined;
66972
+ if (storedSessionId) {
66973
+ return {
66974
+ storedSessionId,
66975
+ sessionId: storedSessionId,
66976
+ resume: true
66977
+ };
66978
+ }
66979
+ if (resolved.runner.sessionId.create.mode !== "explicit") {
66980
+ return {
66981
+ resume: false
66982
+ };
66983
+ }
66984
+ return {
66985
+ sessionId: createSessionId(),
66986
+ resume: false
66987
+ };
66988
+ }
66989
+ async setActive(resolved, params) {
66990
+ return this.sessionState.touchSessionEntry(resolved, {
66991
+ sessionId: params.sessionId,
66992
+ runnerCommand: params.runnerCommand,
66993
+ runtime: params.runtime
66994
+ });
66995
+ }
66996
+ async clearActive(resolved, params = {}) {
66997
+ return this.sessionState.clearSessionIdEntry(resolved, params);
66998
+ }
66999
+ async touch(resolved, params = {}) {
67000
+ return this.sessionState.touchSessionEntry(resolved, params);
67001
+ }
66749
67002
  }
66750
67003
 
66751
67004
  // src/agents/session-key.ts
67005
+ import { createHash } from "node:crypto";
66752
67006
  var DEFAULT_MAIN_KEY = "main";
66753
67007
  var DEFAULT_BOT_ID = "default";
66754
67008
  var DEFAULT_ACCOUNT_ID = DEFAULT_BOT_ID;
@@ -66839,7 +67093,8 @@ function buildTmuxSessionName(params) {
66839
67093
  mainKey: normalizeMainKey(params.mainKey)
66840
67094
  });
66841
67095
  const baseName = sanitizeSessionName(rendered);
66842
- return baseName;
67096
+ const sessionHash = createHash("sha1").update(params.sessionKey).digest("hex").slice(0, 8);
67097
+ return `${baseName}-${sessionHash}`;
66843
67098
  }
66844
67099
 
66845
67100
  // src/agents/resolved-target.ts
@@ -66999,39 +67254,15 @@ class AgentJobQueue {
66999
67254
  if (!state) {
67000
67255
  return 0;
67001
67256
  }
67002
- const keptEntries = state.entries.filter((entry) => entry.status === "running");
67003
- const removedEntries = state.entries.filter((entry) => entry.status === "pending");
67004
- state.entries = keptEntries;
67005
- for (const entry of removedEntries) {
67006
- Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67007
- return;
67008
- });
67009
- entry.reject(new ClearedQueuedTaskError);
67010
- }
67011
- if (state.entries.length === 0 && !state.running) {
67012
- this.states.delete(key);
67013
- }
67014
- return removedEntries.length;
67257
+ return this.clearPendingEntriesForState(key, state, () => true);
67015
67258
  }
67016
- clearPendingByIds(ids) {
67017
- const idSet = new Set(ids);
67018
- let cleared = 0;
67019
- for (const [key, state] of this.states.entries()) {
67020
- const keptEntries = state.entries.filter((entry) => entry.status === "running" || !idSet.has(entry.id));
67021
- const removedEntries = state.entries.filter((entry) => entry.status === "pending" && idSet.has(entry.id));
67022
- state.entries = keptEntries;
67023
- cleared += removedEntries.length;
67024
- for (const entry of removedEntries) {
67025
- Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67026
- return;
67027
- });
67028
- entry.reject(new ClearedQueuedTaskError);
67029
- }
67030
- if (state.entries.length === 0 && !state.running) {
67031
- this.states.delete(key);
67032
- }
67259
+ clearPendingByIdsForKey(key, ids) {
67260
+ const state = this.states.get(key);
67261
+ if (!state) {
67262
+ return 0;
67033
67263
  }
67034
- return cleared;
67264
+ const idSet = new Set(ids);
67265
+ return this.clearPendingEntriesForState(key, state, (entry) => idSet.has(entry.id));
67035
67266
  }
67036
67267
  getOrCreateState(key) {
67037
67268
  const existing = this.states.get(key);
@@ -67056,6 +67287,21 @@ class AgentJobQueue {
67056
67287
  return left.sequence - right.sequence;
67057
67288
  });
67058
67289
  }
67290
+ clearPendingEntriesForState(key, state, shouldClear) {
67291
+ const keptEntries = state.entries.filter((entry) => entry.status === "running" || !shouldClear(entry));
67292
+ const removedEntries = state.entries.filter((entry) => entry.status === "pending" && shouldClear(entry));
67293
+ state.entries = keptEntries;
67294
+ for (const entry of removedEntries) {
67295
+ Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67296
+ return;
67297
+ });
67298
+ entry.reject(new ClearedQueuedTaskError);
67299
+ }
67300
+ if (state.entries.length === 0 && !state.running) {
67301
+ this.states.delete(key);
67302
+ }
67303
+ return removedEntries.length;
67304
+ }
67059
67305
  async drain(key, state) {
67060
67306
  if (state.running) {
67061
67307
  return;
@@ -67142,10 +67388,10 @@ function buildResumeCommandPreview(resolved, sessionId) {
67142
67388
  }
67143
67389
 
67144
67390
  // src/agents/loop-control-shared.ts
67145
- import { randomUUID as randomUUID3 } from "node:crypto";
67391
+ import { randomUUID as randomUUID4 } from "node:crypto";
67146
67392
  import { join as join10 } from "node:path";
67147
67393
  function createLoopId() {
67148
- return randomUUID3().split("-")[0] ?? randomUUID3();
67394
+ return randomUUID4().split("-")[0] ?? randomUUID4();
67149
67395
  }
67150
67396
  function buildStoredLoopSender(params) {
67151
67397
  const providerId = params.providerId.trim();
@@ -67172,10 +67418,11 @@ function createStoredLoopBase(params) {
67172
67418
  updatedAt: now,
67173
67419
  nextRunAt: params.nextRunAt,
67174
67420
  promptText: params.promptText,
67175
- canonicalPromptText: params.canonicalPromptText,
67176
67421
  protectedControlMutationRule: params.protectedControlMutationRule,
67177
67422
  promptSummary: params.promptSummary,
67178
67423
  promptSource: params.promptSource,
67424
+ progressMessages: params.progressMessages,
67425
+ loopStart: params.loopStart,
67179
67426
  createdBy: params.createdBy,
67180
67427
  sender: params.sender ?? deriveLegacyLoopSender({
67181
67428
  createdBy: params.createdBy,
@@ -67202,10 +67449,11 @@ function createStoredIntervalLoop(params) {
67202
67449
  ...createStoredLoopBase({
67203
67450
  nextRunAt: Date.now(),
67204
67451
  promptText: params.promptText,
67205
- canonicalPromptText: params.canonicalPromptText,
67206
67452
  protectedControlMutationRule: params.protectedControlMutationRule,
67207
67453
  promptSummary: params.promptSummary,
67208
67454
  promptSource: params.promptSource,
67455
+ progressMessages: params.progressMessages,
67456
+ loopStart: params.loopStart,
67209
67457
  createdBy: params.createdBy,
67210
67458
  sender: params.sender,
67211
67459
  surfaceBinding: params.surfaceBinding,
@@ -67232,10 +67480,11 @@ function createStoredCalendarLoop(params) {
67232
67480
  ...createStoredLoopBase({
67233
67481
  nextRunAt,
67234
67482
  promptText: params.promptText,
67235
- canonicalPromptText: params.canonicalPromptText,
67236
67483
  protectedControlMutationRule: params.protectedControlMutationRule,
67237
67484
  promptSummary: params.promptSummary,
67238
67485
  promptSource: params.promptSource,
67486
+ progressMessages: params.progressMessages,
67487
+ loopStart: params.loopStart,
67239
67488
  createdBy: params.createdBy,
67240
67489
  sender: params.sender,
67241
67490
  surfaceBinding: params.surfaceBinding,
@@ -67392,87 +67641,86 @@ class ManagedLoopController {
67392
67641
  }
67393
67642
  async reconcilePersistedIntervalLoops() {
67394
67643
  const persistedLoops = await this.deps.sessionState.listIntervalLoops();
67395
- const persistedIds = new Set;
67644
+ const persistedKeys = new Set;
67396
67645
  for (const persisted of persistedLoops) {
67397
- persistedIds.add(persisted.id);
67646
+ const managedKey = this.buildManagedLoopKey(persisted.sessionKey, persisted.id);
67647
+ persistedKeys.add(managedKey);
67398
67648
  if (persisted.attemptedRuns >= persisted.maxRuns) {
67399
- this.dropManagedIntervalLoop(persisted.id);
67649
+ this.dropManagedIntervalLoop(managedKey);
67400
67650
  continue;
67401
67651
  }
67402
- if (this.intervalLoops.has(persisted.id)) {
67652
+ if (this.intervalLoops.has(managedKey)) {
67403
67653
  continue;
67404
67654
  }
67405
- this.intervalLoops.set(persisted.id, {
67655
+ this.intervalLoops.set(managedKey, {
67406
67656
  target: {
67407
67657
  agentId: persisted.agentId,
67408
67658
  sessionKey: persisted.sessionKey
67409
67659
  },
67410
67660
  loop: persisted
67411
67661
  });
67412
- this.scheduleIntervalLoopTimer(persisted.id, Math.max(0, persisted.nextRunAt - Date.now()));
67662
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, persisted.nextRunAt - Date.now()));
67413
67663
  }
67414
- for (const loopId of this.intervalLoops.keys()) {
67415
- if (!persistedIds.has(loopId)) {
67416
- this.dropManagedIntervalLoop(loopId);
67664
+ for (const managedKey of this.intervalLoops.keys()) {
67665
+ if (!persistedKeys.has(managedKey)) {
67666
+ this.dropManagedIntervalLoop(managedKey);
67417
67667
  }
67418
67668
  }
67419
67669
  }
67420
67670
  async createIntervalLoop(params) {
67421
67671
  this.assertActiveLoopCapacity();
67422
67672
  const loop = createStoredIntervalLoop(params);
67673
+ const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
67423
67674
  await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
67424
- this.intervalLoops.set(loop.id, {
67675
+ this.intervalLoops.set(managedKey, {
67425
67676
  target: params.target,
67426
67677
  loop
67427
67678
  });
67428
- await this.runIntervalLoopIteration(loop.id, {
67679
+ await this.runIntervalLoopIteration(managedKey, {
67429
67680
  notifyStart: false
67430
67681
  });
67431
- return this.getIntervalLoop(loop.id);
67682
+ return this.getIntervalLoop(params.target.sessionKey, loop.id);
67432
67683
  }
67433
67684
  async createCalendarLoop(params) {
67434
67685
  this.assertActiveLoopCapacity();
67435
67686
  const loop = createStoredCalendarLoop(params);
67687
+ const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
67436
67688
  await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
67437
- this.intervalLoops.set(loop.id, {
67689
+ this.intervalLoops.set(managedKey, {
67438
67690
  target: params.target,
67439
67691
  loop
67440
67692
  });
67441
- this.scheduleIntervalLoopTimer(loop.id, Math.max(0, loop.nextRunAt - Date.now()));
67442
- return this.getIntervalLoop(loop.id);
67693
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, loop.nextRunAt - Date.now()));
67694
+ return this.getIntervalLoop(params.target.sessionKey, loop.id);
67443
67695
  }
67444
- async cancelIntervalLoop(loopId) {
67445
- const managed = this.intervalLoops.get(loopId);
67696
+ async cancelIntervalLoop(target, loopId) {
67697
+ const managedKey = this.buildManagedLoopKey(target.sessionKey, loopId);
67698
+ const managed = this.intervalLoops.get(managedKey);
67446
67699
  if (!managed) {
67447
67700
  return false;
67448
67701
  }
67449
- if (managed.timer) {
67450
- clearTimeout(managed.timer);
67451
- this.loopTimers.delete(managed.timer);
67452
- }
67453
- this.intervalLoops.delete(loopId);
67454
- await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), loopId);
67702
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67455
67703
  return true;
67456
67704
  }
67457
67705
  async cancelIntervalLoopsForSession(target) {
67458
- const matching = [...this.intervalLoops.values()].filter((managed) => managed.target.sessionKey === target.sessionKey).map((managed) => managed.loop.id);
67459
- for (const loopId of matching) {
67460
- await this.cancelIntervalLoop(loopId);
67706
+ const matching = [...this.intervalLoops.entries()].filter(([, managed]) => managed.target.sessionKey === target.sessionKey);
67707
+ for (const [managedKey, managed] of matching) {
67708
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67461
67709
  }
67462
67710
  return matching.length;
67463
67711
  }
67464
67712
  async cancelAllIntervalLoops() {
67465
- const ids = [...this.intervalLoops.keys()];
67466
- for (const loopId of ids) {
67467
- await this.cancelIntervalLoop(loopId);
67713
+ const matching = [...this.intervalLoops.entries()];
67714
+ for (const [managedKey, managed] of matching) {
67715
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67468
67716
  }
67469
- return ids.length;
67717
+ return matching.length;
67470
67718
  }
67471
67719
  listIntervalLoops(params) {
67472
67720
  return [...this.intervalLoops.values()].filter((managed) => !params?.sessionKey || managed.target.sessionKey === params.sessionKey).map((managed) => this.toLoopStatus(managed)).sort((left, right) => left.nextRunAt - right.nextRunAt);
67473
67721
  }
67474
- getIntervalLoop(loopId) {
67475
- const managed = this.intervalLoops.get(loopId);
67722
+ getIntervalLoop(sessionKey, loopId) {
67723
+ const managed = this.intervalLoops.get(this.buildManagedLoopKey(sessionKey, loopId));
67476
67724
  return managed ? this.toLoopStatus(managed) : null;
67477
67725
  }
67478
67726
  getActiveIntervalLoopCount() {
@@ -67496,8 +67744,8 @@ class ManagedLoopController {
67496
67744
  const entry = await this.deps.sessionState.getEntry(managed.target.sessionKey);
67497
67745
  return (entry?.loops ?? entry?.intervalLoops ?? []).some((loop) => loop.id === managed.loop.id);
67498
67746
  }
67499
- dropManagedIntervalLoop(loopId) {
67500
- const managed = this.intervalLoops.get(loopId);
67747
+ dropManagedIntervalLoop(managedKey) {
67748
+ const managed = this.intervalLoops.get(managedKey);
67501
67749
  if (!managed) {
67502
67750
  return;
67503
67751
  }
@@ -67505,30 +67753,34 @@ class ManagedLoopController {
67505
67753
  clearTimeout(managed.timer);
67506
67754
  this.loopTimers.delete(managed.timer);
67507
67755
  }
67508
- this.intervalLoops.delete(loopId);
67756
+ this.intervalLoops.delete(managedKey);
67757
+ }
67758
+ async cancelManagedIntervalLoop(managedKey, managed) {
67759
+ this.dropManagedIntervalLoop(managedKey);
67760
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
67509
67761
  }
67510
- async runIntervalLoopIteration(loopId, options = {}) {
67511
- const managed = this.intervalLoops.get(loopId);
67762
+ async runIntervalLoopIteration(managedKey, options = {}) {
67763
+ const managed = this.intervalLoops.get(managedKey);
67512
67764
  if (!managed) {
67513
67765
  return;
67514
67766
  }
67515
67767
  if (!await this.isManagedLoopPersisted(managed)) {
67516
- this.dropManagedIntervalLoop(loopId);
67768
+ this.dropManagedIntervalLoop(managedKey);
67517
67769
  return;
67518
67770
  }
67519
67771
  const attemptedRuns = managed.loop.attemptedRuns + 1;
67520
67772
  const now = Date.now();
67521
67773
  const nextLoopState = this.buildNextLoopState(managed.loop, attemptedRuns, now);
67522
67774
  if (await this.deps.isSessionBusy(managed.target)) {
67523
- await this.skipLoopIteration(loopId, managed, nextLoopState, attemptedRuns, now);
67775
+ await this.skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now);
67524
67776
  return;
67525
67777
  }
67526
67778
  nextLoopState.executedRuns += 1;
67527
67779
  if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
67528
- this.dropManagedIntervalLoop(loopId);
67780
+ this.dropManagedIntervalLoop(managedKey);
67529
67781
  return;
67530
67782
  }
67531
- await this.executeLoopIteration(loopId, managed.target, nextLoopState, attemptedRuns, now, options.notifyStart !== false);
67783
+ await this.executeLoopIteration(managedKey, managed.target, nextLoopState, attemptedRuns, now, options.notifyStart !== false);
67532
67784
  }
67533
67785
  buildNextLoopState(loop, attemptedRuns, now) {
67534
67786
  return {
@@ -67538,33 +67790,35 @@ class ManagedLoopController {
67538
67790
  nextRunAt: this.computeNextManagedLoopRunAtMs(loop, now)
67539
67791
  };
67540
67792
  }
67541
- async skipLoopIteration(loopId, managed, nextLoopState, attemptedRuns, now) {
67793
+ async skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now) {
67542
67794
  nextLoopState.skippedRuns += 1;
67543
67795
  if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
67544
- this.dropManagedIntervalLoop(loopId);
67796
+ this.dropManagedIntervalLoop(managedKey);
67545
67797
  return;
67546
67798
  }
67547
67799
  if (attemptedRuns >= managed.loop.maxRuns) {
67548
- await this.cancelIntervalLoop(loopId);
67800
+ this.dropManagedIntervalLoop(managedKey);
67801
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
67549
67802
  return;
67550
67803
  }
67551
- this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
67804
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
67552
67805
  }
67553
- async executeLoopIteration(loopId, target, nextLoopState, attemptedRuns, now, notifyStart) {
67554
- await this.notifyAndEnqueueLoop(loopId, target, nextLoopState, attemptedRuns, notifyStart);
67806
+ async executeLoopIteration(managedKey, target, nextLoopState, attemptedRuns, now, notifyStart) {
67807
+ await this.notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart);
67555
67808
  if (attemptedRuns >= nextLoopState.maxRuns) {
67556
- await this.cancelIntervalLoop(loopId);
67809
+ this.dropManagedIntervalLoop(managedKey);
67810
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(target), nextLoopState.id);
67557
67811
  return;
67558
67812
  }
67559
- this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
67813
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
67560
67814
  }
67561
- async notifyAndEnqueueLoop(loopId, target, nextLoopState, attemptedRuns, notifyStart) {
67815
+ async notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart) {
67562
67816
  if (notifyStart) {
67563
67817
  await this.deps.surfaceRuntime.notifyManagedLoopStart(target, nextLoopState);
67564
67818
  }
67565
67819
  const promptText = await this.deps.surfaceRuntime.buildManagedLoopPrompt(target.agentId, nextLoopState);
67566
67820
  const { result } = this.deps.enqueuePrompt(target, promptText, {
67567
- observerId: `loop:${loopId}:${attemptedRuns}`,
67821
+ observerId: `loop:${managedKey}:${attemptedRuns}`,
67568
67822
  onUpdate: async () => {
67569
67823
  return;
67570
67824
  }
@@ -67575,8 +67829,8 @@ class ManagedLoopController {
67575
67829
  }
67576
67830
  });
67577
67831
  }
67578
- scheduleIntervalLoopTimer(loopId, delayMs) {
67579
- const managed = this.intervalLoops.get(loopId);
67832
+ scheduleIntervalLoopTimer(managedKey, delayMs) {
67833
+ const managed = this.intervalLoops.get(managedKey);
67580
67834
  if (!managed) {
67581
67835
  return;
67582
67836
  }
@@ -67586,12 +67840,12 @@ class ManagedLoopController {
67586
67840
  }
67587
67841
  const timer = setTimeout(() => {
67588
67842
  this.loopTimers.delete(timer);
67589
- const current = this.intervalLoops.get(loopId);
67843
+ const current = this.intervalLoops.get(managedKey);
67590
67844
  if (!current) {
67591
67845
  return;
67592
67846
  }
67593
67847
  current.timer = undefined;
67594
- this.runIntervalLoopIteration(loopId, { notifyStart: true }).catch((error) => {
67848
+ this.runIntervalLoopIteration(managedKey, { notifyStart: true }).catch((error) => {
67595
67849
  if (!this.deps.shouldSuppressShutdownError(error)) {
67596
67850
  console.error("loop execution failed", error);
67597
67851
  }
@@ -67608,6 +67862,9 @@ class ManagedLoopController {
67608
67862
  managed.loop = nextLoopState;
67609
67863
  return true;
67610
67864
  }
67865
+ buildManagedLoopKey(sessionKey, loopId) {
67866
+ return `${sessionKey}::${loopId}`;
67867
+ }
67611
67868
  computeNextManagedLoopRunAtMs(loop, nowMs) {
67612
67869
  if (loop.kind === "calendar") {
67613
67870
  return computeNextCalendarLoopRunAtMs({
@@ -67624,6 +67881,8 @@ class ManagedLoopController {
67624
67881
  }
67625
67882
 
67626
67883
  // src/agents/managed-queue-controller.ts
67884
+ var QUEUE_MESSAGE_TOOL_FINAL_POLL_MS = 250;
67885
+
67627
67886
  class ManagedQueueController {
67628
67887
  deps;
67629
67888
  queuedItems = new Map;
@@ -67646,7 +67905,7 @@ class ManagedQueueController {
67646
67905
  }
67647
67906
  async clearQueuedPrompts(target) {
67648
67907
  const clearedStored = await this.deps.sessionState.clearPendingQueuedItemsForSessionKey(target.sessionKey);
67649
- this.deps.queue.clearPendingByIds(clearedStored.map((item) => item.id));
67908
+ this.deps.queue.clearPendingByIdsForKey(target.sessionKey, clearedStored.map((item) => item.id));
67650
67909
  return clearedStored.length;
67651
67910
  }
67652
67911
  enqueuePrompt(target, prompt, callbacks) {
@@ -67684,8 +67943,8 @@ class ManagedQueueController {
67684
67943
  return;
67685
67944
  }
67686
67945
  this.setManagedQueueItem(target, item, true);
67687
- return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(item)).catch((error) => {
67688
- this.clearPendingById(item);
67946
+ return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(target, item)).catch((error) => {
67947
+ this.clearPendingById(target, item);
67689
67948
  throw error;
67690
67949
  });
67691
67950
  }
@@ -67696,25 +67955,58 @@ class ManagedQueueController {
67696
67955
  await reconciledBeforeStart;
67697
67956
  return !await this.deps.hasBlockingActiveRun(target);
67698
67957
  }
67958
+ async canStartPersistedQueueItem(target, item) {
67959
+ if (await this.deps.hasBlockingActiveRun(target)) {
67960
+ return false;
67961
+ }
67962
+ if (!await this.deps.sessionState.hasQueuedItem(target.sessionKey, item.id)) {
67963
+ this.clearPendingById(target, item);
67964
+ return false;
67965
+ }
67966
+ return true;
67967
+ }
67699
67968
  async reconcilePersistedQueueItems() {
67700
67969
  const persistedItems = await this.deps.sessionState.listQueuedItems({
67701
67970
  statuses: ["pending", "running"]
67702
67971
  });
67703
- const persistedIds = new Set(persistedItems.map((item) => item.id));
67704
- const removedIds = [...this.queuedItems.entries()].filter(([id, managed]) => !managed.persisting && !persistedIds.has(id)).map(([id]) => id);
67705
- if (removedIds.length > 0) {
67706
- this.deps.queue.clearPendingByIds(removedIds);
67707
- for (const id of removedIds) {
67708
- this.queuedItems.delete(id);
67972
+ const persistedKeys = new Set(persistedItems.map((item) => this.buildManagedQueueKey(item.sessionKey, item.id)));
67973
+ const removedManagedItems = [...this.queuedItems.entries()].filter(([key, managed]) => !managed.persisting && !persistedKeys.has(key));
67974
+ if (removedManagedItems.length > 0) {
67975
+ const removedBySession = new Map;
67976
+ for (const [key, managed] of removedManagedItems) {
67977
+ const ids = removedBySession.get(managed.target.sessionKey) ?? [];
67978
+ ids.push(managed.item.id);
67979
+ removedBySession.set(managed.target.sessionKey, ids);
67980
+ this.queuedItems.delete(key);
67981
+ }
67982
+ for (const [sessionKey, itemIds] of removedBySession.entries()) {
67983
+ this.deps.queue.clearPendingByIdsForKey(sessionKey, itemIds);
67709
67984
  }
67710
67985
  }
67711
67986
  for (const persisted of persistedItems) {
67712
- if (persisted.status !== "pending" || this.queuedItems.has(persisted.id)) {
67987
+ if (persisted.status === "running") {
67988
+ await this.clearStaleRunningQueueItem(persisted);
67989
+ continue;
67990
+ }
67991
+ if (this.queuedItems.has(this.buildManagedQueueKey(persisted.sessionKey, persisted.id))) {
67713
67992
  continue;
67714
67993
  }
67715
67994
  this.enqueuePersistedQueueItem(persisted);
67716
67995
  }
67717
67996
  }
67997
+ async clearStaleRunningQueueItem(item) {
67998
+ if (this.queuedItems.has(this.buildManagedQueueKey(item.sessionKey, item.id))) {
67999
+ return;
68000
+ }
68001
+ const target = {
68002
+ agentId: item.agentId,
68003
+ sessionKey: item.sessionKey
68004
+ };
68005
+ if (await this.deps.hasBlockingActiveRun(target)) {
68006
+ return;
68007
+ }
68008
+ await this.removeManagedQueueItem(target, item);
68009
+ }
67718
68010
  persistQueueItem(resolved, item) {
67719
68011
  return this.deps.sessionState.countPendingQueuedItemsForSessionKey(resolved.sessionKey, {
67720
68012
  excludeId: item.id
@@ -67727,21 +68019,22 @@ class ManagedQueueController {
67727
68019
  });
67728
68020
  }
67729
68021
  setManagedQueueItem(target, item, persisting) {
67730
- this.queuedItems.set(item.id, {
68022
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67731
68023
  target,
67732
68024
  item,
67733
68025
  persisting
67734
68026
  });
67735
68027
  }
67736
- markPersisted(item) {
67737
- const managed = this.queuedItems.get(item.id);
68028
+ markPersisted(target, item) {
68029
+ const key = this.buildManagedQueueKey(target.sessionKey, item.id);
68030
+ const managed = this.queuedItems.get(key);
67738
68031
  if (managed) {
67739
- this.queuedItems.set(item.id, { ...managed, persisting: false });
68032
+ this.queuedItems.set(key, { ...managed, persisting: false });
67740
68033
  }
67741
68034
  }
67742
- clearPendingById(item) {
67743
- this.deps.queue.clearPendingByIds([item.id]);
67744
- this.queuedItems.delete(item.id);
68035
+ clearPendingById(target, item) {
68036
+ this.deps.queue.clearPendingByIdsForKey(target.sessionKey, [item.id]);
68037
+ this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
67745
68038
  }
67746
68039
  async markQueueItemRunning(target, item) {
67747
68040
  if (!item) {
@@ -67754,17 +68047,18 @@ class ManagedQueueController {
67754
68047
  startedAt: now,
67755
68048
  updatedAt: now
67756
68049
  };
67757
- this.queuedItems.set(item.id, {
68050
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67758
68051
  target,
67759
68052
  item: next
67760
68053
  });
67761
68054
  await this.deps.sessionState.replaceQueuedItemIfPresent(this.deps.resolveTarget(target), next);
68055
+ return next;
67762
68056
  }
67763
68057
  async removeManagedQueueItem(target, item) {
67764
68058
  if (!item) {
67765
68059
  return;
67766
68060
  }
67767
- this.queuedItems.delete(item.id);
68061
+ this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
67768
68062
  await this.deps.sessionState.removeQueuedItem(this.deps.resolveTarget(target), item.id);
67769
68063
  }
67770
68064
  enqueuePersistedQueueItem(item) {
@@ -67772,28 +68066,43 @@ class ManagedQueueController {
67772
68066
  agentId: item.agentId,
67773
68067
  sessionKey: item.sessionKey
67774
68068
  };
67775
- this.queuedItems.set(item.id, {
68069
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67776
68070
  target,
67777
68071
  item
67778
68072
  });
67779
68073
  const queued = this.deps.queue.enqueue(item.sessionKey, async () => {
67780
- await this.markQueueItemRunning(target, item);
68074
+ const runningItem = await this.markQueueItemRunning(target, item);
67781
68075
  await this.deps.surfaceRuntime.notifyManagedQueueStart(target, item);
67782
68076
  const promptText = await this.deps.surfaceRuntime.buildManagedQueuePrompt(item.agentId, item);
67783
- return this.deps.activeRuns.executePrompt(target, promptText, {
68077
+ const result = this.deps.activeRuns.executePrompt(target, promptText, {
67784
68078
  id: `queue:${item.id}`,
67785
68079
  mode: "live",
67786
68080
  onUpdate: async () => {
67787
68081
  return;
67788
68082
  }
67789
68083
  });
68084
+ let stopWaitingForToolFinal = false;
68085
+ try {
68086
+ const outcome = await Promise.race([
68087
+ result.then((update) => ({ kind: "result", update })),
68088
+ this.waitForMessageToolFinal(target, runningItem ?? item, () => stopWaitingForToolFinal).then((seen) => ({ kind: seen ? "message-tool-final" : "result" }))
68089
+ ]);
68090
+ if (outcome.kind === "message-tool-final") {
68091
+ return this.createMessageToolFinalQueueUpdate(target, item);
68092
+ }
68093
+ return await result;
68094
+ } finally {
68095
+ stopWaitingForToolFinal = true;
68096
+ }
67790
68097
  }, {
67791
68098
  id: item.id,
67792
68099
  createdAt: item.createdAt,
67793
68100
  text: item.promptSummary,
67794
- canStart: async () => !await this.deps.hasBlockingActiveRun(target),
68101
+ canStart: async () => this.canStartPersistedQueueItem(target, item),
67795
68102
  onComplete: async (value) => {
67796
- await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
68103
+ if (!value.messageToolFinalAlreadySent && !await this.hasMessageToolFinalForQueueItem(target, item)) {
68104
+ await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
68105
+ }
67797
68106
  await this.removeManagedQueueItem(target, item);
67798
68107
  },
67799
68108
  onFailure: async (error) => {
@@ -67803,38 +68112,54 @@ class ManagedQueueController {
67803
68112
  onClear: () => this.removeManagedQueueItem(target, item)
67804
68113
  });
67805
68114
  queued.result.catch((error) => {
68115
+ if (error instanceof ClearedQueuedTaskError) {
68116
+ return;
68117
+ }
67806
68118
  if (this.deps.shouldSuppressShutdownError(error)) {
67807
68119
  return;
67808
68120
  }
67809
68121
  console.error("queued prompt execution failed", error);
67810
68122
  });
67811
68123
  }
67812
- }
67813
-
67814
- // src/agents/runner-service.ts
67815
- import { dirname as dirname12 } from "node:path";
67816
-
67817
- // src/agents/session-identity.ts
67818
- import { randomUUID as randomUUID4 } from "node:crypto";
67819
- function createSessionId() {
67820
- return randomUUID4();
67821
- }
67822
- function extractSessionId(snapshot, pattern) {
67823
- const regex = new RegExp(pattern, "ig");
67824
- let lastMatch = null;
67825
- for (;; ) {
67826
- const match = regex.exec(snapshot);
67827
- if (!match) {
67828
- break;
68124
+ async waitForMessageToolFinal(target, item, shouldStop) {
68125
+ while (!shouldStop()) {
68126
+ if (await this.hasMessageToolFinalForQueueItem(target, item)) {
68127
+ return true;
68128
+ }
68129
+ await sleep(QUEUE_MESSAGE_TOOL_FINAL_POLL_MS);
67829
68130
  }
67830
- if (!match[0]) {
67831
- break;
68131
+ return false;
68132
+ }
68133
+ async hasMessageToolFinalForQueueItem(target, item) {
68134
+ const startedAt = this.queuedItems.get(this.buildManagedQueueKey(target.sessionKey, item.id))?.item.startedAt ?? item.startedAt;
68135
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) {
68136
+ return false;
67832
68137
  }
67833
- lastMatch = match;
68138
+ const runtime = await this.deps.sessionState.getSessionRuntime(target);
68139
+ return typeof runtime.messageToolFinalReplyAt === "number" && Number.isFinite(runtime.messageToolFinalReplyAt) && runtime.messageToolFinalReplyAt >= startedAt;
68140
+ }
68141
+ createMessageToolFinalQueueUpdate(target, item) {
68142
+ const resolved = this.deps.resolveTarget(target);
68143
+ return {
68144
+ status: "completed",
68145
+ agentId: target.agentId,
68146
+ sessionKey: target.sessionKey,
68147
+ sessionName: resolved.sessionName,
68148
+ workspacePath: resolved.workspacePath,
68149
+ snapshot: "",
68150
+ fullSnapshot: "",
68151
+ initialSnapshot: "",
68152
+ messageToolFinalAlreadySent: true
68153
+ };
68154
+ }
68155
+ buildManagedQueueKey(sessionKey, itemId) {
68156
+ return `${sessionKey}::${itemId}`;
67834
68157
  }
67835
- return (lastMatch?.[1] ?? lastMatch?.[0] ?? "").trim() || null;
67836
68158
  }
67837
68159
 
68160
+ // src/agents/runner-service.ts
68161
+ import { dirname as dirname12 } from "node:path";
68162
+
67838
68163
  // src/shared/transcript-normalization.ts
67839
68164
  import { stripVTControlCharacters } from "node:util";
67840
68165
  function normalizePaneText(raw) {
@@ -68089,18 +68414,37 @@ function isTimerDrivenStatusLine(line) {
68089
68414
  return isActiveTimerStatusLine(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed);
68090
68415
  }
68091
68416
  function hasActiveTimerStatus(snapshot) {
68092
- return splitNormalizedLines(snapshot).some((line) => isActiveTimerStatusLine(line));
68417
+ return Boolean(extractLatestActiveTimerStatusLine(snapshot));
68093
68418
  }
68094
68419
  function extractLatestActiveTimerStatusLine(snapshot) {
68095
68420
  const lines = splitNormalizedLines(snapshot);
68096
68421
  for (let index = lines.length - 1;index >= 0; index -= 1) {
68097
68422
  const line = lines[index]?.trim() ?? "";
68098
- if (isActiveTimerStatusLine(line)) {
68423
+ if (isActiveTimerStatusLine(line) && isLiveTimerStatusLine(lines, index)) {
68099
68424
  return line;
68100
68425
  }
68101
68426
  }
68102
68427
  return "";
68103
68428
  }
68429
+ function isLiveTimerStatusLine(lines, timerIndex) {
68430
+ for (let index = timerIndex + 1;index < lines.length; index += 1) {
68431
+ const trimmed = lines[index]?.trim() ?? "";
68432
+ if (!trimmed) {
68433
+ continue;
68434
+ }
68435
+ if (isRunnerIdlePromptLine(trimmed) || isRunnerPromptMetadataLine(trimmed)) {
68436
+ continue;
68437
+ }
68438
+ return false;
68439
+ }
68440
+ return true;
68441
+ }
68442
+ function isRunnerIdlePromptLine(trimmed) {
68443
+ return trimmed.startsWith("› ") || trimmed === "›" || trimmed.startsWith("> ");
68444
+ }
68445
+ function isRunnerPromptMetadataLine(trimmed) {
68446
+ return /^gpt-[\w.-]+ .*·/.test(trimmed) || trimmed.startsWith("model:") || trimmed.startsWith("directory:") || trimmed.startsWith("ctx:") || trimmed.startsWith("tokens:") || trimmed.startsWith("? for shortcuts");
68447
+ }
68104
68448
  function shouldDropCodexChromeLine(line) {
68105
68449
  const trimmed = line.trim();
68106
68450
  if (!trimmed) {
@@ -68317,6 +68661,11 @@ function deriveInteractionText(initialSnapshot, currentSnapshot) {
68317
68661
  const current = cleanInteractionSnapshot(currentSnapshot);
68318
68662
  return extractScrolledAppend(previous, current) || diffText(previous, current);
68319
68663
  }
68664
+ function deriveInteractionDiffText(initialSnapshot, currentSnapshot) {
68665
+ const previous = cleanInteractionSnapshot(initialSnapshot);
68666
+ const current = cleanInteractionSnapshot(currentSnapshot);
68667
+ return diffText(previous, current);
68668
+ }
68320
68669
  function appendInteractionText(currentBody, nextDelta) {
68321
68670
  const trimmedCurrent = currentBody.trim();
68322
68671
  const trimmedDelta = nextDelta.trim();
@@ -68773,6 +69122,14 @@ class TmuxSubmitUnconfirmedError extends Error {
68773
69122
  }
68774
69123
  }
68775
69124
  async function submitTmuxSessionInput(params) {
69125
+ if (params.trustPrompt) {
69126
+ await acceptTmuxTrustPromptIfPresent({
69127
+ tmux: params.tmux,
69128
+ sessionName: params.sessionName,
69129
+ captureLines: params.trustPrompt.captureLines,
69130
+ startupDelayMs: params.trustPrompt.startupDelayMs
69131
+ });
69132
+ }
68776
69133
  const prePasteState = await params.tmux.getPaneState(params.sessionName);
68777
69134
  const captureLines = estimatePasteCaptureLines(params.text);
68778
69135
  const prePasteSnapshot = normalizePaneText(await params.tmux.capturePane(params.sessionName, captureLines));
@@ -68826,6 +69183,12 @@ async function submitTmuxSessionInput(params) {
68826
69183
  throw new TmuxSubmitUnconfirmedError;
68827
69184
  }
68828
69185
  async function captureTmuxSessionIdentity(params) {
69186
+ await acceptTmuxTrustPromptIfPresent({
69187
+ tmux: params.tmux,
69188
+ sessionName: params.sessionName,
69189
+ captureLines: params.captureLines,
69190
+ startupDelayMs: params.timeoutMs
69191
+ });
68829
69192
  let statusSubmission = await submitTmuxSessionInput({
68830
69193
  tmux: params.tmux,
68831
69194
  sessionName: params.sessionName,
@@ -68849,7 +69212,7 @@ async function captureTmuxSessionIdentity(params) {
68849
69212
  throw error;
68850
69213
  }
68851
69214
  if (tmuxPaneHasTrustPrompt(snapshot)) {
68852
- await dismissTrustPrompt({
69215
+ await acceptTrustPrompt({
68853
69216
  tmux: params.tmux,
68854
69217
  sessionName: params.sessionName,
68855
69218
  captureLines: params.captureLines
@@ -68864,7 +69227,7 @@ async function captureTmuxSessionIdentity(params) {
68864
69227
  });
68865
69228
  continue;
68866
69229
  }
68867
- const sessionId = extractSessionId(deriveSessionIdentityText(statusSubmission.submittedSnapshot, snapshot), params.pattern);
69230
+ const sessionId = extractSessionIdFromCaptureCandidates(deriveSessionIdCaptureCandidates(statusSubmission.submittedSnapshot, snapshot, params.statusCommand), params.pattern);
68868
69231
  if (sessionId) {
68869
69232
  await waitForTmuxPaneSettle({
68870
69233
  tmux: params.tmux,
@@ -68879,12 +69242,34 @@ async function captureTmuxSessionIdentity(params) {
68879
69242
  }
68880
69243
  return null;
68881
69244
  }
68882
- function deriveSessionIdentityText(submittedSnapshot, snapshot) {
69245
+ function deriveSessionIdCaptureCandidates(submittedSnapshot, snapshot, statusCommand) {
68883
69246
  const rawSubmitted = normalizePaneText(submittedSnapshot);
68884
69247
  const rawSnapshot = normalizePaneText(snapshot);
68885
- return extractScrolledAppend(rawSubmitted, rawSnapshot) || deriveInteractionText(submittedSnapshot, snapshot);
69248
+ return [
69249
+ extractScrolledAppend(rawSubmitted, rawSnapshot),
69250
+ deriveInteractionText(submittedSnapshot, snapshot),
69251
+ deriveInteractionDiffText(submittedSnapshot, snapshot),
69252
+ rawSnapshot,
69253
+ extractStatusCommandTail(rawSnapshot, statusCommand)
69254
+ ].filter((candidate, index, candidates) => candidate && candidates.indexOf(candidate) === index);
69255
+ }
69256
+ function extractStatusCommandTail(snapshot, statusCommand) {
69257
+ const lastStatusIndex = snapshot.lastIndexOf(statusCommand);
69258
+ if (lastStatusIndex < 0) {
69259
+ return "";
69260
+ }
69261
+ return snapshot.slice(lastStatusIndex);
69262
+ }
69263
+ function extractSessionIdFromCaptureCandidates(candidates, pattern) {
69264
+ for (const candidate of candidates) {
69265
+ const sessionId = parseRunnerSessionId(candidate, pattern);
69266
+ if (sessionId) {
69267
+ return sessionId;
69268
+ }
69269
+ }
69270
+ return null;
68886
69271
  }
68887
- async function dismissTmuxTrustPromptIfPresent(params) {
69272
+ async function acceptTmuxTrustPromptIfPresent(params) {
68888
69273
  const deadline = Date.now() + Math.max(TRUST_PROMPT_MAX_WAIT_MS, params.startupDelayMs);
68889
69274
  while (Date.now() <= deadline) {
68890
69275
  let snapshot = "";
@@ -68907,7 +69292,7 @@ async function dismissTmuxTrustPromptIfPresent(params) {
68907
69292
  if (!tmuxPaneHasTrustPrompt(snapshot)) {
68908
69293
  return;
68909
69294
  }
68910
- await dismissTrustPrompt({
69295
+ await acceptTrustPrompt({
68911
69296
  tmux: params.tmux,
68912
69297
  sessionName: params.sessionName,
68913
69298
  captureLines: params.captureLines
@@ -68939,7 +69324,7 @@ async function waitForTmuxSessionBootstrap(params) {
68939
69324
  if (snapshot) {
68940
69325
  lastSnapshot = snapshot;
68941
69326
  if (params.trustWorkspace && tmuxPaneHasTrustPrompt(snapshot)) {
68942
- await dismissTrustPrompt({
69327
+ await acceptTrustPrompt({
68943
69328
  tmux: params.tmux,
68944
69329
  sessionName: params.sessionName,
68945
69330
  captureLines: params.captureLines
@@ -68956,7 +69341,7 @@ async function waitForTmuxSessionBootstrap(params) {
68956
69341
  };
68957
69342
  }
68958
69343
  }
68959
- if (readyRegex && !readyRegex.test(snapshot)) {
69344
+ if (readyRegex && !snapshotHasActiveReadyPattern(snapshot, readyRegex)) {
68960
69345
  await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
68961
69346
  continue;
68962
69347
  }
@@ -68972,7 +69357,7 @@ async function waitForTmuxSessionBootstrap(params) {
68972
69357
  snapshot: lastSnapshot
68973
69358
  };
68974
69359
  }
68975
- async function dismissTrustPrompt(params) {
69360
+ async function acceptTrustPrompt(params) {
68976
69361
  await params.tmux.sendKey(params.sessionName, "Enter");
68977
69362
  const deadline = Date.now() + TRUST_PROMPT_MAX_WAIT_MS;
68978
69363
  while (Date.now() <= deadline) {
@@ -69190,6 +69575,29 @@ function buildBootstrapSessionLostError(sessionName, error) {
69190
69575
  function arePaneStatesEqual(left, right) {
69191
69576
  return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
69192
69577
  }
69578
+ function snapshotHasActiveReadyPattern(snapshot, readyRegex) {
69579
+ const lines = splitNormalizedLines(snapshot);
69580
+ let readyLineIndex = -1;
69581
+ for (let index = 0;index < lines.length; index += 1) {
69582
+ if (readyRegex.test(lines[index] ?? "")) {
69583
+ readyLineIndex = index;
69584
+ }
69585
+ }
69586
+ if (readyLineIndex < 0) {
69587
+ return readyRegex.test(snapshot);
69588
+ }
69589
+ for (const rawLine of lines.slice(readyLineIndex + 1)) {
69590
+ const line = rawLine.trim();
69591
+ if (!line || isPromptMetadataLine(line)) {
69592
+ continue;
69593
+ }
69594
+ return false;
69595
+ }
69596
+ return true;
69597
+ }
69598
+ function isPromptMetadataLine(line) {
69599
+ return /^gpt-[\w.-]+\b/i.test(line) || /^model:\s*/i.test(line) || /^session:\s*/i.test(line);
69600
+ }
69193
69601
  function looksLikeClaudeTrustPrompt(snapshot) {
69194
69602
  return snapshot.includes("Quick safety check:") && snapshot.includes("Yes, I trust this folder") || snapshot.includes("Enter to confirm · Esc to cancel");
69195
69603
  }
@@ -69364,8 +69772,10 @@ var TMUX_DUPLICATE_SESSION_PATTERN = /duplicate session:/i;
69364
69772
  var TMUX_TRANSIENT_TARGET_PATTERN = /(?:no current target|can't find pane|can't find window|no such pane|no such window|tmux pane state unavailable)/i;
69365
69773
  var SESSION_READY_CAPTURE_RETRY_COUNT = 5;
69366
69774
  var SESSION_READY_CAPTURE_RETRY_DELAY_MS = 100;
69367
- var TRIGGER_NEW_SESSION_CAPTURE_RETRY_COUNT = 3;
69775
+ var STARTUP_SESSION_ID_CAPTURE_RETRY_COUNT = 2;
69776
+ var STARTUP_SESSION_ID_CAPTURE_RETRY_DELAY_MS = 500;
69368
69777
  var SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS = 15000;
69778
+ var PRESERVED_SESSION_ID_RETRY_MESSAGE = "The previous runner session could not be resumed. clisbot preserved the stored session id instead of opening a new conversation automatically. Use `/new` if you want to trigger a new runner conversation, then resend the prompt.";
69369
69779
  function summarizeSnapshot(snapshot) {
69370
69780
  const compact = snapshot.split(`
69371
69781
  `).map((line) => line.trim()).filter(Boolean).join(" ").slice(0, 220);
@@ -69396,19 +69806,24 @@ function isFreshStartRetryablePromptDeliveryError(error) {
69396
69806
  function isRetryableFreshStartFault(error) {
69397
69807
  return isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error) || isFreshStartRetryablePromptDeliveryError(error);
69398
69808
  }
69809
+ function canRestartWithStoredSessionId(resolved) {
69810
+ return resolved.runner.sessionId.resume.mode === "command" || resolved.runner.sessionId.create.mode === "explicit";
69811
+ }
69399
69812
 
69400
69813
  class RunnerService {
69401
69814
  loadedConfig;
69402
69815
  tmux;
69403
69816
  sessionState;
69404
69817
  resolveTarget;
69818
+ sessionMapping;
69405
69819
  cleanupInFlight = false;
69406
69820
  sessionIdentityCaptureRetryAt = new Map;
69407
- constructor(loadedConfig, tmux, sessionState, resolveTarget) {
69821
+ constructor(loadedConfig, tmux, sessionState, resolveTarget, sessionMapping = new SessionMapping(sessionState)) {
69408
69822
  this.loadedConfig = loadedConfig;
69409
69823
  this.tmux = tmux;
69410
69824
  this.sessionState = sessionState;
69411
69825
  this.resolveTarget = resolveTarget;
69826
+ this.sessionMapping = sessionMapping;
69412
69827
  }
69413
69828
  async mapSessionError(error, sessionName, action, lastSnapshot = "") {
69414
69829
  if (isRecoverableStartupSessionLoss(error)) {
@@ -69452,24 +69867,21 @@ class RunnerService {
69452
69867
  args: args.map((value) => applyTemplate(value, values))
69453
69868
  };
69454
69869
  }
69455
- async syncSessionIdentity(resolved) {
69456
- const existing = await this.sessionState.getEntry(resolved.sessionKey);
69870
+ async syncStoredSessionIdForResolvedTarget(resolved) {
69871
+ const existing = await this.sessionMapping.get(resolved.sessionKey);
69457
69872
  if (existing?.sessionId) {
69458
- this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
69459
- return this.sessionState.touchSessionEntry(resolved, {
69460
- sessionId: existing.sessionId,
69461
- runnerCommand: resolved.runner.command
69462
- });
69873
+ await this.persistStoredSessionIdBestEffort(resolved, existing.sessionId, resolved.runner.command);
69874
+ return existing;
69463
69875
  }
69464
69876
  const retryAt = this.sessionIdentityCaptureRetryAt.get(resolved.sessionKey) ?? 0;
69465
69877
  if (retryAt > Date.now()) {
69466
- return this.sessionState.touchSessionEntry(resolved, {
69878
+ return this.sessionMapping.touch(resolved, {
69467
69879
  runnerCommand: resolved.runner.command
69468
69880
  });
69469
69881
  }
69470
69882
  let sessionId;
69471
69883
  try {
69472
- sessionId = await this.captureSessionIdentity(resolved);
69884
+ sessionId = await this.captureSessionIdFromRunner(resolved);
69473
69885
  } catch (error) {
69474
69886
  if (isFreshStartRetryablePromptDeliveryError(error)) {
69475
69887
  this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
@@ -69477,16 +69889,36 @@ class RunnerService {
69477
69889
  throw error;
69478
69890
  }
69479
69891
  if (sessionId) {
69480
- this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
69481
- } else {
69482
- this.sessionIdentityCaptureRetryAt.set(resolved.sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
69892
+ await this.persistStoredSessionIdBestEffort(resolved, sessionId, resolved.runner.command);
69893
+ return {
69894
+ sessionId
69895
+ };
69483
69896
  }
69484
- return this.sessionState.touchSessionEntry(resolved, {
69485
- sessionId,
69897
+ this.deferStoredSessionIdCapture(resolved.sessionKey);
69898
+ return this.sessionMapping.touch(resolved, {
69486
69899
  runnerCommand: resolved.runner.command
69487
69900
  });
69488
69901
  }
69489
- async captureSessionIdentity(resolved, options = {}) {
69902
+ persistStoredSessionId(resolved, sessionId, runnerCommand = resolved.runner.command) {
69903
+ this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
69904
+ return this.sessionMapping.setActive(resolved, {
69905
+ sessionId,
69906
+ runnerCommand
69907
+ });
69908
+ }
69909
+ async persistStoredSessionIdBestEffort(resolved, sessionId, runnerCommand = resolved.runner.command) {
69910
+ try {
69911
+ await this.persistStoredSessionId(resolved, sessionId, runnerCommand);
69912
+ return true;
69913
+ } catch (error) {
69914
+ this.warnStartupSessionIdentityDegraded(resolved, error);
69915
+ return false;
69916
+ }
69917
+ }
69918
+ deferStoredSessionIdCapture(sessionKey) {
69919
+ this.sessionIdentityCaptureRetryAt.set(sessionKey, Date.now() + SESSION_ID_CAPTURE_FAILURE_COOLDOWN_MS);
69920
+ }
69921
+ async captureSessionIdFromRunner(resolved, options = {}) {
69490
69922
  const capture = resolved.runner.sessionId.capture;
69491
69923
  if (capture.mode !== "status-command" && !options.forceStatusCommand) {
69492
69924
  return null;
@@ -69514,12 +69946,39 @@ class RunnerService {
69514
69946
  remainingFreshRetries: remainingFreshRetries - 1
69515
69947
  });
69516
69948
  }
69517
- async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
69949
+ async retryAfterStartupFault(target, resolved, error, remainingFreshRetries, allowFreshResumeFallback) {
69950
+ if (allowFreshResumeFallback) {
69951
+ const resumedFresh = await this.retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries);
69952
+ if (resumedFresh) {
69953
+ return resumedFresh;
69954
+ }
69955
+ }
69518
69956
  if (!isRetryableFreshStartFault(error)) {
69519
69957
  return null;
69520
69958
  }
69521
69959
  return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
69522
69960
  }
69961
+ async retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries) {
69962
+ if (!isRecoverableStartupSessionLoss(error)) {
69963
+ return null;
69964
+ }
69965
+ if (resolved.runner.sessionId.resume.mode !== "command" || resolved.runner.sessionId.create.mode !== "runner") {
69966
+ return null;
69967
+ }
69968
+ const existing = await this.sessionMapping.get(resolved.sessionKey);
69969
+ if (!existing?.sessionId) {
69970
+ return null;
69971
+ }
69972
+ const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69973
+ if (!exitRecord || exitRecord.exitCode === 0) {
69974
+ return null;
69975
+ }
69976
+ console.log(`clisbot preserved stored sessionId after failed runner resume startup ${resolved.sessionName}`);
69977
+ await this.sessionMapping.touch(resolved, {
69978
+ runnerCommand: resolved.runner.command
69979
+ });
69980
+ throw new Error(PRESERVED_SESSION_ID_RETRY_MESSAGE);
69981
+ }
69523
69982
  async retryAfterStartupTimeout(target, resolved, remainingFreshRetries) {
69524
69983
  return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
69525
69984
  }
@@ -69547,7 +70006,7 @@ class RunnerService {
69547
70006
  try {
69548
70007
  const snapshot = await this.captureSessionSnapshot(resolved);
69549
70008
  if (tmuxPaneHasTrustPrompt(snapshot)) {
69550
- await this.dismissVisibleTrustPrompt(resolved);
70009
+ await this.acceptVisibleWorkspaceTrustPrompt(resolved);
69551
70010
  continue;
69552
70011
  }
69553
70012
  return;
@@ -69610,18 +70069,16 @@ class RunnerService {
69610
70069
  await ensureDir2(resolved.workspacePath);
69611
70070
  await ensureDir2(dirname12(this.loadedConfig.raw.tmux.socketPath));
69612
70071
  await ensureRunnerExitRecordDir(this.loadedConfig.stateDir, resolved.sessionName);
69613
- const existing = await this.sessionState.getEntry(resolved.sessionKey);
70072
+ const preparedMapping = await this.sessionMapping.prepareStartup(resolved);
69614
70073
  const serverRunning = await this.tmux.isServerRunning();
69615
70074
  if (serverRunning && await this.tmux.hasSession(resolved.sessionName)) {
69616
70075
  logLatencyDebug("ensure-session-ready-existing-session", timingContext, {
69617
- hasStoredSessionId: Boolean(existing?.sessionId)
70076
+ hasStoredSessionId: Boolean(preparedMapping.storedSessionId)
69618
70077
  });
69619
70078
  try {
69620
70079
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69621
- await this.sessionState.touchSessionEntry(resolved, {
69622
- sessionId: existing?.sessionId,
69623
- runnerCommand: resolved.runner.command
69624
- });
70080
+ await this.acceptWorkspaceTrustPromptIfPresent(resolved);
70081
+ await this.syncStoredSessionIdForResolvedTarget(resolved);
69625
70082
  } catch (error) {
69626
70083
  throw await this.mapSessionError(error, resolved.sessionName, "during startup");
69627
70084
  }
@@ -69634,10 +70091,10 @@ class RunnerService {
69634
70091
  if (!resolved.session.createIfMissing) {
69635
70092
  throw new Error(`tmux session "${resolved.sessionName}" does not exist`);
69636
70093
  }
69637
- const startupSessionId = existing?.sessionId || (resolved.runner.sessionId.create.mode === "explicit" ? createSessionId() : "");
69638
- const resumingExistingSession = Boolean(existing?.sessionId);
70094
+ const storedOrExplicitSessionId = preparedMapping.sessionId ?? "";
70095
+ const resumingExistingSession = preparedMapping.resume;
69639
70096
  const runnerLaunch = this.buildRunnerArgs(resolved, {
69640
- sessionId: startupSessionId || undefined,
70097
+ sessionId: storedOrExplicitSessionId || undefined,
69641
70098
  resume: resumingExistingSession
69642
70099
  });
69643
70100
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
@@ -69665,7 +70122,7 @@ class RunnerService {
69665
70122
  logLatencyDebug("ensure-session-ready-new-session", timingContext, {
69666
70123
  startupDelayMs: resolved.runner.startupDelayMs,
69667
70124
  resumingExistingSession,
69668
- hasStoredSessionId: Boolean(existing?.sessionId)
70125
+ hasStoredSessionId: Boolean(preparedMapping.storedSessionId)
69669
70126
  });
69670
70127
  const bootstrapResult = await waitForTmuxSessionBootstrap({
69671
70128
  tmux: this.tmux,
@@ -69687,11 +70144,11 @@ class RunnerService {
69687
70144
  await this.abortUnreadySession(resolved, `Runner session "${resolved.sessionName}" did not reach the configured ready state within ${resolved.runner.startupDelayMs}ms.`, bootstrapResult.snapshot);
69688
70145
  }
69689
70146
  await this.finalizeSessionStartup(resolved, {
69690
- startupSessionId,
70147
+ storedOrExplicitSessionId,
69691
70148
  runnerCommand: runnerLaunch.command
69692
70149
  });
69693
70150
  } catch (error) {
69694
- const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries);
70151
+ const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries, options.allowFreshRetry !== false);
69695
70152
  if (retried) {
69696
70153
  return retried;
69697
70154
  }
@@ -69704,25 +70161,48 @@ class RunnerService {
69704
70161
  return resolved;
69705
70162
  }
69706
70163
  async finalizeSessionStartup(resolved, params) {
69707
- await this.dismissTrustPrompt(resolved);
70164
+ await this.acceptWorkspaceTrustPromptIfPresent(resolved);
69708
70165
  await this.verifySessionReady(resolved);
69709
- if (params.startupSessionId) {
69710
- await this.sessionState.touchSessionEntry(resolved, {
69711
- sessionId: params.startupSessionId,
69712
- runnerCommand: params.runnerCommand
69713
- });
70166
+ if (params.storedOrExplicitSessionId) {
70167
+ await this.persistStoredSessionIdBestEffort(resolved, params.storedOrExplicitSessionId, params.runnerCommand);
69714
70168
  return;
69715
70169
  }
69716
- await this.syncSessionIdentity(resolved);
70170
+ const entry = await this.syncStoredSessionIdForResolvedTarget(resolved);
70171
+ if (entry?.sessionId) {
70172
+ return;
70173
+ }
70174
+ await this.retryMissingStoredSessionIdAfterStartup(resolved);
69717
70175
  }
69718
- async dismissTrustPrompt(resolved) {
70176
+ warnStartupSessionIdentityDegraded(resolved, error) {
70177
+ console.warn(`clisbot could not persist or confirm a durable sessionId after startup for ${resolved.sessionName}; continuing without resumable state`, error);
70178
+ }
70179
+ async retryMissingStoredSessionIdAfterStartup(resolved) {
70180
+ for (let attempt = 0;attempt < STARTUP_SESSION_ID_CAPTURE_RETRY_COUNT; attempt += 1) {
70181
+ await sleep(STARTUP_SESSION_ID_CAPTURE_RETRY_DELAY_MS);
70182
+ let sessionId = null;
70183
+ try {
70184
+ sessionId = await this.captureSessionIdFromRunner(resolved);
70185
+ } catch (error) {
70186
+ if (isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error) || isFreshStartRetryablePromptDeliveryError(error)) {
70187
+ continue;
70188
+ }
70189
+ return;
70190
+ }
70191
+ if (!sessionId) {
70192
+ continue;
70193
+ }
70194
+ await this.persistStoredSessionIdBestEffort(resolved, sessionId);
70195
+ return;
70196
+ }
70197
+ }
70198
+ async acceptWorkspaceTrustPromptIfPresent(resolved) {
69719
70199
  if (!resolved.runner.trustWorkspace) {
69720
70200
  return;
69721
70201
  }
69722
- await this.dismissVisibleTrustPrompt(resolved);
70202
+ await this.acceptVisibleWorkspaceTrustPrompt(resolved);
69723
70203
  }
69724
- async dismissVisibleTrustPrompt(resolved) {
69725
- await dismissTmuxTrustPromptIfPresent({
70204
+ async acceptVisibleWorkspaceTrustPrompt(resolved) {
70205
+ await acceptTmuxTrustPromptIfPresent({
69726
70206
  tmux: this.tmux,
69727
70207
  sessionName: resolved.sessionName,
69728
70208
  captureLines: resolved.stream.captureLines,
@@ -69765,8 +70245,8 @@ class RunnerService {
69765
70245
  }
69766
70246
  async reopenRunContext(target, timingContext) {
69767
70247
  const resolved = this.resolveTarget(target);
69768
- const existing = await this.sessionState.getEntry(resolved.sessionKey);
69769
- if (!existing?.sessionId || resolved.runner.sessionId.resume.mode !== "command") {
70248
+ const existing = await this.sessionMapping.get(resolved.sessionKey);
70249
+ if (!existing?.sessionId || !canRestartWithStoredSessionId(resolved)) {
69770
70250
  throw new Error(`Runner session "${resolved.sessionName}" cannot reopen the same conversation context.`);
69771
70251
  }
69772
70252
  return this.ensureRunnerReady(target, { allowFreshRetryBeforePrompt: false, timingContext });
@@ -69777,7 +70257,9 @@ class RunnerService {
69777
70257
  return;
69778
70258
  });
69779
70259
  console.log(`clisbot clearing stored sessionId for explicit fresh session ${resolved.sessionName}`);
69780
- await this.sessionState.clearSessionIdEntry(resolved, { runnerCommand: resolved.runner.command });
70260
+ await this.sessionMapping.clearActive(resolved, {
70261
+ runnerCommand: resolved.runner.command
70262
+ });
69781
70263
  return this.ensureRunnerReady(target, {
69782
70264
  allowFreshRetryBeforePrompt: false,
69783
70265
  timingContext
@@ -69799,26 +70281,37 @@ class RunnerService {
69799
70281
  });
69800
70282
  }
69801
70283
  async triggerNewSessionInLiveRunner(resolved) {
69802
- const oldSessionId = (await this.sessionState.getEntry(resolved.sessionKey))?.sessionId;
70284
+ const oldSessionId = (await this.sessionMapping.get(resolved.sessionKey))?.sessionId;
69803
70285
  const command = this.resolveNewSessionCommand(resolved);
69804
- let sessionId = null;
69805
- for (let attempt = 0;attempt < 2; attempt += 1) {
70286
+ await this.acceptWorkspaceTrustPromptIfPresent(resolved);
70287
+ let submitUnconfirmedError = null;
70288
+ try {
69806
70289
  await this.submitNewSessionCommand(resolved, command);
69807
- sessionId = await this.captureNewSessionIdentityAfterTrigger(resolved, oldSessionId);
69808
- if (sessionId) {
69809
- break;
70290
+ } catch (error) {
70291
+ if (error instanceof TmuxSubmitUnconfirmedError) {
70292
+ submitUnconfirmedError = error;
70293
+ } else {
70294
+ throw error;
69810
70295
  }
69811
70296
  }
70297
+ const sessionId = await this.captureNewSessionIdentityAfterTrigger(resolved, oldSessionId);
69812
70298
  if (!sessionId) {
69813
- await this.clearSessionIdAfterNewSessionFailure(resolved, command);
69814
- }
69815
- await this.sessionState.touchSessionEntry(resolved, {
69816
- sessionId,
69817
- runnerCommand: resolved.runner.command,
69818
- runtime: {
69819
- state: "idle"
70299
+ if (submitUnconfirmedError) {
70300
+ throw submitUnconfirmedError;
69820
70301
  }
69821
- });
70302
+ this.throwNewSessionCaptureFailure(command, oldSessionId);
70303
+ }
70304
+ try {
70305
+ await this.sessionMapping.setActive(resolved, {
70306
+ sessionId,
70307
+ runnerCommand: resolved.runner.command,
70308
+ runtime: {
70309
+ state: "idle"
70310
+ }
70311
+ });
70312
+ } catch (error) {
70313
+ this.throwNewSessionPersistFailure(command, sessionId, error);
70314
+ }
69822
70315
  return {
69823
70316
  agentId: resolved.agentId,
69824
70317
  sessionKey: resolved.sessionKey,
@@ -69831,7 +70324,7 @@ class RunnerService {
69831
70324
  }
69832
70325
  async restartRunnerWithFreshSessionIdForNewCommand(target) {
69833
70326
  const { resolved } = await this.restartRunnerWithFreshSessionId(target);
69834
- const entry = await this.sessionState.getEntry(resolved.sessionKey);
70327
+ const entry = await this.sessionMapping.get(resolved.sessionKey);
69835
70328
  return {
69836
70329
  agentId: resolved.agentId,
69837
70330
  sessionKey: resolved.sessionKey,
@@ -69852,29 +70345,34 @@ class RunnerService {
69852
70345
  });
69853
70346
  }
69854
70347
  async captureNewSessionIdentityAfterTrigger(resolved, oldSessionId) {
69855
- for (let attempt = 0;attempt < TRIGGER_NEW_SESSION_CAPTURE_RETRY_COUNT; attempt += 1) {
69856
- const sessionId = await this.captureSessionIdentity(resolved, {
70348
+ for (let attempt = 0;attempt < SESSION_READY_CAPTURE_RETRY_COUNT; attempt += 1) {
70349
+ const sessionId = await this.captureSessionIdFromRunner(resolved, {
69857
70350
  forceStatusCommand: true
69858
70351
  });
69859
70352
  if (sessionId && sessionId !== oldSessionId) {
69860
70353
  return sessionId;
69861
70354
  }
69862
- if (attempt < TRIGGER_NEW_SESSION_CAPTURE_RETRY_COUNT - 1) {
70355
+ if (attempt < SESSION_READY_CAPTURE_RETRY_COUNT - 1) {
69863
70356
  await sleep(SESSION_READY_CAPTURE_RETRY_DELAY_MS);
69864
70357
  }
69865
70358
  }
69866
70359
  return null;
69867
70360
  }
69868
- async clearSessionIdAfterNewSessionFailure(resolved, command) {
69869
- console.log(`clisbot clearing stored sessionId after ${command} because status capture returned no id for ${resolved.sessionName}`);
69870
- await this.sessionState.clearSessionIdEntry(resolved, {
69871
- runnerCommand: resolved.runner.command
70361
+ throwNewSessionCaptureFailure(command, oldSessionId) {
70362
+ console.log(`clisbot preserved the previous stored sessionId after ${command} because status capture returned no id`);
70363
+ throw new Error(oldSessionId ? `${command} completed, but clisbot could not confirm the rotated session id. The previous stored session id was preserved instead of being cleared automatically.` : `${command} completed, but clisbot could not capture a new session id from the runner status command.`);
70364
+ }
70365
+ throwNewSessionPersistFailure(command, sessionId, error) {
70366
+ console.error(`clisbot failed to persist rotated sessionId after ${command}`, {
70367
+ sessionId,
70368
+ error
69872
70369
  });
69873
- throw new Error(`${command} completed, but clisbot could not capture a new session id from the runner status command.`);
70370
+ const details = error instanceof Error && error.message.trim() ? ` Persist error: ${error.message.trim()}` : "";
70371
+ throw new Error(`${command} completed and clisbot captured session id ${sessionId}, but could not persist it. The durable session mapping was left unchanged.${details}`);
69874
70372
  }
69875
70373
  async killRunnerAndPreserveSessionId(resolved) {
69876
70374
  await this.tmux.killSession(resolved.sessionName);
69877
- await this.sessionState.touchSessionEntry(resolved, {
70375
+ await this.sessionMapping.touch(resolved, {
69878
70376
  runnerCommand: resolved.runner.command
69879
70377
  });
69880
70378
  }
@@ -69892,7 +70390,6 @@ class RunnerService {
69892
70390
  snapshot: ""
69893
70391
  };
69894
70392
  }
69895
- await this.sessionState.touchSessionEntry(resolved);
69896
70393
  try {
69897
70394
  return {
69898
70395
  agentId: resolved.agentId,
@@ -69918,14 +70415,13 @@ class RunnerService {
69918
70415
  const resolved = this.resolveTarget(target);
69919
70416
  const existed = await this.tmux.hasSession(resolved.sessionName);
69920
70417
  if (existed) {
69921
- await this.sessionState.touchSessionEntry(resolved, {
70418
+ await this.sessionMapping.touch(resolved, {
69922
70419
  runtime: {
69923
70420
  state: "idle"
69924
70421
  }
69925
70422
  });
69926
70423
  try {
69927
70424
  await this.tmux.sendKey(resolved.sessionName, "Escape");
69928
- await sleep(150);
69929
70425
  } catch {}
69930
70426
  }
69931
70427
  return {
@@ -69941,7 +70437,7 @@ class RunnerService {
69941
70437
  const existed = await this.tmux.hasSession(resolved.sessionName);
69942
70438
  if (existed) {
69943
70439
  await this.tmux.sendKey(resolved.sessionName, "Enter");
69944
- await this.sessionState.touchSessionEntry(resolved);
70440
+ await this.sessionMapping.touch(resolved);
69945
70441
  }
69946
70442
  return {
69947
70443
  agentId: resolved.agentId,
@@ -69976,6 +70472,7 @@ class RunnerService {
69976
70472
  if (!await this.tmux.hasSession(resolved.sessionName)) {
69977
70473
  throw new Error(`tmux session "${resolved.sessionName}" does not exist`);
69978
70474
  }
70475
+ await this.acceptWorkspaceTrustPromptIfPresent(resolved);
69979
70476
  await submitTmuxSessionInput({
69980
70477
  tmux: this.tmux,
69981
70478
  sessionName: resolved.sessionName,
@@ -69983,7 +70480,7 @@ class RunnerService {
69983
70480
  promptSubmitDelayMs: resolved.runner.promptSubmitDelayMs,
69984
70481
  timingContext: undefined
69985
70482
  });
69986
- await this.sessionState.touchSessionEntry(resolved);
70483
+ await this.sessionMapping.touch(resolved);
69987
70484
  return {
69988
70485
  agentId: resolved.agentId,
69989
70486
  sessionKey: resolved.sessionKey,
@@ -70048,12 +70545,20 @@ async function monitorTmuxRun(params) {
70048
70545
  let previousRenderedRunningSnapshot = "";
70049
70546
  let lastPaneChangeAt = params.startedAt;
70050
70547
  let sawActivity = false;
70051
- let sawPaneChange = false;
70548
+ let sawPaneChange = Boolean(normalizePaneText(params.initialSnapshot));
70052
70549
  let sawPromptSubmission = Boolean(params.prompt);
70053
70550
  let detachedNotified = params.detachedAlready;
70054
70551
  let firstMeaningfulDeltaLogged = false;
70055
70552
  let noOutputThresholdLogged = false;
70056
70553
  if (params.prompt) {
70554
+ if (params.trustWorkspace) {
70555
+ await acceptTmuxTrustPromptIfPresent({
70556
+ tmux: params.tmux,
70557
+ sessionName: params.sessionName,
70558
+ captureLines: params.captureLines,
70559
+ startupDelayMs: params.startupDelayMs ?? 0
70560
+ });
70561
+ }
70057
70562
  logLatencyDebug("tmux-submit-start", params.timingContext, {
70058
70563
  sessionName: params.sessionName,
70059
70564
  promptSubmitDelayMs: params.promptSubmitDelayMs
@@ -70193,6 +70698,12 @@ function isRetryableObserverDeliveryError(error) {
70193
70698
  const message = error instanceof Error ? `${error.name} ${error.message}` : String(error);
70194
70699
  return /fetch failed|request timed out|network|socket hang up/i.test(message);
70195
70700
  }
70701
+ function buildMissingSessionIdStartupWarning() {
70702
+ return [
70703
+ "Runner session started, but clisbot could not capture a durable session id yet.",
70704
+ "This session is running, but it is not resumable until a session id is captured and persisted."
70705
+ ].join(" ");
70706
+ }
70196
70707
 
70197
70708
  class ActiveRunInProgressError extends Error {
70198
70709
  update;
@@ -70247,15 +70758,40 @@ class SessionService {
70247
70758
  this.resolveTarget = resolveTarget;
70248
70759
  }
70249
70760
  async recoverPersistedRuns() {
70761
+ const activeSessionKeys = new Set;
70250
70762
  const entries = await this.sessionState.listEntries();
70251
70763
  for (const entry of entries) {
70252
70764
  if (!entry.runtime || entry.runtime.state === "idle") {
70253
70765
  continue;
70254
70766
  }
70255
- await this.reconcilePersistedActiveRun({
70767
+ const run = await this.reconcilePersistedActiveRun({
70256
70768
  agentId: entry.agentId,
70257
70769
  sessionKey: entry.sessionKey
70258
70770
  });
70771
+ if (run) {
70772
+ activeSessionKeys.add(run.resolved.sessionKey);
70773
+ }
70774
+ }
70775
+ return activeSessionKeys;
70776
+ }
70777
+ async clearLostPersistedActiveRuns() {
70778
+ const entries = await this.sessionState.listEntries();
70779
+ for (const entry of entries) {
70780
+ if (!entry.runtime || entry.runtime.state === "idle") {
70781
+ continue;
70782
+ }
70783
+ if (this.activeRuns.has(entry.sessionKey)) {
70784
+ continue;
70785
+ }
70786
+ const resolved = this.resolveTarget({
70787
+ agentId: entry.agentId,
70788
+ sessionKey: entry.sessionKey
70789
+ });
70790
+ if (!await this.tmux.hasSession(resolved.sessionName)) {
70791
+ await this.sessionState.setSessionRuntime(resolved, {
70792
+ state: "idle"
70793
+ });
70794
+ }
70259
70795
  }
70260
70796
  }
70261
70797
  async executePrompt(target, prompt, observer, options = {}) {
@@ -70310,6 +70846,7 @@ class SessionService {
70310
70846
  throw new Error(`Active run disappeared during startup for ${provisionalResolved.sessionKey}.`);
70311
70847
  }
70312
70848
  run.resolved = resolved;
70849
+ run.sessionId = (await this.sessionState.getEntry(resolved.sessionKey))?.sessionId?.trim() || undefined;
70313
70850
  run.startedAt = startedAt;
70314
70851
  run.latestUpdate = this.createRunUpdate({
70315
70852
  resolved,
@@ -70322,6 +70859,17 @@ class SessionService {
70322
70859
  state: "running",
70323
70860
  startedAt
70324
70861
  });
70862
+ if (!run.sessionId) {
70863
+ await this.notifyRunObservers(run, this.createRunUpdate({
70864
+ resolved,
70865
+ status: "running",
70866
+ snapshot: "",
70867
+ fullSnapshot: initialSnapshot,
70868
+ initialSnapshot,
70869
+ note: buildMissingSessionIdStartupWarning(),
70870
+ forceVisible: true
70871
+ }));
70872
+ }
70325
70873
  this.startRunMonitor(resolved.sessionKey, {
70326
70874
  runId,
70327
70875
  prompt,
@@ -70392,6 +70940,34 @@ class SessionService {
70392
70940
  detached: true
70393
70941
  };
70394
70942
  }
70943
+ async interruptActiveRun(target) {
70944
+ const run = this.activeRuns.get(target.sessionKey) ?? await this.reconcilePersistedActiveRun(target);
70945
+ if (!run) {
70946
+ return {
70947
+ interrupted: false
70948
+ };
70949
+ }
70950
+ const error = new Error("Run interrupted by /stop.");
70951
+ const update = this.createRunUpdate({
70952
+ resolved: run.resolved,
70953
+ status: "error",
70954
+ snapshot: error.message,
70955
+ fullSnapshot: run.latestUpdate.fullSnapshot,
70956
+ initialSnapshot: run.latestUpdate.initialSnapshot,
70957
+ note: "Run interrupted."
70958
+ });
70959
+ await this.sessionState.setSessionRuntime(run.resolved, {
70960
+ state: "idle"
70961
+ });
70962
+ await this.notifyRunObservers(run, update);
70963
+ if (!run.initialResult.settled) {
70964
+ run.initialResult.reject(error);
70965
+ }
70966
+ this.activeRuns.delete(run.resolved.sessionKey);
70967
+ return {
70968
+ interrupted: true
70969
+ };
70970
+ }
70395
70971
  hasActiveRun(target) {
70396
70972
  return this.activeRuns.has(target.sessionKey);
70397
70973
  }
@@ -70439,6 +71015,17 @@ class SessionService {
70439
71015
  }
70440
71016
  this.activeRuns.clear();
70441
71017
  }
71018
+ listLiveSessionRuntimes() {
71019
+ return [...this.activeRuns.values()].map((run) => ({
71020
+ state: run.latestUpdate.status === "detached" ? "detached" : "running",
71021
+ startedAt: run.startedAt,
71022
+ sessionKey: run.resolved.sessionKey,
71023
+ agentId: run.resolved.agentId
71024
+ }));
71025
+ }
71026
+ getLiveSessionId(target) {
71027
+ return this.activeRuns.get(target.sessionKey)?.sessionId;
71028
+ }
70442
71029
  buildDetachedNote(resolved) {
70443
71030
  return `This session has been running for over ${resolved.stream.maxRuntimeLabel}. clisbot left it running and will post the final result here when it completes. Use \`/attach\` for live updates, \`/watch every <duration>\` for periodic updates, or \`/stop\` to interrupt it.`;
70444
71031
  }
@@ -70641,6 +71228,7 @@ class SessionService {
70641
71228
  return true;
70642
71229
  }
70643
71230
  currentRun.resolved = recovered.resolved;
71231
+ currentRun.sessionId = (await this.sessionState.getEntry(recovered.resolved.sessionKey))?.sessionId?.trim() || currentRun.sessionId;
70644
71232
  currentRun.latestUpdate = this.createRunUpdate({
70645
71233
  resolved: currentRun.resolved,
70646
71234
  status: currentRun.latestUpdate.status === "detached" ? "detached" : "running",
@@ -70674,7 +71262,7 @@ class SessionService {
70674
71262
  if (!currentRun) {
70675
71263
  return true;
70676
71264
  }
70677
- if (await this.hasStoredResumableSessionId(currentRun.resolved)) {
71265
+ if (await this.requiresManualNewAfterFailedResume(currentRun)) {
70678
71266
  await this.notifyRecoveryStep(currentRun, buildRunRecoveryNote("resume-failed"));
70679
71267
  await this.failActiveRun(sessionKey, currentRun.runId, new Error(buildRunRecoveryNote("manual-new-required")));
70680
71268
  return true;
@@ -70690,12 +71278,13 @@ class SessionService {
70690
71278
  return true;
70691
71279
  }
70692
71280
  }
70693
- async hasStoredResumableSessionId(resolved) {
70694
- if (resolved.runner.sessionId.resume.mode !== "command") {
71281
+ async requiresManualNewAfterFailedResume(run) {
71282
+ const storedSessionId = (await this.sessionState.getEntry(run.resolved.sessionKey))?.sessionId?.trim() || "";
71283
+ if (!storedSessionId) {
70695
71284
  return false;
70696
71285
  }
70697
- const entry = await this.sessionState.getEntry(resolved.sessionKey);
70698
- return Boolean(entry?.sessionId);
71286
+ run.sessionId = storedSessionId;
71287
+ return true;
70699
71288
  }
70700
71289
  startRunMonitor(sessionKey, params) {
70701
71290
  const run = this.getRun(sessionKey, params.runId);
@@ -70712,6 +71301,8 @@ class SessionService {
70712
71301
  sessionName: run.resolved.sessionName,
70713
71302
  prompt: params.prompt,
70714
71303
  promptSubmitDelayMs: run.resolved.runner.promptSubmitDelayMs,
71304
+ trustWorkspace: run.resolved.runner.trustWorkspace,
71305
+ startupDelayMs: run.resolved.runner.startupDelayMs,
70715
71306
  captureLines: run.resolved.stream.captureLines,
70716
71307
  updateIntervalMs: run.resolved.stream.updateIntervalMs,
70717
71308
  idleTimeoutMs: run.resolved.stream.idleTimeoutMs,
@@ -70857,6 +71448,7 @@ class SessionService {
70857
71448
  const run = {
70858
71449
  runId,
70859
71450
  resolved,
71451
+ sessionId: (await this.sessionState.getEntry(resolved.sessionKey))?.sessionId?.trim() || undefined,
70860
71452
  observers: new Map,
70861
71453
  observerFailures: new Map,
70862
71454
  initialResult,
@@ -71071,14 +71663,9 @@ var EMPTY_PROGRESS_PHRASE = "";
71071
71663
  var PROGRESS_FLAG_SUFFIX = "|progress";
71072
71664
  var EMPTY_PROGRESS_FLAG_SUFFIX = "";
71073
71665
  var PROGRESS_RULES_BLOCK = `- use that command to send progress updates and the final reply back to the conversation
71074
- - send at most {{max_progress_messages}} progress updates
71075
- - keep progress updates short and meaningful
71076
- - do not send progress updates for trivial internal steps
71077
- `;
71078
- var FINAL_ONLY_RULES_BLOCK = `- use that command only for the final user-facing reply
71079
- - do not send user-facing progress updates for this conversation
71666
+ - send at most {{max_progress_messages}} short, meaningful progress updates; skip trivial internal steps
71080
71667
  `;
71081
- var FINAL_RULE_REQUIRED = "send exactly 1 final user-facing response";
71668
+ var FINAL_RULE_REQUIRED = "send a single final user-facing message by default; split only when channel limits require it or clarity would otherwise suffer";
71082
71669
  var FINAL_RULE_OPTIONAL = "final response is optional";
71083
71670
  var EMPTY_REPLY_COMMAND = "";
71084
71671
  var EMPTY_REPLY_RULES = "";
@@ -71096,8 +71683,10 @@ var TELEGRAM_REPLY_COMMAND_BASE = `{{command}} message send \\
71096
71683
  --render native \\
71097
71684
  `;
71098
71685
  var SLACK_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
71686
+ For clickable links, use canonical URLs and do not wrap them in backticks.
71099
71687
  Keep each paragraph, list, or code block under 2500 chars.`;
71100
71688
  var TELEGRAM_REPLY_STYLE_HINT = `Put readable hierarchical Markdown in the --message body.
71689
+ For clickable links, use canonical URLs and do not wrap them in backticks.
71101
71690
  Keep the Markdown body under 3000 chars.`;
71102
71691
  var ACCOUNT_CLAUSE = " --account {{account_id}} \\\n";
71103
71692
  var EMPTY_ACCOUNT_CLAUSE = "";
@@ -71138,7 +71727,8 @@ function buildChannelPromptText(params) {
71138
71727
  identity: params.identity,
71139
71728
  config: params.config,
71140
71729
  responseMode: params.responseMode,
71141
- streaming: params.streaming
71730
+ streaming: params.streaming,
71731
+ maxProgressMessagesOverride: params.maxProgressMessagesOverride
71142
71732
  });
71143
71733
  const context = resolvePromptContext(params);
71144
71734
  return renderTemplate(BASE_TEMPLATE, {
@@ -71183,10 +71773,11 @@ function renderMessagePromptParts(params) {
71183
71773
  replyStyleHint: EMPTY_REPLY_STYLE_HINT
71184
71774
  };
71185
71775
  }
71186
- const allowProgress = (params.streaming ?? "off") === "off";
71187
- const progressPhrase = allowProgress ? PROGRESS_PHRASE : EMPTY_PROGRESS_PHRASE;
71188
- const progressFlagSuffix = allowProgress ? PROGRESS_FLAG_SUFFIX : EMPTY_PROGRESS_FLAG_SUFFIX;
71189
- const progressRulesBlock = allowProgress ? PROGRESS_RULES_BLOCK : FINAL_ONLY_RULES_BLOCK;
71776
+ const maxProgressMessages = Math.max(0, params.maxProgressMessagesOverride ?? params.config.maxProgressMessages);
71777
+ const progressEnabled = maxProgressMessages > 0;
71778
+ const progressPhrase = progressEnabled ? PROGRESS_PHRASE : EMPTY_PROGRESS_PHRASE;
71779
+ const progressFlagSuffix = progressEnabled ? PROGRESS_FLAG_SUFFIX : EMPTY_PROGRESS_FLAG_SUFFIX;
71780
+ const progressRulesBlock = progressEnabled ? PROGRESS_RULES_BLOCK : "";
71190
71781
  const finalRuleLine = params.config.requireFinalResponse ? FINAL_RULE_REQUIRED : FINAL_RULE_OPTIONAL;
71191
71782
  return {
71192
71783
  deliveryIntro: renderTemplate(DELIVERY_INTRO, {
@@ -71201,7 +71792,7 @@ function renderMessagePromptParts(params) {
71201
71792
  }),
71202
71793
  replyRules: renderTemplate(REPLY_RULES, {
71203
71794
  progress_rules_block: renderTemplate(progressRulesBlock, {
71204
- max_progress_messages: String(params.config.maxProgressMessages)
71795
+ max_progress_messages: String(maxProgressMessages)
71205
71796
  }),
71206
71797
  final_rule_line: finalRuleLine
71207
71798
  }),
@@ -71215,8 +71806,9 @@ function renderConfigurationGuidance() {
71215
71806
  const cliName = getRenderedCliName();
71216
71807
  return [
71217
71808
  `When the user asks to change ${cliName} configuration, use ${cliName} CLI commands; see ${renderCliCommand("--help", { inline: true })}, ${renderCliCommand("bots --help", { inline: true })}, ${renderCliCommand("routes --help", { inline: true })}, ${renderCliCommand("auth --help", { inline: true })}, or ${renderCliCommand("update --help", { inline: true })} for details.`,
71809
+ `For ${cliName} install or update requests, check ${renderCliCommand("update --help", { inline: true })} first and follow it.`,
71218
71810
  `For schedule/loop/reminder requests, inspect ${renderCliCommand("loops --help", { inline: true })} and use the loops CLI.`,
71219
- `For durable queue inspection or one-shot queued prompts, inspect ${renderCliCommand("queues --help", { inline: true })} and use ${renderCliCommand("queues create --channel <slack|telegram> --target <route> --sender <principal> <prompt>", { inline: true })}.`
71811
+ `For durable queue requests, inspect ${renderCliCommand("queues --help", { inline: true })} and use the queues CLI.`
71220
71812
  ].join(`
71221
71813
  `);
71222
71814
  }
@@ -71266,9 +71858,11 @@ function buildReplyCommandBase(params) {
71266
71858
  }
71267
71859
 
71268
71860
  // src/channels/mode-config-shared.ts
71269
- function createTelegramRouteOverride() {
71861
+ function createTelegramRouteOverride(base) {
71270
71862
  return {
71271
- enabled: true,
71863
+ enabled: base?.enabled ?? true,
71864
+ requireMention: base?.requireMention ?? true,
71865
+ allowBots: base?.allowBots ?? false,
71272
71866
  allowUsers: [],
71273
71867
  blockUsers: []
71274
71868
  };
@@ -71291,7 +71885,7 @@ function getOrCreateTelegramTopicRoute(bot, chatId, topicId) {
71291
71885
  if (existingTopic) {
71292
71886
  return existingTopic;
71293
71887
  }
71294
- const createdTopic = createTelegramRouteOverride();
71888
+ const createdTopic = createTelegramRouteOverride(group);
71295
71889
  group.topics[topicId] = createdTopic;
71296
71890
  return createdTopic;
71297
71891
  }
@@ -71722,8 +72316,9 @@ class SurfaceRuntime {
71722
72316
  }
71723
72317
  const identity = this.buildLoopChannelIdentity(loop);
71724
72318
  const notifications = this.resolveSurfaceNotifications(identity);
72319
+ const mode = loop.loopStart ?? notifications.loopStart;
71725
72320
  const text = loop.kind === "calendar" ? renderLoopStartNotification({
71726
- mode: notifications.loopStart,
72321
+ mode,
71727
72322
  agentId: target.agentId,
71728
72323
  loopId: loop.id,
71729
72324
  promptSummary: loop.promptSummary,
@@ -71736,7 +72331,7 @@ class SurfaceRuntime {
71736
72331
  maxRuns: loop.maxRuns,
71737
72332
  kind: "calendar"
71738
72333
  }) : renderLoopStartNotification({
71739
- mode: notifications.loopStart,
72334
+ mode,
71740
72335
  agentId: target.agentId,
71741
72336
  loopId: loop.id,
71742
72337
  promptSummary: loop.promptSummary,
@@ -71824,7 +72419,7 @@ class SurfaceRuntime {
71824
72419
  }
71825
72420
  }
71826
72421
  async buildManagedLoopPrompt(agentId, loop) {
71827
- if (!loop.canonicalPromptText || !loop.surfaceBinding) {
72422
+ if (!loop.surfaceBinding) {
71828
72423
  return loop.promptText;
71829
72424
  }
71830
72425
  const identity = this.buildLoopChannelIdentity(loop);
@@ -71839,7 +72434,7 @@ class SurfaceRuntime {
71839
72434
  scheduledLoopId: loop.id
71840
72435
  });
71841
72436
  return buildAgentPromptText({
71842
- text: loop.canonicalPromptText,
72437
+ text: loop.promptText,
71843
72438
  identity,
71844
72439
  config: channelConfig.agentPrompt,
71845
72440
  cliTool: getAgentEntry2(this.loadedConfig, agentId)?.cli,
@@ -71849,11 +72444,12 @@ class SurfaceRuntime {
71849
72444
  agentId,
71850
72445
  time: promptTime,
71851
72446
  promptContext,
71852
- scheduledLoopId: loop.id
72447
+ scheduledLoopId: loop.id,
72448
+ maxProgressMessagesOverride: loop.progressMessages
71853
72449
  });
71854
72450
  }
71855
72451
  async buildManagedQueuePrompt(agentId, item) {
71856
- if (!item.canonicalPromptText || !item.surfaceBinding) {
72452
+ if (!item.surfaceBinding) {
71857
72453
  return item.promptText;
71858
72454
  }
71859
72455
  const identity = this.buildQueueChannelIdentity(item);
@@ -71867,7 +72463,7 @@ class SurfaceRuntime {
71867
72463
  time: promptTime
71868
72464
  });
71869
72465
  return buildAgentPromptText({
71870
- text: item.canonicalPromptText,
72466
+ text: item.promptText,
71871
72467
  identity,
71872
72468
  config: channelConfig.agentPrompt,
71873
72469
  cliTool: getAgentEntry2(this.loadedConfig, agentId)?.cli,
@@ -72034,6 +72630,7 @@ class AgentService {
72034
72630
  tmuxClient;
72035
72631
  queue = new AgentJobQueue;
72036
72632
  sessionState;
72633
+ sessionMapping;
72037
72634
  runnerSessions;
72038
72635
  activeRuns;
72039
72636
  managedLoops;
@@ -72048,7 +72645,8 @@ class AgentService {
72048
72645
  this.tmuxClient = deps.tmux ?? new TmuxClient(this.loadedConfig.raw.tmux.socketPath);
72049
72646
  const sessionStore = deps.sessionStore ?? new SessionStore(resolveSessionStorePath(this.loadedConfig));
72050
72647
  this.sessionState = new AgentSessionState(sessionStore);
72051
- this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target));
72648
+ this.sessionMapping = new SessionMapping(this.sessionState);
72649
+ this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target), this.sessionMapping);
72052
72650
  this.activeRuns = this.createSessionService();
72053
72651
  this.surfaceRuntime = new SurfaceRuntime(this.loadedConfig);
72054
72652
  this.managedQueues = this.createManagedQueueController();
@@ -72059,7 +72657,7 @@ class AgentService {
72059
72657
  }
72060
72658
  set tmux(value) {
72061
72659
  this.tmuxClient = value;
72062
- this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target));
72660
+ this.runnerSessions = new RunnerService(this.loadedConfig, this.tmuxClient, this.sessionState, (target) => this.resolveTarget(target), this.sessionMapping);
72063
72661
  this.activeRuns = this.createSessionService();
72064
72662
  this.managedQueues = this.createManagedQueueController();
72065
72663
  }
@@ -72090,9 +72688,8 @@ class AgentService {
72090
72688
  });
72091
72689
  }
72092
72690
  async start() {
72093
- await this.activeRuns.recoverPersistedRuns();
72094
- const activeSessions = new Set((await this.sessionState.listActiveSessionRuntimes()).map((runtime) => runtime.sessionKey));
72095
- await this.sessionState.resetStaleRunningQueuedItems(activeSessions);
72691
+ const activeSessionKeys = await this.activeRuns.recoverPersistedRuns();
72692
+ await this.sessionState.resetStaleRunningQueuedItems(activeSessionKeys);
72096
72693
  await this.managedQueues.reconcilePersistedQueueItems();
72097
72694
  this.queueReconcileTimer = setInterval(() => {
72098
72695
  this.managedQueues.reconcilePersistedQueueItems().catch((error) => {
@@ -72150,7 +72747,12 @@ class AgentService {
72150
72747
  return this.runnerSessions.captureTranscript(target);
72151
72748
  }
72152
72749
  async interruptSession(target) {
72153
- return this.runnerSessions.interruptSession(target);
72750
+ const runner = await this.runnerSessions.interruptSession(target);
72751
+ const activeRun = await this.activeRuns.interruptActiveRun(target);
72752
+ return {
72753
+ ...runner,
72754
+ interrupted: runner.interrupted || activeRun.interrupted
72755
+ };
72154
72756
  }
72155
72757
  async nudgeSession(target) {
72156
72758
  return this.runnerSessions.nudgeSession(target);
@@ -72164,17 +72766,29 @@ class AgentService {
72164
72766
  async getSessionRuntime(target) {
72165
72767
  return this.sessionState.getSessionRuntime(target);
72166
72768
  }
72769
+ async getSessionEntry(target) {
72770
+ return this.sessionState.getEntry(target.sessionKey);
72771
+ }
72772
+ async listSessionEntries() {
72773
+ return this.sessionState.listEntries();
72774
+ }
72167
72775
  async getSessionDiagnostics(target) {
72168
72776
  const resolved = this.resolveTarget(target);
72169
72777
  const entry = await this.sessionState.getEntry(target.sessionKey);
72170
- const sessionId = entry?.sessionId?.trim() || undefined;
72778
+ const liveSessionId = this.activeRuns.getLiveSessionId(target);
72779
+ const storedSessionId = entry?.sessionId?.trim() || undefined;
72780
+ const sessionId = liveSessionId ?? storedSessionId;
72171
72781
  return {
72782
+ sessionName: resolved.sessionName,
72172
72783
  sessionId,
72784
+ sessionIdPersistence: sessionId && sessionId === storedSessionId ? "persisted" : sessionId ? "not-persisted-yet" : undefined,
72785
+ storedSessionId,
72173
72786
  resumeCommand: buildResumeCommandPreview(resolved, sessionId)
72174
72787
  };
72175
72788
  }
72176
- async listActiveSessionRuntimes() {
72177
- return this.sessionState.listActiveSessionRuntimes();
72789
+ async listLiveSessionRuntimes() {
72790
+ await this.activeRuns.clearLostPersistedActiveRuns();
72791
+ return this.activeRuns.listLiveSessionRuntimes();
72178
72792
  }
72179
72793
  async setConversationFollowUpMode(target, mode) {
72180
72794
  return this.sessionState.setConversationFollowUpMode(this.resolveTarget(target), mode);
@@ -72275,8 +72889,8 @@ class AgentService {
72275
72889
  async createCalendarLoop(params) {
72276
72890
  return this.managedLoops.createCalendarLoop(params);
72277
72891
  }
72278
- async cancelIntervalLoop(loopId) {
72279
- return this.managedLoops.cancelIntervalLoop(loopId);
72892
+ async cancelIntervalLoop(target, loopId) {
72893
+ return this.managedLoops.cancelIntervalLoop(target, loopId);
72280
72894
  }
72281
72895
  async cancelIntervalLoopsForSession(target) {
72282
72896
  return this.managedLoops.cancelIntervalLoopsForSession(target);
@@ -72287,8 +72901,8 @@ class AgentService {
72287
72901
  listIntervalLoops(params) {
72288
72902
  return this.managedLoops.listIntervalLoops(params);
72289
72903
  }
72290
- getIntervalLoop(loopId) {
72291
- return this.managedLoops.getIntervalLoop(loopId);
72904
+ getIntervalLoop(target, loopId) {
72905
+ return this.managedLoops.getIntervalLoop(target.sessionKey, loopId);
72292
72906
  }
72293
72907
  getActiveIntervalLoopCount() {
72294
72908
  return this.managedLoops.getActiveIntervalLoopCount();
@@ -72340,9 +72954,8 @@ function createStoredQueueItem(params) {
72340
72954
  createdAt: now,
72341
72955
  updatedAt: now,
72342
72956
  promptText: params.promptText,
72343
- canonicalPromptText: params.canonicalPromptText,
72344
72957
  protectedControlMutationRule: params.protectedControlMutationRule,
72345
- promptSummary: params.promptSummary ?? summarizeQueuePrompt(params.canonicalPromptText ?? params.promptText),
72958
+ promptSummary: params.promptSummary ?? summarizeQueuePrompt(params.promptText),
72346
72959
  promptSource: "custom",
72347
72960
  createdBy: params.createdBy,
72348
72961
  sender: params.sender,
@@ -72593,20 +73206,13 @@ function renderStartupSteeringUnavailableMessage() {
72593
73206
  ].join(`
72594
73207
  `);
72595
73208
  }
72596
- function renderPrincipalFormat(identity) {
72597
- if (identity.platform === "slack") {
72598
- return "slack:<nativeUserId>";
72599
- }
72600
- return "telegram:<nativeUserId>";
72601
- }
72602
- function renderPrincipalExample(identity) {
72603
- if (identity.senderId) {
72604
- return `${identity.platform}:${identity.senderId}`;
72605
- }
72606
- if (identity.platform === "slack") {
72607
- return "slack:U123ABC456";
72608
- }
72609
- return "telegram:1276408333";
73209
+ function renderNewSessionFailureMessage(error) {
73210
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : "Unknown error.";
73211
+ return [
73212
+ "Could not finish opening a new runner conversation.",
73213
+ details
73214
+ ].join(`
73215
+ `);
72610
73216
  }
72611
73217
  function renderWhoAmIMessage(params) {
72612
73218
  const lines = [
@@ -72615,9 +73221,13 @@ function renderWhoAmIMessage(params) {
72615
73221
  `platform: \`${params.identity.platform}\``,
72616
73222
  `conversationKind: \`${params.identity.conversationKind}\``,
72617
73223
  `agentId: \`${params.route.agentId}\``,
72618
- `sessionKey: \`${params.sessionTarget.sessionKey}\``,
72619
- `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
73224
+ `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
73225
+ `sessionId: \`${params.sessionDiagnostics.sessionId ?? "(not available yet)"}\``,
73226
+ `sessionIdPersistence: \`${params.sessionDiagnostics.sessionIdPersistence ?? "not stored yet"}\``
72620
73227
  ];
73228
+ if (params.sessionDiagnostics.storedSessionId && params.sessionDiagnostics.storedSessionId !== params.sessionDiagnostics.sessionId) {
73229
+ lines.push(`storedSessionId: \`${params.sessionDiagnostics.storedSessionId}\``);
73230
+ }
72621
73231
  if (params.identity.senderId) {
72622
73232
  lines.push(`senderId: \`${params.identity.senderId}\``);
72623
73233
  }
@@ -72633,7 +73243,7 @@ function renderWhoAmIMessage(params) {
72633
73243
  if (params.identity.topicId) {
72634
73244
  lines.push(`topicId: \`${params.identity.topicId}\``);
72635
73245
  }
72636
- lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
73246
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassPairing: \`${params.auth.mayBypassPairing}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `verbose: \`${params.route.verbose}\``);
72637
73247
  return lines.join(`
72638
73248
  `);
72639
73249
  }
@@ -72644,9 +73254,13 @@ function renderRouteStatusMessage(params) {
72644
73254
  `platform: \`${params.identity.platform}\``,
72645
73255
  `conversationKind: \`${params.identity.conversationKind}\``,
72646
73256
  `agentId: \`${params.route.agentId}\``,
72647
- `sessionKey: \`${params.sessionTarget.sessionKey}\``,
72648
- `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
73257
+ `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
73258
+ `sessionId: \`${params.sessionDiagnostics.sessionId ?? "(not available yet)"}\``,
73259
+ `sessionIdPersistence: \`${params.sessionDiagnostics.sessionIdPersistence ?? "not stored yet"}\``
72649
73260
  ];
73261
+ if (params.sessionDiagnostics.storedSessionId && params.sessionDiagnostics.storedSessionId !== params.sessionDiagnostics.sessionId) {
73262
+ lines.push(`storedSessionId: \`${params.sessionDiagnostics.storedSessionId}\``);
73263
+ }
72650
73264
  if (params.identity.senderId) {
72651
73265
  lines.push(`senderId: \`${params.identity.senderId}\``);
72652
73266
  }
@@ -72662,7 +73276,7 @@ function renderRouteStatusMessage(params) {
72662
73276
  if (params.identity.topicId) {
72663
73277
  lines.push(`topicId: \`${params.identity.topicId}\``);
72664
73278
  }
72665
- lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `principalFormat: \`${renderPrincipalFormat(params.identity)}\``, `principalExample: \`${renderPrincipalExample(params.identity)}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `responseMode: \`${params.route.responseMode}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone.effective: \`${params.timezone.effective}\``, `timezone.route: \`${params.timezone.route ?? "(inherit)"}\``, `timezone.bot: \`${params.timezone.bot ?? "(inherit)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
73279
+ lines.push(`resumeCommand: \`${params.sessionDiagnostics.resumeCommand ?? "(not available yet)"}\``, `principal: \`${params.auth.principal ?? "(none)"}\``, `streaming: \`${params.route.streaming}\``, `response: \`${params.route.response}\``, `additionalMessageMode: \`${params.route.additionalMessageMode}\``, `surfaceNotifications.queueStart: \`${params.route.surfaceNotifications.queueStart}\``, `surfaceNotifications.loopStart: \`${params.route.surfaceNotifications.loopStart}\``, `verbose: \`${params.route.verbose}\``, `appRole: \`${params.auth.appRole}\``, `agentRole: \`${params.auth.agentRole}\``, `mayBypassSharedSenderPolicy: \`${params.auth.mayBypassSharedSenderPolicy}\``, `mayManageProtectedResources: \`${params.auth.mayManageProtectedResources}\``, `canUseShell: \`${params.auth.canUseShell}\``, `timezone.effective: \`${params.timezone.effective}\``, `timezone.route: \`${params.timezone.route ?? "(inherit)"}\``, `timezone.bot: \`${params.timezone.bot ?? "(inherit)"}\``, `followUp.mode: \`${params.followUpState.overrideMode ?? params.route.followUp.mode}\``, `followUp.windowMinutes: \`${formatFollowUpTtlMinutes(params.route.followUp.participationTtlMs)}\``, `run.state: \`${params.runtimeState.state}\``);
72666
73280
  if (params.runtimeState.startedAt) {
72667
73281
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
72668
73282
  }
@@ -73422,7 +74036,6 @@ async function processChannelInteraction(params) {
73422
74036
  };
73423
74037
  const queueItem = forceQueuedDelivery ? createStoredQueueItem({
73424
74038
  promptText: delayedPromptQueueText,
73425
- canonicalPromptText: delayedPromptQueueText,
73426
74039
  promptSummary: explicitQueueMessage ?? summarizeQueuePrompt(delayedPromptQueueText),
73427
74040
  createdBy: params.senderId,
73428
74041
  sender: buildLoopSender(params.identity),
@@ -73552,14 +74165,18 @@ async function processChannelInteraction(params) {
73552
74165
  await params.agentService.recordConversationReply(params.sessionTarget);
73553
74166
  return interactionResult;
73554
74167
  }
73555
- const rotated = await params.agentService.triggerNewSession(params.sessionTarget);
73556
- await params.postText([
73557
- `Triggered a new runner conversation for agent \`${rotated.agentId}\`.`,
73558
- `sessionName: \`${rotated.sessionName}\``,
73559
- `storedSessionId: \`${rotated.sessionId ?? "none"}\``,
73560
- rotated.restartedRunner ? "No live runner existed, so clisbot opened a fresh runner session." : `triggerCommand: \`${rotated.command}\``
73561
- ].join(`
74168
+ try {
74169
+ const rotated = await params.agentService.triggerNewSession(params.sessionTarget);
74170
+ await params.postText([
74171
+ `Triggered a new runner conversation for agent \`${rotated.agentId}\`.`,
74172
+ `sessionName: \`${rotated.sessionName}\``,
74173
+ `sessionId: \`${rotated.sessionId ?? "none"}\``,
74174
+ rotated.restartedRunner ? "No live runner existed, so clisbot opened a fresh runner session." : `triggerCommand: \`${rotated.command}\``
74175
+ ].join(`
73562
74176
  `));
74177
+ } catch (error) {
74178
+ await params.postText(renderNewSessionFailureMessage(error));
74179
+ }
73563
74180
  await params.agentService.recordConversationReply(params.sessionTarget);
73564
74181
  return interactionResult;
73565
74182
  }
@@ -73735,7 +74352,7 @@ async function processChannelInteraction(params) {
73735
74352
  await params.agentService.recordConversationReply(params.sessionTarget);
73736
74353
  return interactionResult;
73737
74354
  }
73738
- const cancelled = await params.agentService.cancelIntervalLoop(targetLoopId);
74355
+ const cancelled = await params.agentService.cancelIntervalLoop(params.sessionTarget, targetLoopId);
73739
74356
  await params.postText(cancelled ? `Cancelled loop \`${targetLoopId}\`.` : `No active loop found with id \`${targetLoopId}\`.`);
73740
74357
  await params.agentService.recordConversationReply(params.sessionTarget);
73741
74358
  return interactionResult;
@@ -73820,9 +74437,9 @@ ${renderLoopUsage()}`);
73820
74437
  const createdLoop2 = await params.agentService.createCalendarLoop({
73821
74438
  target: params.sessionTarget,
73822
74439
  promptText: resolvedLoopPrompt.text,
73823
- canonicalPromptText: resolvedLoopPrompt.text,
73824
74440
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
73825
74441
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
74442
+ loopStart: slashCommand.params.loopStart,
73826
74443
  surfaceBinding: buildLoopSurfaceBinding(params.identity),
73827
74444
  cadence: slashCommand.params.cadence,
73828
74445
  dayOfWeek: slashCommand.params.dayOfWeek,
@@ -73859,9 +74476,9 @@ ${renderLoopUsage()}`);
73859
74476
  const createdLoop = await params.agentService.createIntervalLoop({
73860
74477
  target: params.sessionTarget,
73861
74478
  promptText: resolvedLoopPrompt.text,
73862
- canonicalPromptText: resolvedLoopPrompt.text,
73863
74479
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
73864
74480
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
74481
+ loopStart: slashCommand.params.loopStart,
73865
74482
  surfaceBinding: buildLoopSurfaceBinding(params.identity),
73866
74483
  intervalMs: effectiveIntervalMs,
73867
74484
  maxRuns: maxRunsPerLoop,
@@ -75347,9 +75964,9 @@ function getSlackMaxChars(maxMessageChars) {
75347
75964
  }
75348
75965
 
75349
75966
  // src/channels/runtime-identity.ts
75350
- import { createHash } from "node:crypto";
75967
+ import { createHash as createHash2 } from "node:crypto";
75351
75968
  function buildTokenHint(token) {
75352
- return createHash("sha256").update(token.trim()).digest("hex").slice(0, 8);
75969
+ return createHash2("sha256").update(token.trim()).digest("hex").slice(0, 8);
75353
75970
  }
75354
75971
 
75355
75972
  // src/channels/slack/service.ts
@@ -75609,6 +76226,12 @@ class SlackSocketService {
75609
76226
  allowFrom: params.route.allowUsers ?? [],
75610
76227
  userId: senderId
75611
76228
  })) {
76229
+ const explicitlyAddressed = params.wasMentioned || hasBotMention(event.text ?? "", this.botUserId);
76230
+ if (params.route.requireMention && !explicitlyAddressed) {
76231
+ debugSlackEvent("drop-shared-not-addressed", { eventId, senderId });
76232
+ await this.processedEventsStore.markCompleted(eventId);
76233
+ return;
76234
+ }
75612
76235
  try {
75613
76236
  await postSlackText(this.app.client, {
75614
76237
  channel: channelId,
@@ -75949,7 +76572,7 @@ class SlackSocketService {
75949
76572
  senderId: slackSenderId,
75950
76573
  text,
75951
76574
  agentPromptText,
75952
- agentPromptBuilder: (nextText) => buildAgentPromptText({
76575
+ agentPromptBuilder: (nextText, options) => buildAgentPromptText({
75953
76576
  text: enrichPromptText(nextText),
75954
76577
  identity,
75955
76578
  config: this.getBotConfig().agentPrompt,
@@ -75967,7 +76590,8 @@ class SlackSocketService {
75967
76590
  agentId: params.route.agentId,
75968
76591
  routeTimezone: params.route.timezone,
75969
76592
  botTimezone: params.route.botTimezone
75970
- }).timezone
76593
+ }).timezone,
76594
+ maxProgressMessagesOverride: options?.maxProgressMessagesOverride
75971
76595
  }),
75972
76596
  promptContext,
75973
76597
  protectedControlMutationRule,
@@ -76538,10 +77162,11 @@ function resolveSlackMessageContent(params) {
76538
77162
  throw new Error("Slack blocks input supports only --render none or --render blocks");
76539
77163
  }
76540
77164
  const blocks = parseSlackBlocksInput(text);
77165
+ const fallbackText = buildSlackBlocksFallbackText(blocks);
76541
77166
  return {
76542
- text: buildSlackBlocksFallbackText(blocks),
77167
+ text: fallbackText,
76543
77168
  blocks,
76544
- apiText: "​"
77169
+ apiText: fallbackText
76545
77170
  };
76546
77171
  }
76547
77172
  if (inputFormat === "html") {
@@ -76549,10 +77174,11 @@ function resolveSlackMessageContent(params) {
76549
77174
  }
76550
77175
  if (renderMode === "blocks") {
76551
77176
  const blocks = renderMarkdownToSlackBlocks(text);
77177
+ const fallbackText = buildSlackBlocksFallbackText(blocks);
76552
77178
  return {
76553
- text: buildSlackBlocksFallbackText(blocks),
77179
+ text: fallbackText,
76554
77180
  blocks,
76555
- apiText: "​"
77181
+ apiText: fallbackText
76556
77182
  };
76557
77183
  }
76558
77184
  if (renderMode === "html") {
@@ -77538,6 +78164,33 @@ function sanitizeTelegramHref(rawHref) {
77538
78164
  }
77539
78165
  return null;
77540
78166
  }
78167
+ function splitTrailingUrlPunctuation(rawUrl) {
78168
+ let core = rawUrl;
78169
+ let trailing = "";
78170
+ while (/[.,!?;:]$/.test(core)) {
78171
+ trailing = core.slice(-1) + trailing;
78172
+ core = core.slice(0, -1);
78173
+ }
78174
+ while (core.endsWith(")")) {
78175
+ const openCount = (core.match(/\(/g) ?? []).length;
78176
+ const closeCount = (core.match(/\)/g) ?? []).length;
78177
+ if (closeCount <= openCount) {
78178
+ break;
78179
+ }
78180
+ trailing = ")" + trailing;
78181
+ core = core.slice(0, -1);
78182
+ }
78183
+ while (core.endsWith("]")) {
78184
+ const openCount = (core.match(/\[/g) ?? []).length;
78185
+ const closeCount = (core.match(/]/g) ?? []).length;
78186
+ if (closeCount <= openCount) {
78187
+ break;
78188
+ }
78189
+ trailing = "]" + trailing;
78190
+ core = core.slice(0, -1);
78191
+ }
78192
+ return { core, trailing };
78193
+ }
77541
78194
  function storeToken(tokens, value) {
77542
78195
  const token = `${TOKEN_PREFIX}${tokens.length};\x00`;
77543
78196
  tokens.push(value);
@@ -77564,6 +78217,14 @@ function renderInlineMarkdownToTelegramHtml(text) {
77564
78217
  }
77565
78218
  return storeToken(tokens, `<a href="${safeHref}">${escapeHtml(label)}</a>`);
77566
78219
  });
78220
+ working = working.replaceAll(/\b(?:https?:\/\/|tg:\/\/|mailto:)[^\s<>"`]+/g, (rawUrl) => {
78221
+ const { core, trailing } = splitTrailingUrlPunctuation(rawUrl);
78222
+ const safeHref = sanitizeTelegramHref(core);
78223
+ if (!safeHref) {
78224
+ return rawUrl;
78225
+ }
78226
+ return storeToken(tokens, `<a href="${safeHref}">${escapeHtml(core)}</a>`) + trailing;
78227
+ });
77567
78228
  working = escapeHtml(working);
77568
78229
  working = applyInlineFormatting(working);
77569
78230
  return restoreTokens(working, tokens);
@@ -77955,6 +78616,7 @@ var TELEGRAM_FULL_COMMANDS = [
77955
78616
  { command: "detach", description: "Stop live updates for this thread" },
77956
78617
  { command: "watch", description: "Watch the active run on an interval" },
77957
78618
  { command: "stop", description: "Interrupt current run" },
78619
+ { command: "new", description: "Start new session" },
77958
78620
  { command: "nudge", description: "Send one extra Enter to the session" },
77959
78621
  { command: "followup", description: "Show or change follow-up mode" },
77960
78622
  { command: "mention", description: "Require explicit mention for later turns" },
@@ -78580,7 +79242,7 @@ class TelegramPollingService {
78580
79242
  senderId: message.from?.id != null ? String(message.from.id).trim() : undefined,
78581
79243
  text,
78582
79244
  agentPromptText,
78583
- agentPromptBuilder: (nextText) => buildAgentPromptText({
79245
+ agentPromptBuilder: (nextText, options) => buildAgentPromptText({
78584
79246
  text: enrichPromptText(nextText),
78585
79247
  identity,
78586
79248
  config: this.getBotConfig().agentPrompt,
@@ -78598,7 +79260,8 @@ class TelegramPollingService {
78598
79260
  agentId: route.agentId,
78599
79261
  routeTimezone: route.timezone,
78600
79262
  botTimezone: route.botTimezone
78601
- }).timezone
79263
+ }).timezone,
79264
+ maxProgressMessagesOverride: options?.maxProgressMessagesOverride
78602
79265
  }),
78603
79266
  promptContext,
78604
79267
  protectedControlMutationRule,
@@ -79286,8 +79949,8 @@ var defaultRuntimeMonitorDependencies = {
79286
79949
  },
79287
79950
  readState: readRuntimeMonitorState,
79288
79951
  writeState: writeRuntimeMonitorState,
79289
- removePid: (pidPath) => rmSync2(pidPath, { force: true }),
79290
- removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync2(runtimeCredentialsPath, { force: true }),
79952
+ removePid: (pidPath) => rmSync3(pidPath, { force: true }),
79953
+ removeRuntimeCredentials: (runtimeCredentialsPath) => rmSync3(runtimeCredentialsPath, { force: true }),
79291
79954
  sleep,
79292
79955
  now: () => Date.now(),
79293
79956
  spawnChild: (command, args, options) => spawn2(command, args, {
@@ -79837,7 +80500,7 @@ async function startDetachedRuntime(params) {
79837
80500
  };
79838
80501
  }
79839
80502
  if (existingPid) {
79840
- rmSync3(pidPath, { force: true });
80503
+ rmSync4(pidPath, { force: true });
79841
80504
  }
79842
80505
  if (existingMonitorState?.runtimePid && isProcessRunning(existingMonitorState.runtimePid)) {
79843
80506
  kill2(existingMonitorState.runtimePid, "SIGTERM");
@@ -79946,7 +80609,7 @@ async function stopDetachedRuntime(params, dependencies = {}) {
79946
80609
  }
79947
80610
  }
79948
80611
  }
79949
- rmSync3(pidPath, { force: true });
80612
+ rmSync4(pidPath, { force: true });
79950
80613
  removeRuntimeCredentials(runtimeCredentialsPath);
79951
80614
  await disableExpiredMemAccountsInConfig(configPath);
79952
80615
  if (monitorState) {
@@ -79988,7 +80651,7 @@ async function writeRuntimePid(pidPath, pid = process.pid) {
79988
80651
  `);
79989
80652
  }
79990
80653
  function removeRuntimePid(pidPath) {
79991
- rmSync3(resolvePidPath(pidPath), { force: true });
80654
+ rmSync4(resolvePidPath(pidPath), { force: true });
79992
80655
  }
79993
80656
  async function getRuntimeStatus(params = {}) {
79994
80657
  const configPath = resolveConfigPath2(params.configPath);
@@ -80849,7 +81512,7 @@ function resolveSlackLoopCliContext(params) {
80849
81512
  sessionTarget,
80850
81513
  identity,
80851
81514
  route,
80852
- buildLoopPromptText: (text) => buildAgentPromptText({
81515
+ buildLoopPromptText: (text, options) => buildAgentPromptText({
80853
81516
  text,
80854
81517
  identity,
80855
81518
  config: botConfig.agentPrompt,
@@ -80863,7 +81526,8 @@ function resolveSlackLoopCliContext(params) {
80863
81526
  agentId: sessionTarget.agentId,
80864
81527
  routeTimezone: route.timezone,
80865
81528
  botTimezone: route.botTimezone
80866
- }).timezone
81529
+ }).timezone,
81530
+ maxProgressMessagesOverride: options?.maxProgressMessagesOverride
80867
81531
  })
80868
81532
  };
80869
81533
  }
@@ -80919,7 +81583,7 @@ function resolveTelegramLoopCliContext(params) {
80919
81583
  sessionTarget,
80920
81584
  identity,
80921
81585
  route,
80922
- buildLoopPromptText: (text) => buildAgentPromptText({
81586
+ buildLoopPromptText: (text, options) => buildAgentPromptText({
80923
81587
  text,
80924
81588
  identity,
80925
81589
  config: botConfig.agentPrompt,
@@ -80933,7 +81597,8 @@ function resolveTelegramLoopCliContext(params) {
80933
81597
  agentId: sessionTarget.agentId,
80934
81598
  routeTimezone: route.timezone,
80935
81599
  botTimezone: route.botTimezone
80936
- }).timezone
81600
+ }).timezone,
81601
+ maxProgressMessagesOverride: options?.maxProgressMessagesOverride
80937
81602
  })
80938
81603
  };
80939
81604
  }
@@ -81093,6 +81758,8 @@ function renderLoopsHelp() {
81093
81758
  " - `--sender <principal>` is required when creating loops, using `slack:<user-id>` or `telegram:<user-id>`",
81094
81759
  " - optional creator display fields: `--sender-name <name>` and `--sender-handle <handle>`",
81095
81760
  " - `--timezone <iana>` is a one-off wall-clock loop override and is frozen on the created loop record",
81761
+ ` - \`${LOOP_START_FLAG} <none|brief|full>\` controls scheduled loop-start notifications only; it does not control injected agent progress messages`,
81762
+ " - `--progress <count>` overrides loop progress-message injection for agent replies; `0` disables progress messages, and omitting the flag inherits the normal clisbot prompt config",
81096
81763
  " - in Telegram forum groups, omitting `--topic-id` targets the parent chat surface; sends then follow Telegram's normal no-`message_thread_id` behavior, which is the General topic when that forum has one",
81097
81764
  "",
81098
81765
  "Expressions:",
@@ -81114,6 +81781,7 @@ function renderLoopsHelp() {
81114
81781
  " - CLI loop creation fails without `--sender` so scheduled prompts can preserve creator identity",
81115
81782
  " - the first wall-clock loop returns `confirmation_required` and does not persist until rerun with `--confirm`",
81116
81783
  " - recurring interval loops and confirmed wall-clock loops are persisted immediately and picked up by the runtime when it is running",
81784
+ " - loop-created agent prompts inherit the normal clisbot prompt config unless `--progress <count>` overrides that loop",
81117
81785
  " - if runtime is stopped, recurring loops activate on the next `clisbot start`",
81118
81786
  " - global `cancel --all` clears the whole app; scoped `cancel --all` clears one routed session",
81119
81787
  " - `cancel --all --app` is accepted only with a scoped session target, matching `/loop cancel --all --app`",
@@ -81143,6 +81811,8 @@ function renderLoopsCreateHelp() {
81143
81811
  " - `--new-thread` creates a Slack thread anchor before persisting the loop",
81144
81812
  " - `--timezone <iana>` freezes a one-off wall-clock timezone on the loop record",
81145
81813
  " - `--confirm` persists the first wall-clock loop after reviewing the confirmation output",
81814
+ ` - advanced: \`${LOOP_START_FLAG} <none|brief|full>\` overrides the default scheduled loop-start notification behavior for that recurring loop`,
81815
+ " - advanced: `--progress <count>` overrides loop agent progress-message injection; `0` disables progress messages, and omitting the flag inherits the normal clisbot prompt config",
81146
81816
  "",
81147
81817
  "Examples:",
81148
81818
  ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI")}`,
@@ -81152,7 +81822,8 @@ function renderLoopsCreateHelp() {
81152
81822
  "Behavior:",
81153
81823
  " - create without `--sender` fails by design",
81154
81824
  " - the `--sender` platform must match `--channel`",
81155
- " - recurring CLI-created loops persist creator metadata into the session store"
81825
+ " - recurring CLI-created loops persist creator metadata into the session store",
81826
+ " - CLI-created loop prompts inherit the normal clisbot prompt config unless `--progress <count>` is provided"
81156
81827
  ].join(`
81157
81828
  `);
81158
81829
  }
@@ -81363,6 +82034,7 @@ async function getScopedLoopCounts(params) {
81363
82034
  // src/control/loops-cli.ts
81364
82035
  var LOOP_BUSY_RETRY_MS = 250;
81365
82036
  var LOOP_CONFIRM_FLAG = "--confirm";
82037
+ var LOOP_PROGRESS_FLAG = "--progress";
81366
82038
  var LOOP_SENDER_FLAG = "--sender";
81367
82039
  var LOOP_SENDER_NAME_FLAG = "--sender-name";
81368
82040
  var LOOP_SENDER_HANDLE_FLAG = "--sender-handle";
@@ -81522,7 +82194,9 @@ async function waitForSessionIdle(agentService, target) {
81522
82194
  }
81523
82195
  async function executeCountLoop(params) {
81524
82196
  const agentService = new AgentService(params.state.loadedConfig);
81525
- const builtPrompt = params.context.buildLoopPromptText(params.promptText);
82197
+ const builtPrompt = params.context.buildLoopPromptText(params.promptText, params.progressMessages == null ? undefined : {
82198
+ maxProgressMessagesOverride: params.progressMessages
82199
+ });
81526
82200
  console.log(renderLoopStartedMessage({
81527
82201
  mode: "times",
81528
82202
  count: params.count,
@@ -81576,8 +82250,35 @@ function stripLoopCreatorArgs(args) {
81576
82250
  }
81577
82251
  return remaining;
81578
82252
  }
82253
+ function parseLoopProgress(args) {
82254
+ const raw = parseOptionValue3(args, LOOP_PROGRESS_FLAG);
82255
+ if (raw == null) {
82256
+ return;
82257
+ }
82258
+ const parsed = Number.parseInt(raw, 10);
82259
+ if (!Number.isFinite(parsed) || String(parsed) !== raw.trim() || parsed < 0) {
82260
+ throw new Error(`${LOOP_PROGRESS_FLAG} must be a non-negative integer.`);
82261
+ }
82262
+ return parsed;
82263
+ }
82264
+ function stripLoopProgressArgs(args) {
82265
+ const remaining = [];
82266
+ for (let index = 0;index < args.length; index += 1) {
82267
+ const current = args[index];
82268
+ if (current === "--") {
82269
+ remaining.push(...args.slice(index));
82270
+ break;
82271
+ }
82272
+ if (current === LOOP_PROGRESS_FLAG) {
82273
+ index += 1;
82274
+ continue;
82275
+ }
82276
+ remaining.push(current);
82277
+ }
82278
+ return remaining;
82279
+ }
81579
82280
  function parseCreateExpression(rawArgs, explicitCreateSubcommand) {
81580
- const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs)));
82281
+ const expressionArgs = stripLoopContextArgs(stripLoopCreatorArgs(stripLoopProgressArgs(stripConfirmFlag(explicitCreateSubcommand ? rawArgs.slice(1) : rawArgs))));
81581
82282
  const expression = expressionArgs.join(" ").trim();
81582
82283
  if (!expression) {
81583
82284
  throw new Error("Loop creation requires an interval, count, or schedule expression.");
@@ -81657,6 +82358,7 @@ function requireValidIntervalLoop(parsed) {
81657
82358
  async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand) {
81658
82359
  const confirm = hasFlag4(rawArgs, LOOP_CONFIRM_FLAG);
81659
82360
  const loopTimezone = parseLoopTimezone(rawArgs);
82361
+ const progressMessages = parseLoopProgress(rawArgs);
81660
82362
  const expression = parseCreateExpression(rawArgs, explicitCreateSubcommand);
81661
82363
  const parsed = parseCreateCommand(expression);
81662
82364
  let addressing = parseAddressing(rawArgs);
@@ -81686,7 +82388,8 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
81686
82388
  maxActiveLoops,
81687
82389
  expression,
81688
82390
  confirm,
81689
- loopTimezone
82391
+ loopTimezone,
82392
+ progressMessages
81690
82393
  };
81691
82394
  }
81692
82395
  addressing = await prepareLoopCreateAddressing({
@@ -81717,7 +82420,8 @@ async function resolveLoopCreateRequest(state, rawArgs, explicitCreateSubcommand
81717
82420
  maxActiveLoops,
81718
82421
  expression,
81719
82422
  confirm,
81720
- loopTimezone
82423
+ loopTimezone,
82424
+ progressMessages
81721
82425
  };
81722
82426
  }
81723
82427
  function buildLoopSurfaceBinding2(request) {
@@ -81746,9 +82450,10 @@ function buildRecurringLoopCreateBase(state, request) {
81746
82450
  function buildRecurringLoopPromptMetadata(request) {
81747
82451
  return {
81748
82452
  promptText: request.resolvedPrompt.text,
81749
- canonicalPromptText: request.resolvedPrompt.text,
81750
82453
  promptSummary: summarizeLoopPrompt(request.resolvedPrompt.text, request.resolvedPrompt.maintenancePrompt),
81751
82454
  promptSource: request.resolvedPrompt.maintenancePrompt ? "LOOP.md" : "custom",
82455
+ progressMessages: request.progressMessages,
82456
+ loopStart: request.parsed.mode === "times" ? undefined : request.parsed.loopStart,
81752
82457
  maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
81753
82458
  createdBy: request.creator.providerId,
81754
82459
  sender: request.creator,
@@ -81888,7 +82593,8 @@ async function createLoop(state, rawArgs, options = {}) {
81888
82593
  context: request.deliveryContext ?? request.context,
81889
82594
  promptText: request.resolvedPrompt.text,
81890
82595
  count: request.parsed.count,
81891
- maintenancePrompt: request.resolvedPrompt.maintenancePrompt
82596
+ maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
82597
+ progressMessages: request.progressMessages
81892
82598
  });
81893
82599
  return;
81894
82600
  }
@@ -82254,6 +82960,11 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
82254
82960
  var QUEUE_SENDER_FLAG = "--sender";
82255
82961
  var QUEUE_SENDER_NAME_FLAG = "--sender-name";
82256
82962
  var QUEUE_SENDER_HANDLE_FLAG = "--sender-handle";
82963
+ var defaultQueueCliDependencies = {
82964
+ print: (text) => console.log(text),
82965
+ warn: (text) => console.warn(text),
82966
+ sendQueueCreatedNotification: sendQueueCreatedNotificationToSurface
82967
+ };
82257
82968
  function getEditableConfigPath9() {
82258
82969
  return process.env.CLISBOT_CONFIG_PATH;
82259
82970
  }
@@ -82411,7 +83122,6 @@ function resolveProtectedControlMutationRule(state, agentId, sender) {
82411
83122
  function createQueueItemForContext(params) {
82412
83123
  return createStoredQueueItem({
82413
83124
  promptText: params.promptText,
82414
- canonicalPromptText: params.promptText,
82415
83125
  protectedControlMutationRule: resolveProtectedControlMutationRule(params.state, params.context.sessionTarget.agentId, params.sender),
82416
83126
  promptSummary: params.promptText,
82417
83127
  createdBy: params.sender.providerId,
@@ -82419,6 +83129,56 @@ function createQueueItemForContext(params) {
82419
83129
  surfaceBinding: buildQueueSurfaceBinding(params.context)
82420
83130
  });
82421
83131
  }
83132
+ function renderQueueCreatedNotification(params) {
83133
+ const queueLine = params.positionAhead > 0 ? `Queued \`${params.queueId}\`: ${params.positionAhead} ahead.` : `Queued \`${params.queueId}\`.`;
83134
+ return `${queueLine}
83135
+
83136
+ ${params.promptText.trim()}`;
83137
+ }
83138
+ async function getQueuePositionAhead(state, sessionKey, itemId) {
83139
+ const queues = await state.sessionState.listQueuedItems({
83140
+ sessionKey,
83141
+ statuses: ["pending", "running"]
83142
+ });
83143
+ const index = queues.findIndex((item) => item.id === itemId);
83144
+ return index >= 0 ? index : 0;
83145
+ }
83146
+ function buildQueueCreatedMessageCommand(params) {
83147
+ return {
83148
+ action: "send",
83149
+ channel: params.context.channel,
83150
+ account: params.context.botId,
83151
+ target: params.context.target,
83152
+ message: params.text,
83153
+ threadId: params.context.threadId,
83154
+ remove: false,
83155
+ pollOptions: [],
83156
+ forceDocument: false,
83157
+ silent: false,
83158
+ progress: false,
83159
+ final: false,
83160
+ json: false,
83161
+ inputFormat: "plain",
83162
+ renderMode: "none"
83163
+ };
83164
+ }
83165
+ async function sendQueueCreatedNotificationToSurface(params) {
83166
+ if (!params.item.surfaceBinding) {
83167
+ return;
83168
+ }
83169
+ const loadedConfig = await loadConfig(params.state.configPath, {
83170
+ materializeChannels: [params.context.channel]
83171
+ });
83172
+ const plugin = listChannelPlugins().find((entry) => entry.id === params.context.channel);
83173
+ if (!plugin) {
83174
+ throw new Error(`Unsupported queue notification channel: ${params.context.channel}`);
83175
+ }
83176
+ await plugin.runMessageCommand(loadedConfig, buildQueueCreatedMessageCommand({
83177
+ context: params.context,
83178
+ text: params.text
83179
+ }));
83180
+ await params.state.sessionState.recordConversationReply(params.resolved);
83181
+ }
82422
83182
  function renderQueueInventory(params) {
82423
83183
  const lines = [
82424
83184
  `Queue ${params.commandLabel}`,
@@ -82433,20 +83193,20 @@ function renderQueueInventory(params) {
82433
83193
  return lines.join(`
82434
83194
  `);
82435
83195
  }
82436
- async function listQueues(state, addressing, commandLabel) {
83196
+ async function listQueues(state, addressing, commandLabel, deps) {
82437
83197
  const context = addressing.channel || addressing.target ? resolveScopedContext(state, addressing) : undefined;
82438
83198
  const sessionKey = context?.sessionTarget.sessionKey;
82439
83199
  const queues = await state.sessionState.listQueuedItems({
82440
83200
  sessionKey,
82441
83201
  statuses: commandLabel === "list" ? ["pending"] : ["pending", "running"]
82442
83202
  });
82443
- console.log(renderQueueInventory({
83203
+ deps.print(renderQueueInventory({
82444
83204
  commandLabel,
82445
83205
  sessionStorePath: state.sessionStorePath,
82446
83206
  queues
82447
83207
  }));
82448
83208
  }
82449
- async function createQueue(state, args) {
83209
+ async function createQueue(state, args, deps) {
82450
83210
  const addressing = parseQueueCliAddressing(args);
82451
83211
  const promptText = stripQueueArgs(args.slice(1)).join(" ").trim();
82452
83212
  if (!promptText) {
@@ -82467,18 +83227,30 @@ async function createQueue(state, args) {
82467
83227
  sender
82468
83228
  });
82469
83229
  await state.sessionState.setQueuedItem(resolved, item);
82470
- console.log(`Queued prompt \`${item.id}\` for \`${context.sessionTarget.sessionKey}\`.`);
83230
+ const positionAhead = await getQueuePositionAhead(state, context.sessionTarget.sessionKey, item.id);
83231
+ const text = renderQueueCreatedNotification({ queueId: item.id, positionAhead, promptText });
83232
+ await deps.sendQueueCreatedNotification({
83233
+ state,
83234
+ context,
83235
+ resolved,
83236
+ item,
83237
+ positionAhead,
83238
+ text
83239
+ }).catch((error) => {
83240
+ deps.warn(`Queued prompt ${item.id}, but surface acknowledgement failed: ${String(error)}`);
83241
+ });
83242
+ deps.print(`Queued prompt \`${item.id}\` for \`${context.sessionTarget.sessionKey}\`.`);
82471
83243
  }
82472
- async function clearQueues(state, addressing) {
83244
+ async function clearQueues(state, addressing, deps) {
82473
83245
  if (addressing.all) {
82474
83246
  const cleared2 = await state.sessionState.clearAllPendingQueuedItems();
82475
- console.log(`Cleared ${cleared2.length} pending queued prompt${cleared2.length === 1 ? "" : "s"} across the whole app.`);
83247
+ deps.print(`Cleared ${cleared2.length} pending queued prompt${cleared2.length === 1 ? "" : "s"} across the whole app.`);
82476
83248
  return;
82477
83249
  }
82478
83250
  const context = resolveScopedContext(state, addressing);
82479
83251
  const sessionKey = context.sessionTarget.sessionKey;
82480
83252
  const cleared = await state.sessionState.clearPendingQueuedItemsForSessionKey(sessionKey);
82481
- console.log(`Cleared ${cleared.length} pending queued prompt${cleared.length === 1 ? "" : "s"} for \`${sessionKey}\`.`);
83253
+ deps.print(`Cleared ${cleared.length} pending queued prompt${cleared.length === 1 ? "" : "s"} for \`${sessionKey}\`.`);
82482
83254
  }
82483
83255
  function renderQueuesHelp() {
82484
83256
  return [
@@ -82499,23 +83271,24 @@ function renderQueuesHelp() {
82499
83271
  ].join(`
82500
83272
  `);
82501
83273
  }
82502
- async function runQueuesCli(args) {
83274
+ async function runQueuesCli(args, dependencies = {}) {
83275
+ const deps = { ...defaultQueueCliDependencies, ...dependencies };
82503
83276
  if (args[0] === "--help" || args[0] === "help" || args.length === 0) {
82504
- console.log(renderQueuesHelp());
83277
+ deps.print(renderQueuesHelp());
82505
83278
  return;
82506
83279
  }
82507
83280
  const command = args[0];
82508
83281
  const state = await loadQueueControlState();
82509
83282
  if (command === "list" || command === "status") {
82510
- await listQueues(state, parseQueueCliAddressing(args.slice(1)), command);
83283
+ await listQueues(state, parseQueueCliAddressing(args.slice(1)), command, deps);
82511
83284
  return;
82512
83285
  }
82513
83286
  if (command === "create") {
82514
- await createQueue(state, args);
83287
+ await createQueue(state, args, deps);
82515
83288
  return;
82516
83289
  }
82517
83290
  if (command === "clear") {
82518
- await clearQueues(state, parseQueueCliAddressing(args.slice(1)));
83291
+ await clearQueues(state, parseQueueCliAddressing(args.slice(1)), deps);
82519
83292
  return;
82520
83293
  }
82521
83294
  throw new Error(`Unknown queues subcommand: ${command}`);
@@ -83280,13 +84053,37 @@ function sortRunnerSessionMetadataNewestFirst(entries) {
83280
84053
  return left.sessionName.localeCompare(right.sessionName);
83281
84054
  });
83282
84055
  }
84056
+ function deriveRunnerSessionIdentity(params) {
84057
+ const storedSessionId = params.entry?.sessionId?.trim() || undefined;
84058
+ const liveSessionId = params.liveSessionId?.trim() || undefined;
84059
+ const sessionId = liveSessionId ?? storedSessionId;
84060
+ return {
84061
+ sessionId,
84062
+ sessionIdPersistence: sessionId && sessionId === storedSessionId ? "persisted" : sessionId ? "not-persisted-yet" : undefined,
84063
+ storedSessionId
84064
+ };
84065
+ }
84066
+ function parseRunnerSessionIdFromSnapshot(loadedConfig, entry, snapshot) {
84067
+ if (!entry) {
84068
+ return;
84069
+ }
84070
+ const resolved = resolveAgentTarget(loadedConfig, {
84071
+ agentId: entry.agentId,
84072
+ sessionKey: entry.sessionKey
84073
+ });
84074
+ const pattern = resolved.runner.sessionId.capture.pattern?.trim();
84075
+ if (!pattern) {
84076
+ return;
84077
+ }
84078
+ return parseRunnerSessionId(snapshot, pattern) ?? undefined;
84079
+ }
83283
84080
  async function listRunnerSessions(loadedConfig) {
83284
84081
  const sessionStore = new SessionStore(resolveSessionStorePath(loadedConfig));
83285
84082
  const metadata = buildRunnerSessionMetadata(loadedConfig, await sessionStore.list());
83286
84083
  const sessionByName = new Map(metadata.map((item) => [item.sessionName, item.entry]));
83287
84084
  const tmux = new TmuxClient(loadedConfig.raw.tmux.socketPath);
83288
84085
  const liveSessionNames = new Set(await tmux.listSessions());
83289
- return [...liveSessionNames].sort((left, right) => {
84086
+ const orderedSessionNames = [...liveSessionNames].sort((left, right) => {
83290
84087
  const leftEntry = sessionByName.get(left);
83291
84088
  const rightEntry = sessionByName.get(right);
83292
84089
  const leftPromptAt = leftEntry?.lastAdmittedPromptAt ?? 0;
@@ -83300,12 +84097,19 @@ async function listRunnerSessions(loadedConfig) {
83300
84097
  return rightUpdatedAt - leftUpdatedAt;
83301
84098
  }
83302
84099
  return left.localeCompare(right);
83303
- }).map((sessionName, index) => ({
83304
- index: index + 1,
83305
- sessionName,
83306
- live: true,
83307
- entry: sessionByName.get(sessionName)
83308
- }));
84100
+ });
84101
+ return orderedSessionNames.map((sessionName, index) => {
84102
+ const entry = sessionByName.get(sessionName);
84103
+ return {
84104
+ index: index + 1,
84105
+ sessionName,
84106
+ live: true,
84107
+ entry,
84108
+ identity: deriveRunnerSessionIdentity({
84109
+ entry
84110
+ })
84111
+ };
84112
+ });
83309
84113
  }
83310
84114
 
83311
84115
  // src/control/runner-cli.ts
@@ -83318,6 +84122,7 @@ var SMOKE_SCENARIOS = [
83318
84122
  "recover_after_runner_loss"
83319
84123
  ];
83320
84124
  var SMOKE_SUITES = ["launch-trio"];
84125
+ var MISSING_STORED_SESSION_ID_TEXT = "not stored";
83321
84126
  function parseRepeatedOption4(args, name) {
83322
84127
  const values = [];
83323
84128
  for (let index = 0;index < args.length; index += 1) {
@@ -83428,7 +84233,7 @@ function renderRunnerHelp() {
83428
84233
  ` ${renderCliCommand("runner smoke --backend all --suite launch-trio [--workspace <path>] [--agent <id>] [--artifact-dir <path>] [--timeout-ms <n>] [--keep-session] [--json]")}`,
83429
84234
  "",
83430
84235
  "Operator session debugging:",
83431
- " - `list` shows current tmux runner sessions, newest admitted turn first when known, plus stored sessionId/state when available",
84236
+ " - `list` shows current tmux runner sessions, newest admitted turn first when known, plus sessionId and persistence state when available",
83432
84237
  " - `inspect` captures one snapshot; default tail is 100 lines",
83433
84238
  " - `--index <n>` selects the 1-based order printed by `runner list`",
83434
84239
  " - `watch --latest` follows the session that most recently admitted a new prompt",
@@ -83604,10 +84409,11 @@ function renderWatchFrame(params) {
83604
84409
  "",
83605
84410
  `session: ${params.sessionName}`,
83606
84411
  params.agentId ? `agent: ${params.agentId}` : null,
83607
- params.sessionKey ? `sessionKey: ${params.sessionKey}` : null,
84412
+ `sessionId: ${params.sessionId?.trim() || MISSING_STORED_SESSION_ID_TEXT}`,
84413
+ `sessionIdPersistence: ${params.sessionIdPersistence ?? "not stored yet"}`,
84414
+ params.storedSessionId && params.storedSessionId !== params.sessionId ? `storedSessionId: ${params.storedSessionId}` : null,
83608
84415
  `lines: ${params.lines}`,
83609
- `intervalMs: ${params.intervalMs}`,
83610
- `status: ${params.status}`,
84416
+ `state: ${params.state}`,
83611
84417
  "",
83612
84418
  params.snapshot.trimEnd() || "(empty pane)"
83613
84419
  ].filter((line) => Boolean(line)).join(`
@@ -83644,21 +84450,20 @@ function renderRunnerListSession(session) {
83644
84450
  if (!session.entry) {
83645
84451
  return [
83646
84452
  prefix,
83647
- " sessionId: none",
83648
- " state: unmanaged",
83649
- " live: yes"
84453
+ ` sessionId: ${MISSING_STORED_SESSION_ID_TEXT}`,
84454
+ " state: unmanaged"
83650
84455
  ].join(`
83651
84456
  `);
83652
84457
  }
83653
84458
  return [
83654
84459
  prefix,
83655
84460
  ` agent: ${session.entry.agentId}`,
83656
- ` sessionKey: ${session.entry.sessionKey}`,
83657
- ` sessionId: ${session.entry.sessionId?.trim() || "none"}`,
84461
+ ` sessionId: ${session.identity?.sessionId?.trim() || MISSING_STORED_SESSION_ID_TEXT}`,
84462
+ ` sessionIdPersistence: ${session.identity?.sessionIdPersistence ?? "not stored yet"}`,
84463
+ session.identity?.storedSessionId && session.identity.storedSessionId !== session.identity.sessionId ? ` storedSessionId: ${session.identity.storedSessionId}` : null,
83658
84464
  ` state: ${session.entry.runtime?.state ?? "no-runtime"}`,
83659
- ` live: ${session.live ? "yes" : "no"}`,
83660
84465
  ` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
83661
- ].join(`
84466
+ ].filter((line) => Boolean(line)).join(`
83662
84467
  `);
83663
84468
  }
83664
84469
  function resolveIndexedSession(sessions, index) {
@@ -83697,6 +84502,7 @@ async function resolveWatchSelection(options) {
83697
84502
  const sessionMetadata = buildRunnerSessionMetadata(context.loadedConfig, context.entries);
83698
84503
  if (options.sessionName) {
83699
84504
  return {
84505
+ loadedConfig: context.loadedConfig,
83700
84506
  sessionName: options.sessionName,
83701
84507
  metadata: sessionMetadata.find((item) => item.sessionName === options.sessionName) ?? null,
83702
84508
  tmux: context.tmux
@@ -83705,6 +84511,7 @@ async function resolveWatchSelection(options) {
83705
84511
  if (options.index != null) {
83706
84512
  const selected = resolveIndexedSession(await listRunnerSessions(context.loadedConfig), options.index);
83707
84513
  return {
84514
+ loadedConfig: context.loadedConfig,
83708
84515
  sessionName: selected.sessionName,
83709
84516
  metadata: sessionMetadata.find((item) => item.sessionName === selected.sessionName) ?? null,
83710
84517
  tmux: context.tmux
@@ -83716,6 +84523,7 @@ async function resolveWatchSelection(options) {
83716
84523
  throw new CliCommandError(`No admitted prompt is recorded yet. Use ${renderCliCommand("runner watch --next", { inline: true })} or watch a named session.`, 1);
83717
84524
  }
83718
84525
  return {
84526
+ loadedConfig: context.loadedConfig,
83719
84527
  sessionName: latest.sessionName,
83720
84528
  metadata: latest,
83721
84529
  tmux: context.tmux
@@ -83735,6 +84543,7 @@ async function resolveWatchSelection(options) {
83735
84543
  });
83736
84544
  if (admittedAfterBaseline[0]) {
83737
84545
  return {
84546
+ loadedConfig: context.loadedConfig,
83738
84547
  sessionName: admittedAfterBaseline[0].sessionName,
83739
84548
  metadata: admittedAfterBaseline[0],
83740
84549
  tmux: context.tmux
@@ -83765,13 +84574,18 @@ async function runWatchCli(args) {
83765
84574
  throw error;
83766
84575
  }
83767
84576
  }
84577
+ const identity = deriveRunnerSessionIdentity({
84578
+ entry: selection.metadata?.entry,
84579
+ liveSessionId: parseRunnerSessionIdFromSnapshot(selection.loadedConfig, selection.metadata?.entry, snapshot)
84580
+ });
83768
84581
  const frame = renderWatchFrame({
83769
84582
  sessionName: selection.sessionName,
83770
- sessionKey: selection.metadata?.entry.sessionKey,
84583
+ sessionId: identity.sessionId,
84584
+ sessionIdPersistence: identity.sessionIdPersistence,
84585
+ storedSessionId: identity.storedSessionId,
83771
84586
  agentId: selection.metadata?.entry.agentId,
83772
84587
  lines: options.lines,
83773
- intervalMs: options.intervalMs,
83774
- status,
84588
+ state: status,
83775
84589
  snapshot
83776
84590
  });
83777
84591
  if (process.stdout.isTTY) {
@@ -83887,6 +84701,7 @@ var GITHUB_RAW_BASE = "https://raw.githubusercontent.com/longbkit/clisbot/main";
83887
84701
  function renderUpdateHelp() {
83888
84702
  return [
83889
84703
  `${renderCliCommand("update")} / ${renderCliCommand("update --help")}`,
84704
+ "Start here for any clisbot install or update request.",
83890
84705
  "Prints this guide only. Direct update is not supported yet.",
83891
84706
  "A bot can use this guide to update itself.",
83892
84707
  "",
@@ -83897,21 +84712,30 @@ function renderUpdateHelp() {
83897
84712
  "",
83898
84713
  "Flow:",
83899
84714
  ` 1. ${renderCliCommand("status")}`,
83900
- " 2. Read docs in priority order.",
84715
+ " 2. Read docs in priority order and follow them before installing.",
83901
84716
  " 3. npm install -g clisbot@<target> && clisbot restart",
83902
84717
  ` 4. ${renderCliCommand("status")}`,
83903
- " 5. Report version, health, manual action, and useful release-note highlights.",
84718
+ " 5. Report version, health, manual action, and useful release highlights.",
83904
84719
  "",
83905
84720
  "Docs, read in order:",
83906
84721
  ` 1. Migration index: ${GITHUB_RAW_BASE}/docs/migrations/index.md`,
83907
84722
  " If Manual action: required, follow its runbook. If none, continue.",
83908
- ` 2. Release quick info: ${GITHUB_RAW_BASE}/docs/releases/README.md`,
83909
- " Use for release-note map and current release highlights.",
83910
- ` 3. Update guide: ${GITHUB_RAW_BASE}/docs/update/README.md`,
83911
- " Use for release notes, useful new features, things to try, and user-guide links.",
83912
- " 4. Full docs: https://github.com/longbkit/clisbot/tree/main/docs",
84723
+ ` 2. Update guide: ${GITHUB_RAW_BASE}/docs/updates/update-guide.md`,
84724
+ " Use for target choice, install flow, verification, and wrong-publish recovery.",
84725
+ ` 3. Release notes: ${GITHUB_RAW_BASE}/docs/releases/README.md`,
84726
+ " Use for the canonical version map and full version notes.",
84727
+ ` 4. Release guides: ${GITHUB_RAW_BASE}/docs/updates/README.md`,
84728
+ " Use for shorter catch-up notes: what changed, what to try, and what to watch.",
84729
+ " 5. Full docs: https://github.com/longbkit/clisbot/tree/main/docs",
83913
84730
  " Use for deep questions. If needed, fetch or clone docs and inspect relevant files.",
83914
84731
  "",
84732
+ "Recovery:",
84733
+ " - If a version was published by mistake, publish the corrected target or tag first.",
84734
+ " - Then deprecate the wrong version.",
84735
+ " - Start with `npm login` in an attached session.",
84736
+ " - If npm returns a browser approval URL, keep that same session open and continue it after approval.",
84737
+ " - If the write command still returns EOTP, ask the operator for a current OTP and rerun the exact command with --otp=<code>.",
84738
+ "",
83915
84739
  "Rules:",
83916
84740
  " - Use npm dist-tags, not highest semver.",
83917
84741
  " - Stable/latest is default; beta only when the user asks.",
@@ -84220,7 +85044,7 @@ function appendBootstrapGuidance(lines, summary) {
84220
85044
  lines.push(` Agent ${agent.id} still needs bootstrap completion.`);
84221
85045
  lines.push(` workspace: ${agent.workspacePath}`);
84222
85046
  lines.push(" next: chat with the bot or open the workspace");
84223
- lines.push(` follow: BOOTSTRAP.md and the ${agent.bootstrapMode} personality files`);
85047
+ lines.push(" follow: BOOTSTRAP.md, AGENTS.md, and the rest of the seeded workspace files");
84224
85048
  }
84225
85049
  lines.push("");
84226
85050
  lines.push(" Next steps after bootstrap:");
@@ -84269,7 +85093,7 @@ function renderStartSummary(summary) {
84269
85093
  lines.push(` Example: ${renderCliCommand("start --cli codex --bot-type team")}`);
84270
85094
  lines.push(` Manual setup is still available with ${renderCliCommand("agents add ...", { inline: true })}.`);
84271
85095
  lines.push(...renderOperatorHelpLines(" "));
84272
- lines.push(" Bootstrap files will be seeded in the agent workspace. Review BOOTSTRAP.md, SOUL.md, USER.md, IDENTITY.md, and MEMORY.md.");
85096
+ lines.push(" Bootstrap files are optional. If you use `--bot-type`, clisbot seeds BOOTSTRAP.md, AGENTS.md, SOUL.md, USER.md, IDENTITY.md, and related files into the agent workspace.");
84273
85097
  return lines.join(`
84274
85098
  `);
84275
85099
  }
@@ -84465,7 +85289,7 @@ async function getRuntimeOperatorSummary(params) {
84465
85289
  },
84466
85290
  agentSummaries,
84467
85291
  channelSummaries,
84468
- activeRuns: await agentService.listActiveSessionRuntimes(),
85292
+ activeRuns: await agentService.listLiveSessionRuntimes(),
84469
85293
  configuredAgents: agentSummaries.length,
84470
85294
  bootstrapPendingAgents: agentSummaries.filter((item) => item.bootstrapState === "missing" || item.bootstrapState === "not-bootstrapped").length,
84471
85295
  bootstrappedAgents: agentSummaries.filter((item) => item.bootstrapState === "bootstrapped").length,
@@ -84547,7 +85371,10 @@ function renderBootstrapCommandHelp(commandName) {
84547
85371
  "Behavior:",
84548
85372
  ` - ${behavior}`,
84549
85373
  " - first-run agent bootstrap needs both `--cli` and `--bot-type`",
85374
+ " - `--bot-type` is a workspace-template choice, not a general runtime requirement for later agents",
84550
85375
  " - `--bot-type personal` maps to `personal-assistant`; `--bot-type team` maps to `team-assistant`",
85376
+ " - bootstrap seeds `AGENTS.md`, `BOOTSTRAP.md`, `IDENTITY.md`, and the rest of the workspace guidance files",
85377
+ " - Claude and Gemini bootstraps also create `CLAUDE.md` or `GEMINI.md` as symlinks to `AGENTS.md`",
84551
85378
  " - explicit credential flags only enable the channels and bots you named in this command",
84552
85379
  " - env-style values such as `SLACK_APP_TOKEN` or `${SLACK_APP_TOKEN}` stay env-backed in config",
84553
85380
  commandName === "start" ? " - literal token values without `--persist` stay runtime-only for this start invocation" : " - literal token values on `init` require `--persist` because no runtime exists yet",
@@ -85491,6 +86318,8 @@ async function withStartupTimeout(name, start2) {
85491
86318
  }
85492
86319
 
85493
86320
  // src/control/runtime-management-cli.ts
86321
+ var RESTART_STOP_STATUS_RECHECK_TIMEOUT_MS = 2000;
86322
+ var RESTART_STOP_STATUS_RECHECK_INTERVAL_MS = 100;
85494
86323
  function getOperatorConfigPath() {
85495
86324
  return expandHomePath(process.env.CLISBOT_CONFIG_PATH || DEFAULT_CONFIG_PATH);
85496
86325
  }
@@ -85727,11 +86556,36 @@ async function stop(hard = false) {
85727
86556
  console.log("clisbot stopped");
85728
86557
  printCommandOutcomeFooter("success");
85729
86558
  }
85730
- async function restart() {
85731
- await stopDetachedRuntime({
85732
- configPath: getOperatorConfigPath(),
85733
- hard: false
85734
- });
86559
+ async function restart(dependencies = {
86560
+ stopDetachedRuntime,
86561
+ getRuntimeStatus,
86562
+ sleep,
86563
+ warn: (message) => console.error(message)
86564
+ }) {
86565
+ const configPath = getOperatorConfigPath();
86566
+ try {
86567
+ await dependencies.stopDetachedRuntime({
86568
+ configPath,
86569
+ hard: false
86570
+ });
86571
+ } catch (error) {
86572
+ const status = await waitForStoppedRuntimeAfterStopError(configPath, dependencies);
86573
+ if (status.running) {
86574
+ throw error;
86575
+ }
86576
+ const message = error instanceof Error ? error.message : String(error);
86577
+ dependencies.warn(`warning: clisbot stop reported an error, but status now shows the service is stopped; continuing with start. Stop error: ${message}`);
86578
+ }
86579
+ }
86580
+ async function waitForStoppedRuntimeAfterStopError(configPath, dependencies) {
86581
+ const deadline = Date.now() + RESTART_STOP_STATUS_RECHECK_TIMEOUT_MS;
86582
+ while (true) {
86583
+ const status = await dependencies.getRuntimeStatus({ configPath });
86584
+ if (!status.running || Date.now() >= deadline) {
86585
+ return status;
86586
+ }
86587
+ await dependencies.sleep(RESTART_STOP_STATUS_RECHECK_INTERVAL_MS);
86588
+ }
85735
86589
  }
85736
86590
  async function status() {
85737
86591
  await printStatusSummary();