clisbot 0.1.46-beta.0 → 0.1.46-beta.1

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.
Files changed (2) hide show
  1. package/dist/main.js +494 -100
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -58902,10 +58902,12 @@ function parseCommandDurationMs(raw) {
58902
58902
  // src/agents/loop-command.ts
58903
58903
  var DEFAULT_LOOP_MAX_TIMES = 50;
58904
58904
  var LOOP_FORCE_FLAG = "--force";
58905
+ var LOOP_START_FLAG = "--loop-start";
58905
58906
  var LOOP_ALL_FLAG = "--all";
58906
58907
  var LOOP_APP_FLAG = "--app";
58907
58908
  var MIN_LOOP_INTERVAL_MS = 60000;
58908
58909
  var FORCE_LOOP_INTERVAL_MS = 5 * 60000;
58910
+ var LOOP_START_MODES = ["none", "brief", "full"];
58909
58911
  var LOOP_WEEKDAY_LABELS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
58910
58912
  var LOOP_WEEKDAY_ALIASES = {
58911
58913
  sun: 0,
@@ -58934,7 +58936,12 @@ function parseLoopSlashCommand(raw) {
58934
58936
  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
58937
  };
58936
58938
  }
58937
- const tokens = trimmed.split(/\s+/).filter(Boolean);
58939
+ const modifier = extractLoopStartModifier(trimmed);
58940
+ if ("error" in modifier) {
58941
+ return modifier;
58942
+ }
58943
+ const normalizedText = modifier.normalizedText;
58944
+ const tokens = modifier.normalizedText.split(/\s+/).filter(Boolean);
58938
58945
  const forceTokenIndexes = tokens.map((token, index) => token.toLowerCase() === LOOP_FORCE_FLAG ? index : -1).filter((index) => index >= 0);
58939
58946
  if (forceTokenIndexes.length > 1) {
58940
58947
  return {
@@ -58943,7 +58950,7 @@ function parseLoopSlashCommand(raw) {
58943
58950
  }
58944
58951
  const forceTokenIndex = forceTokenIndexes[0];
58945
58952
  const leadingToken = tokens[0] ?? "";
58946
- const everyDayMatch = trimmed.match(/^every\s+day\s+at\s+(\S+)(?:\s+(.*))?$/i);
58953
+ const everyDayMatch = normalizedText.match(/^every\s+day\s+at\s+(\S+)(?:\s+(.*))?$/i);
58947
58954
  if (everyDayMatch) {
58948
58955
  if (forceTokenIndex !== undefined) {
58949
58956
  return {
@@ -58956,7 +58963,7 @@ function parseLoopSlashCommand(raw) {
58956
58963
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
58957
58964
  };
58958
58965
  }
58959
- return {
58966
+ const parsed = {
58960
58967
  mode: "calendar",
58961
58968
  cadence: "daily",
58962
58969
  localTime: parsedTime.localTime,
@@ -58966,8 +58973,18 @@ function parseLoopSlashCommand(raw) {
58966
58973
  force: false,
58967
58974
  syntax: "calendar-at"
58968
58975
  };
58976
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
58977
+ if (validationError) {
58978
+ return {
58979
+ error: validationError
58980
+ };
58981
+ }
58982
+ return {
58983
+ ...parsed,
58984
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
58985
+ };
58969
58986
  }
58970
- const everyWeekdayMatch = trimmed.match(/^every\s+weekday\s+at\s+(\S+)(?:\s+(.*))?$/i);
58987
+ const everyWeekdayMatch = normalizedText.match(/^every\s+weekday\s+at\s+(\S+)(?:\s+(.*))?$/i);
58971
58988
  if (everyWeekdayMatch) {
58972
58989
  if (forceTokenIndex !== undefined) {
58973
58990
  return {
@@ -58980,7 +58997,7 @@ function parseLoopSlashCommand(raw) {
58980
58997
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
58981
58998
  };
58982
58999
  }
58983
- return {
59000
+ const parsed = {
58984
59001
  mode: "calendar",
58985
59002
  cadence: "weekday",
58986
59003
  localTime: parsedTime.localTime,
@@ -58990,8 +59007,18 @@ function parseLoopSlashCommand(raw) {
58990
59007
  force: false,
58991
59008
  syntax: "calendar-at"
58992
59009
  };
59010
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59011
+ if (validationError) {
59012
+ return {
59013
+ error: validationError
59014
+ };
59015
+ }
59016
+ return {
59017
+ ...parsed,
59018
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59019
+ };
58993
59020
  }
58994
- const everyDayOfWeekMatch = trimmed.match(/^every\s+([a-z]+)\s+at\s+(\S+)(?:\s+(.*))?$/i);
59021
+ const everyDayOfWeekMatch = normalizedText.match(/^every\s+([a-z]+)\s+at\s+(\S+)(?:\s+(.*))?$/i);
58995
59022
  if (everyDayOfWeekMatch) {
58996
59023
  const dayOfWeek = resolveLoopDayOfWeek(everyDayOfWeekMatch[1] ?? "");
58997
59024
  if (dayOfWeek != null) {
@@ -59006,7 +59033,7 @@ function parseLoopSlashCommand(raw) {
59006
59033
  error: "Loop wall-clock time must use `HH:MM` in 24-hour format."
59007
59034
  };
59008
59035
  }
59009
- return {
59036
+ const parsed = {
59010
59037
  mode: "calendar",
59011
59038
  cadence: "day-of-week",
59012
59039
  dayOfWeek,
@@ -59017,6 +59044,16 @@ function parseLoopSlashCommand(raw) {
59017
59044
  force: false,
59018
59045
  syntax: "calendar-at"
59019
59046
  };
59047
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59048
+ if (validationError) {
59049
+ return {
59050
+ error: validationError
59051
+ };
59052
+ }
59053
+ return {
59054
+ ...parsed,
59055
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59056
+ };
59020
59057
  }
59021
59058
  }
59022
59059
  const leadingIntervalMs = parseCommandDurationMs(leadingToken);
@@ -59028,15 +59065,30 @@ function parseLoopSlashCommand(raw) {
59028
59065
  }
59029
59066
  const promptTokens = tokens.slice(forceTokenIndex === 1 ? 2 : 1);
59030
59067
  const promptText = promptTokens.join(" ").trim() || undefined;
59031
- return {
59068
+ const parsed = {
59032
59069
  mode: "interval",
59033
59070
  intervalMs: leadingIntervalMs,
59034
59071
  promptText,
59035
59072
  force: forceTokenIndex === 1,
59036
59073
  syntax: "leading-interval"
59037
59074
  };
59075
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59076
+ if (validationError) {
59077
+ return {
59078
+ error: validationError
59079
+ };
59080
+ }
59081
+ return {
59082
+ ...parsed,
59083
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59084
+ };
59038
59085
  }
59039
59086
  if (/^-?\d+$/.test(leadingToken)) {
59087
+ if (modifier.loopStart) {
59088
+ return {
59089
+ error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
59090
+ };
59091
+ }
59040
59092
  if (forceTokenIndex !== undefined) {
59041
59093
  return {
59042
59094
  error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
@@ -59057,8 +59109,13 @@ function parseLoopSlashCommand(raw) {
59057
59109
  syntax: "leading-count"
59058
59110
  };
59059
59111
  }
59060
- const trailingTimes = trimmed.match(/^(.*?)(?:\s+)?(-?\d+)\s+times$/i);
59112
+ const trailingTimes = normalizedText.match(/^(.*?)(?:\s+)?(-?\d+)\s+times$/i);
59061
59113
  if (trailingTimes) {
59114
+ if (modifier.loopStart) {
59115
+ return {
59116
+ error: `\`${LOOP_START_FLAG}\` is only supported for recurring interval and wall-clock loops.`
59117
+ };
59118
+ }
59062
59119
  if (forceTokenIndex !== undefined) {
59063
59120
  return {
59064
59121
  error: `\`${LOOP_FORCE_FLAG}\` is only supported for interval loops.`
@@ -59079,7 +59136,7 @@ function parseLoopSlashCommand(raw) {
59079
59136
  syntax: "trailing-times"
59080
59137
  };
59081
59138
  }
59082
- const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") : trimmed;
59139
+ const withoutTrailingForce = forceTokenIndex === tokens.length - 1 ? tokens.slice(0, -1).join(" ") : normalizedText;
59083
59140
  const normalizedEveryInput = withoutTrailingForce.trim();
59084
59141
  const hasTrailingForce = forceTokenIndex === tokens.length - 1;
59085
59142
  if (forceTokenIndex !== undefined && !hasTrailingForce) {
@@ -59096,13 +59153,23 @@ function parseLoopSlashCommand(raw) {
59096
59153
  };
59097
59154
  }
59098
59155
  const promptText = everyCompactClause[1]?.trim() || undefined;
59099
- return {
59156
+ const parsed = {
59100
59157
  mode: "interval",
59101
59158
  intervalMs,
59102
59159
  promptText,
59103
59160
  force: hasTrailingForce,
59104
59161
  syntax: "every-clause"
59105
59162
  };
59163
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59164
+ if (validationError) {
59165
+ return {
59166
+ error: validationError
59167
+ };
59168
+ }
59169
+ return {
59170
+ ...parsed,
59171
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59172
+ };
59106
59173
  }
59107
59174
  const everyClause = normalizedEveryInput.match(/^(.*?)(?:\s+)?every\s+(-?\d+)\s+([a-z]+)$/i);
59108
59175
  if (everyClause) {
@@ -59120,18 +59187,84 @@ function parseLoopSlashCommand(raw) {
59120
59187
  };
59121
59188
  }
59122
59189
  const promptText = everyClause[1]?.trim() || undefined;
59123
- return {
59190
+ const parsed = {
59124
59191
  mode: "interval",
59125
59192
  intervalMs,
59126
59193
  promptText,
59127
59194
  force: hasTrailingForce,
59128
59195
  syntax: "every-clause"
59129
59196
  };
59197
+ const validationError = validateLoopStartModifierPlacement(parsed, modifier, modifier.tokens.length);
59198
+ if (validationError) {
59199
+ return {
59200
+ error: validationError
59201
+ };
59202
+ }
59203
+ return {
59204
+ ...parsed,
59205
+ ...modifier.loopStart ? { loopStart: modifier.loopStart } : {}
59206
+ };
59130
59207
  }
59131
59208
  return {
59132
59209
  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
59210
  };
59134
59211
  }
59212
+ function extractLoopStartModifier(raw) {
59213
+ const tokens = raw.split(/\s+/).filter(Boolean);
59214
+ const indexes = tokens.map((token, index) => token.trim().toLowerCase() === LOOP_START_FLAG ? index : -1).filter((index) => index >= 0);
59215
+ if (indexes.length > 1) {
59216
+ return {
59217
+ error: `Loop accepts at most one \`${LOOP_START_FLAG}\` flag.`
59218
+ };
59219
+ }
59220
+ const flagIndex = indexes[0];
59221
+ if (flagIndex == null) {
59222
+ return {
59223
+ normalizedText: raw,
59224
+ tokens
59225
+ };
59226
+ }
59227
+ const rawMode = tokens[flagIndex + 1]?.trim().toLowerCase();
59228
+ if (!rawMode) {
59229
+ return {
59230
+ error: `Loop requires \`${LOOP_START_FLAG} <none|brief|full>\`.`
59231
+ };
59232
+ }
59233
+ if (!LOOP_START_MODES.includes(rawMode)) {
59234
+ return {
59235
+ error: `\`${LOOP_START_FLAG}\` must be one of \`none\`, \`brief\`, or \`full\`.`
59236
+ };
59237
+ }
59238
+ const strippedTokens = tokens.filter((_, index) => index !== flagIndex && index !== flagIndex + 1);
59239
+ return {
59240
+ normalizedText: strippedTokens.join(" "),
59241
+ tokens,
59242
+ flagIndex,
59243
+ loopStart: rawMode
59244
+ };
59245
+ }
59246
+ function validateLoopStartModifierPlacement(parsed, modifier, tokenCount) {
59247
+ if (!modifier.loopStart || modifier.flagIndex == null) {
59248
+ return;
59249
+ }
59250
+ if (parsed.mode === "calendar") {
59251
+ if (modifier.flagIndex === 4) {
59252
+ return;
59253
+ }
59254
+ 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\`.`;
59255
+ }
59256
+ if (parsed.syntax === "leading-interval") {
59257
+ const expectedIndex = parsed.force ? 2 : 1;
59258
+ if (modifier.flagIndex === expectedIndex) {
59259
+ return;
59260
+ }
59261
+ 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\`.`;
59262
+ }
59263
+ if (modifier.flagIndex === tokenCount - 2) {
59264
+ return;
59265
+ }
59266
+ 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\`.`;
59267
+ }
59135
59268
  function formatLoopIntervalShort(intervalMs) {
59136
59269
  if (intervalMs % (60 * 60000) === 0) {
59137
59270
  return `${intervalMs / (60 * 60000)}h`;
@@ -59161,7 +59294,8 @@ function renderLoopHelpLines() {
59161
59294
  "- `/loop /codereview 3 times`: run the slash command 3 times",
59162
59295
  "- `/loop status`: show active loops for this session",
59163
59296
  `- \`/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}\``
59297
+ `- 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}\``,
59298
+ `- 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
59299
  ];
59166
59300
  }
59167
59301
  function hasLoopFlag(raw, flag) {
@@ -65882,12 +66016,19 @@ function parseAgentCommand(text, options = {}) {
65882
66016
  if (lowered === "loop") {
65883
66017
  const loopText = withoutSlash.slice(command.length).trim();
65884
66018
  const loweredLoopText = loopText.toLowerCase();
66019
+ const hasLoopStartOverride = hasLoopFlag(loopText, LOOP_START_FLAG);
65885
66020
  if (!loweredLoopText || loweredLoopText === "help") {
65886
66021
  return {
65887
66022
  type: "control",
65888
66023
  name: "loop-help"
65889
66024
  };
65890
66025
  }
66026
+ if (hasLoopStartOverride && (loweredLoopText.startsWith("help ") || loweredLoopText === "status" || loweredLoopText.startsWith("status "))) {
66027
+ return {
66028
+ type: "loop-error",
66029
+ message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
66030
+ };
66031
+ }
65891
66032
  if (loweredLoopText === "status") {
65892
66033
  return {
65893
66034
  type: "loop-control",
@@ -65896,6 +66037,12 @@ function parseAgentCommand(text, options = {}) {
65896
66037
  }
65897
66038
  if (loweredLoopText === "cancel" || loweredLoopText.startsWith("cancel ")) {
65898
66039
  const cancelArgs = loopText.slice("cancel".length).trim();
66040
+ if (hasLoopStartOverride) {
66041
+ return {
66042
+ type: "loop-error",
66043
+ message: `\`${LOOP_START_FLAG}\` is only supported when creating recurring interval and wall-clock loops.`
66044
+ };
66045
+ }
65899
66046
  if (hasLoopFlag(cancelArgs, LOOP_FORCE_FLAG)) {
65900
66047
  return {
65901
66048
  type: "loop-error",
@@ -66011,7 +66158,7 @@ function renderAgentControlSlashHelp() {
66011
66158
  "- `/detach`: stop live updates for this thread while still posting the final result here",
66012
66159
  "- `/watch every 30s [for 10m]`: post the latest state on an interval until the run settles or the watch window ends",
66013
66160
  "- `/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",
66161
+ "- `/new`: start a new session for this routed conversation and store the new session id",
66015
66162
  "- `/nudge`: send one extra Enter to the current tmux session without resending the prompt text",
66016
66163
  "- `/followup status`: show the current conversation follow-up policy",
66017
66164
  "- `/followup auto`: allow natural follow-up after the bot has replied in-thread",
@@ -67176,6 +67323,7 @@ function createStoredLoopBase(params) {
67176
67323
  protectedControlMutationRule: params.protectedControlMutationRule,
67177
67324
  promptSummary: params.promptSummary,
67178
67325
  promptSource: params.promptSource,
67326
+ loopStart: params.loopStart,
67179
67327
  createdBy: params.createdBy,
67180
67328
  sender: params.sender ?? deriveLegacyLoopSender({
67181
67329
  createdBy: params.createdBy,
@@ -67206,6 +67354,7 @@ function createStoredIntervalLoop(params) {
67206
67354
  protectedControlMutationRule: params.protectedControlMutationRule,
67207
67355
  promptSummary: params.promptSummary,
67208
67356
  promptSource: params.promptSource,
67357
+ loopStart: params.loopStart,
67209
67358
  createdBy: params.createdBy,
67210
67359
  sender: params.sender,
67211
67360
  surfaceBinding: params.surfaceBinding,
@@ -67236,6 +67385,7 @@ function createStoredCalendarLoop(params) {
67236
67385
  protectedControlMutationRule: params.protectedControlMutationRule,
67237
67386
  promptSummary: params.promptSummary,
67238
67387
  promptSource: params.promptSource,
67388
+ loopStart: params.loopStart,
67239
67389
  createdBy: params.createdBy,
67240
67390
  sender: params.sender,
67241
67391
  surfaceBinding: params.surfaceBinding,
@@ -67624,6 +67774,8 @@ class ManagedLoopController {
67624
67774
  }
67625
67775
 
67626
67776
  // src/agents/managed-queue-controller.ts
67777
+ var QUEUE_MESSAGE_TOOL_FINAL_POLL_MS = 250;
67778
+
67627
67779
  class ManagedQueueController {
67628
67780
  deps;
67629
67781
  queuedItems = new Map;
@@ -67709,12 +67861,29 @@ class ManagedQueueController {
67709
67861
  }
67710
67862
  }
67711
67863
  for (const persisted of persistedItems) {
67712
- if (persisted.status !== "pending" || this.queuedItems.has(persisted.id)) {
67864
+ if (persisted.status === "running") {
67865
+ await this.clearStaleRunningQueueItem(persisted);
67866
+ continue;
67867
+ }
67868
+ if (this.queuedItems.has(persisted.id)) {
67713
67869
  continue;
67714
67870
  }
67715
67871
  this.enqueuePersistedQueueItem(persisted);
67716
67872
  }
67717
67873
  }
67874
+ async clearStaleRunningQueueItem(item) {
67875
+ if (this.queuedItems.has(item.id)) {
67876
+ return;
67877
+ }
67878
+ const target = {
67879
+ agentId: item.agentId,
67880
+ sessionKey: item.sessionKey
67881
+ };
67882
+ if (await this.deps.hasBlockingActiveRun(target)) {
67883
+ return;
67884
+ }
67885
+ await this.removeManagedQueueItem(target, item);
67886
+ }
67718
67887
  persistQueueItem(resolved, item) {
67719
67888
  return this.deps.sessionState.countPendingQueuedItemsForSessionKey(resolved.sessionKey, {
67720
67889
  excludeId: item.id
@@ -67759,6 +67928,7 @@ class ManagedQueueController {
67759
67928
  item: next
67760
67929
  });
67761
67930
  await this.deps.sessionState.replaceQueuedItemIfPresent(this.deps.resolveTarget(target), next);
67931
+ return next;
67762
67932
  }
67763
67933
  async removeManagedQueueItem(target, item) {
67764
67934
  if (!item) {
@@ -67777,23 +67947,38 @@ class ManagedQueueController {
67777
67947
  item
67778
67948
  });
67779
67949
  const queued = this.deps.queue.enqueue(item.sessionKey, async () => {
67780
- await this.markQueueItemRunning(target, item);
67950
+ const runningItem = await this.markQueueItemRunning(target, item);
67781
67951
  await this.deps.surfaceRuntime.notifyManagedQueueStart(target, item);
67782
67952
  const promptText = await this.deps.surfaceRuntime.buildManagedQueuePrompt(item.agentId, item);
67783
- return this.deps.activeRuns.executePrompt(target, promptText, {
67953
+ const result = this.deps.activeRuns.executePrompt(target, promptText, {
67784
67954
  id: `queue:${item.id}`,
67785
67955
  mode: "live",
67786
67956
  onUpdate: async () => {
67787
67957
  return;
67788
67958
  }
67789
67959
  });
67960
+ let stopWaitingForToolFinal = false;
67961
+ try {
67962
+ const outcome = await Promise.race([
67963
+ result.then((update) => ({ kind: "result", update })),
67964
+ this.waitForMessageToolFinal(target, runningItem ?? item, () => stopWaitingForToolFinal).then((seen) => ({ kind: seen ? "message-tool-final" : "result" }))
67965
+ ]);
67966
+ if (outcome.kind === "message-tool-final") {
67967
+ return this.createMessageToolFinalQueueUpdate(target, item);
67968
+ }
67969
+ return await result;
67970
+ } finally {
67971
+ stopWaitingForToolFinal = true;
67972
+ }
67790
67973
  }, {
67791
67974
  id: item.id,
67792
67975
  createdAt: item.createdAt,
67793
67976
  text: item.promptSummary,
67794
67977
  canStart: async () => !await this.deps.hasBlockingActiveRun(target),
67795
67978
  onComplete: async (value) => {
67796
- await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
67979
+ if (!value.messageToolFinalAlreadySent && !await this.hasMessageToolFinalForQueueItem(target, item)) {
67980
+ await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
67981
+ }
67797
67982
  await this.removeManagedQueueItem(target, item);
67798
67983
  },
67799
67984
  onFailure: async (error) => {
@@ -67809,6 +67994,37 @@ class ManagedQueueController {
67809
67994
  console.error("queued prompt execution failed", error);
67810
67995
  });
67811
67996
  }
67997
+ async waitForMessageToolFinal(target, item, shouldStop) {
67998
+ while (!shouldStop()) {
67999
+ if (await this.hasMessageToolFinalForQueueItem(target, item)) {
68000
+ return true;
68001
+ }
68002
+ await sleep(QUEUE_MESSAGE_TOOL_FINAL_POLL_MS);
68003
+ }
68004
+ return false;
68005
+ }
68006
+ async hasMessageToolFinalForQueueItem(target, item) {
68007
+ const startedAt = this.queuedItems.get(item.id)?.item.startedAt ?? item.startedAt;
68008
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) {
68009
+ return false;
68010
+ }
68011
+ const runtime = await this.deps.sessionState.getSessionRuntime(target);
68012
+ return typeof runtime.messageToolFinalReplyAt === "number" && Number.isFinite(runtime.messageToolFinalReplyAt) && runtime.messageToolFinalReplyAt >= startedAt;
68013
+ }
68014
+ createMessageToolFinalQueueUpdate(target, item) {
68015
+ const resolved = this.deps.resolveTarget(target);
68016
+ return {
68017
+ status: "completed",
68018
+ agentId: target.agentId,
68019
+ sessionKey: target.sessionKey,
68020
+ sessionName: resolved.sessionName,
68021
+ workspacePath: resolved.workspacePath,
68022
+ snapshot: "",
68023
+ fullSnapshot: "",
68024
+ initialSnapshot: "",
68025
+ messageToolFinalAlreadySent: true
68026
+ };
68027
+ }
67812
68028
  }
67813
68029
 
67814
68030
  // src/agents/runner-service.ts
@@ -68089,18 +68305,37 @@ function isTimerDrivenStatusLine(line) {
68089
68305
  return isActiveTimerStatusLine(trimmed) || CLAUDE_WORKED_STATUS_PATTERN.test(trimmed);
68090
68306
  }
68091
68307
  function hasActiveTimerStatus(snapshot) {
68092
- return splitNormalizedLines(snapshot).some((line) => isActiveTimerStatusLine(line));
68308
+ return Boolean(extractLatestActiveTimerStatusLine(snapshot));
68093
68309
  }
68094
68310
  function extractLatestActiveTimerStatusLine(snapshot) {
68095
68311
  const lines = splitNormalizedLines(snapshot);
68096
68312
  for (let index = lines.length - 1;index >= 0; index -= 1) {
68097
68313
  const line = lines[index]?.trim() ?? "";
68098
- if (isActiveTimerStatusLine(line)) {
68314
+ if (isActiveTimerStatusLine(line) && isLiveTimerStatusLine(lines, index)) {
68099
68315
  return line;
68100
68316
  }
68101
68317
  }
68102
68318
  return "";
68103
68319
  }
68320
+ function isLiveTimerStatusLine(lines, timerIndex) {
68321
+ for (let index = timerIndex + 1;index < lines.length; index += 1) {
68322
+ const trimmed = lines[index]?.trim() ?? "";
68323
+ if (!trimmed) {
68324
+ continue;
68325
+ }
68326
+ if (isRunnerIdlePromptLine(trimmed) || isRunnerPromptMetadataLine(trimmed)) {
68327
+ continue;
68328
+ }
68329
+ return false;
68330
+ }
68331
+ return true;
68332
+ }
68333
+ function isRunnerIdlePromptLine(trimmed) {
68334
+ return trimmed.startsWith("› ") || trimmed === "›" || trimmed.startsWith("> ");
68335
+ }
68336
+ function isRunnerPromptMetadataLine(trimmed) {
68337
+ return /^gpt-[\w.-]+ .*·/.test(trimmed) || trimmed.startsWith("model:") || trimmed.startsWith("directory:") || trimmed.startsWith("ctx:") || trimmed.startsWith("tokens:") || trimmed.startsWith("? for shortcuts");
68338
+ }
68104
68339
  function shouldDropCodexChromeLine(line) {
68105
68340
  const trimmed = line.trim();
68106
68341
  if (!trimmed) {
@@ -68317,6 +68552,11 @@ function deriveInteractionText(initialSnapshot, currentSnapshot) {
68317
68552
  const current = cleanInteractionSnapshot(currentSnapshot);
68318
68553
  return extractScrolledAppend(previous, current) || diffText(previous, current);
68319
68554
  }
68555
+ function deriveInteractionDiffText(initialSnapshot, currentSnapshot) {
68556
+ const previous = cleanInteractionSnapshot(initialSnapshot);
68557
+ const current = cleanInteractionSnapshot(currentSnapshot);
68558
+ return diffText(previous, current);
68559
+ }
68320
68560
  function appendInteractionText(currentBody, nextDelta) {
68321
68561
  const trimmedCurrent = currentBody.trim();
68322
68562
  const trimmedDelta = nextDelta.trim();
@@ -68864,7 +69104,7 @@ async function captureTmuxSessionIdentity(params) {
68864
69104
  });
68865
69105
  continue;
68866
69106
  }
68867
- const sessionId = extractSessionId(deriveSessionIdentityText(statusSubmission.submittedSnapshot, snapshot), params.pattern);
69107
+ const sessionId = extractSessionIdFromCandidates(deriveSessionIdentityTexts(statusSubmission.submittedSnapshot, snapshot), params.pattern);
68868
69108
  if (sessionId) {
68869
69109
  await waitForTmuxPaneSettle({
68870
69110
  tmux: params.tmux,
@@ -68879,10 +69119,23 @@ async function captureTmuxSessionIdentity(params) {
68879
69119
  }
68880
69120
  return null;
68881
69121
  }
68882
- function deriveSessionIdentityText(submittedSnapshot, snapshot) {
69122
+ function deriveSessionIdentityTexts(submittedSnapshot, snapshot) {
68883
69123
  const rawSubmitted = normalizePaneText(submittedSnapshot);
68884
69124
  const rawSnapshot = normalizePaneText(snapshot);
68885
- return extractScrolledAppend(rawSubmitted, rawSnapshot) || deriveInteractionText(submittedSnapshot, snapshot);
69125
+ return [
69126
+ extractScrolledAppend(rawSubmitted, rawSnapshot),
69127
+ deriveInteractionText(submittedSnapshot, snapshot),
69128
+ deriveInteractionDiffText(submittedSnapshot, snapshot)
69129
+ ].filter((candidate, index, candidates) => candidate && candidates.indexOf(candidate) === index);
69130
+ }
69131
+ function extractSessionIdFromCandidates(candidates, pattern) {
69132
+ for (const candidate of candidates) {
69133
+ const sessionId = extractSessionId(candidate, pattern);
69134
+ if (sessionId) {
69135
+ return sessionId;
69136
+ }
69137
+ }
69138
+ return null;
68886
69139
  }
68887
69140
  async function dismissTmuxTrustPromptIfPresent(params) {
68888
69141
  const deadline = Date.now() + Math.max(TRUST_PROMPT_MAX_WAIT_MS, params.startupDelayMs);
@@ -68956,7 +69209,7 @@ async function waitForTmuxSessionBootstrap(params) {
68956
69209
  };
68957
69210
  }
68958
69211
  }
68959
- if (readyRegex && !readyRegex.test(snapshot)) {
69212
+ if (readyRegex && !snapshotHasActiveReadyPattern(snapshot, readyRegex)) {
68960
69213
  await sleep(SESSION_BOOTSTRAP_POLL_INTERVAL_MS);
68961
69214
  continue;
68962
69215
  }
@@ -69190,6 +69443,29 @@ function buildBootstrapSessionLostError(sessionName, error) {
69190
69443
  function arePaneStatesEqual(left, right) {
69191
69444
  return left.cursorX === right.cursorX && left.cursorY === right.cursorY && left.historySize === right.historySize;
69192
69445
  }
69446
+ function snapshotHasActiveReadyPattern(snapshot, readyRegex) {
69447
+ const lines = splitNormalizedLines(snapshot);
69448
+ let readyLineIndex = -1;
69449
+ for (let index = 0;index < lines.length; index += 1) {
69450
+ if (readyRegex.test(lines[index] ?? "")) {
69451
+ readyLineIndex = index;
69452
+ }
69453
+ }
69454
+ if (readyLineIndex < 0) {
69455
+ return readyRegex.test(snapshot);
69456
+ }
69457
+ for (const rawLine of lines.slice(readyLineIndex + 1)) {
69458
+ const line = rawLine.trim();
69459
+ if (!line || isPromptMetadataLine(line)) {
69460
+ continue;
69461
+ }
69462
+ return false;
69463
+ }
69464
+ return true;
69465
+ }
69466
+ function isPromptMetadataLine(line) {
69467
+ return /^gpt-[\w.-]+\b/i.test(line) || /^model:\s*/i.test(line) || /^session:\s*/i.test(line);
69468
+ }
69193
69469
  function looksLikeClaudeTrustPrompt(snapshot) {
69194
69470
  return snapshot.includes("Quick safety check:") && snapshot.includes("Yes, I trust this folder") || snapshot.includes("Enter to confirm · Esc to cancel");
69195
69471
  }
@@ -69396,6 +69672,9 @@ function isFreshStartRetryablePromptDeliveryError(error) {
69396
69672
  function isRetryableFreshStartFault(error) {
69397
69673
  return isRecoverableStartupSessionLoss(error) || isTransientTmuxTargetError(error) || isFreshStartRetryablePromptDeliveryError(error);
69398
69674
  }
69675
+ function canRestartWithStoredSessionId(resolved) {
69676
+ return resolved.runner.sessionId.resume.mode === "command" || resolved.runner.sessionId.create.mode === "explicit";
69677
+ }
69399
69678
 
69400
69679
  class RunnerService {
69401
69680
  loadedConfig;
@@ -69618,10 +69897,7 @@ class RunnerService {
69618
69897
  });
69619
69898
  try {
69620
69899
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69621
- await this.sessionState.touchSessionEntry(resolved, {
69622
- sessionId: existing?.sessionId,
69623
- runnerCommand: resolved.runner.command
69624
- });
69900
+ await this.syncSessionIdentity(resolved);
69625
69901
  } catch (error) {
69626
69902
  throw await this.mapSessionError(error, resolved.sessionName, "during startup");
69627
69903
  }
@@ -69766,7 +70042,7 @@ class RunnerService {
69766
70042
  async reopenRunContext(target, timingContext) {
69767
70043
  const resolved = this.resolveTarget(target);
69768
70044
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
69769
- if (!existing?.sessionId || resolved.runner.sessionId.resume.mode !== "command") {
70045
+ if (!existing?.sessionId || !canRestartWithStoredSessionId(resolved)) {
69770
70046
  throw new Error(`Runner session "${resolved.sessionName}" cannot reopen the same conversation context.`);
69771
70047
  }
69772
70048
  return this.ensureRunnerReady(target, { allowFreshRetryBeforePrompt: false, timingContext });
@@ -69925,7 +70201,6 @@ class RunnerService {
69925
70201
  });
69926
70202
  try {
69927
70203
  await this.tmux.sendKey(resolved.sessionName, "Escape");
69928
- await sleep(150);
69929
70204
  } catch {}
69930
70205
  }
69931
70206
  return {
@@ -70048,7 +70323,7 @@ async function monitorTmuxRun(params) {
70048
70323
  let previousRenderedRunningSnapshot = "";
70049
70324
  let lastPaneChangeAt = params.startedAt;
70050
70325
  let sawActivity = false;
70051
- let sawPaneChange = false;
70326
+ let sawPaneChange = !params.prompt;
70052
70327
  let sawPromptSubmission = Boolean(params.prompt);
70053
70328
  let detachedNotified = params.detachedAlready;
70054
70329
  let firstMeaningfulDeltaLogged = false;
@@ -70258,6 +70533,26 @@ class SessionService {
70258
70533
  });
70259
70534
  }
70260
70535
  }
70536
+ async clearLostPersistedActiveRuns() {
70537
+ const entries = await this.sessionState.listEntries();
70538
+ for (const entry of entries) {
70539
+ if (!entry.runtime || entry.runtime.state === "idle") {
70540
+ continue;
70541
+ }
70542
+ if (this.activeRuns.has(entry.sessionKey)) {
70543
+ continue;
70544
+ }
70545
+ const resolved = this.resolveTarget({
70546
+ agentId: entry.agentId,
70547
+ sessionKey: entry.sessionKey
70548
+ });
70549
+ if (!await this.tmux.hasSession(resolved.sessionName)) {
70550
+ await this.sessionState.setSessionRuntime(resolved, {
70551
+ state: "idle"
70552
+ });
70553
+ }
70554
+ }
70555
+ }
70261
70556
  async executePrompt(target, prompt, observer, options = {}) {
70262
70557
  if (this.stopping) {
70263
70558
  throw new Error("Runtime is stopping and cannot accept a new prompt.");
@@ -70392,6 +70687,34 @@ class SessionService {
70392
70687
  detached: true
70393
70688
  };
70394
70689
  }
70690
+ async interruptActiveRun(target) {
70691
+ const run = this.activeRuns.get(target.sessionKey) ?? await this.reconcilePersistedActiveRun(target);
70692
+ if (!run) {
70693
+ return {
70694
+ interrupted: false
70695
+ };
70696
+ }
70697
+ const error = new Error("Run interrupted by /stop.");
70698
+ const update = this.createRunUpdate({
70699
+ resolved: run.resolved,
70700
+ status: "error",
70701
+ snapshot: error.message,
70702
+ fullSnapshot: run.latestUpdate.fullSnapshot,
70703
+ initialSnapshot: run.latestUpdate.initialSnapshot,
70704
+ note: "Run interrupted."
70705
+ });
70706
+ await this.sessionState.setSessionRuntime(run.resolved, {
70707
+ state: "idle"
70708
+ });
70709
+ await this.notifyRunObservers(run, update);
70710
+ if (!run.initialResult.settled) {
70711
+ run.initialResult.reject(error);
70712
+ }
70713
+ this.activeRuns.delete(run.resolved.sessionKey);
70714
+ return {
70715
+ interrupted: true
70716
+ };
70717
+ }
70395
70718
  hasActiveRun(target) {
70396
70719
  return this.activeRuns.has(target.sessionKey);
70397
70720
  }
@@ -70691,7 +71014,7 @@ class SessionService {
70691
71014
  }
70692
71015
  }
70693
71016
  async hasStoredResumableSessionId(resolved) {
70694
- if (resolved.runner.sessionId.resume.mode !== "command") {
71017
+ if (resolved.runner.sessionId.resume.mode !== "command" && resolved.runner.sessionId.create.mode !== "explicit") {
70695
71018
  return false;
70696
71019
  }
70697
71020
  const entry = await this.sessionState.getEntry(resolved.sessionKey);
@@ -71067,18 +71390,11 @@ var REPLY_RULES = `When replying to the user:
71067
71390
  - put the user-facing message inside the --message body of that command
71068
71391
  {{progress_rules_block}}- {{final_rule_line}}`;
71069
71392
  var PROGRESS_PHRASE = "progress update or ";
71070
- var EMPTY_PROGRESS_PHRASE = "";
71071
71393
  var PROGRESS_FLAG_SUFFIX = "|progress";
71072
- var EMPTY_PROGRESS_FLAG_SUFFIX = "";
71073
71394
  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
71395
+ - send at most {{max_progress_messages}} short, meaningful progress updates; skip trivial internal steps
71080
71396
  `;
71081
- var FINAL_RULE_REQUIRED = "send exactly 1 final user-facing response";
71397
+ 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
71398
  var FINAL_RULE_OPTIONAL = "final response is optional";
71083
71399
  var EMPTY_REPLY_COMMAND = "";
71084
71400
  var EMPTY_REPLY_RULES = "";
@@ -71183,10 +71499,9 @@ function renderMessagePromptParts(params) {
71183
71499
  replyStyleHint: EMPTY_REPLY_STYLE_HINT
71184
71500
  };
71185
71501
  }
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;
71502
+ const progressPhrase = PROGRESS_PHRASE;
71503
+ const progressFlagSuffix = PROGRESS_FLAG_SUFFIX;
71504
+ const progressRulesBlock = PROGRESS_RULES_BLOCK;
71190
71505
  const finalRuleLine = params.config.requireFinalResponse ? FINAL_RULE_REQUIRED : FINAL_RULE_OPTIONAL;
71191
71506
  return {
71192
71507
  deliveryIntro: renderTemplate(DELIVERY_INTRO, {
@@ -71216,7 +71531,7 @@ function renderConfigurationGuidance() {
71216
71531
  return [
71217
71532
  `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.`,
71218
71533
  `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 })}.`
71534
+ `For durable queue requests, inspect ${renderCliCommand("queues --help", { inline: true })} and use the queues CLI.`
71220
71535
  ].join(`
71221
71536
  `);
71222
71537
  }
@@ -71722,8 +72037,9 @@ class SurfaceRuntime {
71722
72037
  }
71723
72038
  const identity = this.buildLoopChannelIdentity(loop);
71724
72039
  const notifications = this.resolveSurfaceNotifications(identity);
72040
+ const mode = loop.loopStart ?? notifications.loopStart;
71725
72041
  const text = loop.kind === "calendar" ? renderLoopStartNotification({
71726
- mode: notifications.loopStart,
72042
+ mode,
71727
72043
  agentId: target.agentId,
71728
72044
  loopId: loop.id,
71729
72045
  promptSummary: loop.promptSummary,
@@ -71736,7 +72052,7 @@ class SurfaceRuntime {
71736
72052
  maxRuns: loop.maxRuns,
71737
72053
  kind: "calendar"
71738
72054
  }) : renderLoopStartNotification({
71739
- mode: notifications.loopStart,
72055
+ mode,
71740
72056
  agentId: target.agentId,
71741
72057
  loopId: loop.id,
71742
72058
  promptSummary: loop.promptSummary,
@@ -72150,7 +72466,12 @@ class AgentService {
72150
72466
  return this.runnerSessions.captureTranscript(target);
72151
72467
  }
72152
72468
  async interruptSession(target) {
72153
- return this.runnerSessions.interruptSession(target);
72469
+ const runner = await this.runnerSessions.interruptSession(target);
72470
+ const activeRun = await this.activeRuns.interruptActiveRun(target);
72471
+ return {
72472
+ ...runner,
72473
+ interrupted: runner.interrupted || activeRun.interrupted
72474
+ };
72154
72475
  }
72155
72476
  async nudgeSession(target) {
72156
72477
  return this.runnerSessions.nudgeSession(target);
@@ -72167,13 +72488,16 @@ class AgentService {
72167
72488
  async getSessionDiagnostics(target) {
72168
72489
  const resolved = this.resolveTarget(target);
72169
72490
  const entry = await this.sessionState.getEntry(target.sessionKey);
72170
- const sessionId = entry?.sessionId?.trim() || undefined;
72491
+ const storedSessionId = entry?.sessionId?.trim() || undefined;
72171
72492
  return {
72172
- sessionId,
72173
- resumeCommand: buildResumeCommandPreview(resolved, sessionId)
72493
+ sessionName: resolved.sessionName,
72494
+ sessionId: storedSessionId,
72495
+ storedSessionId,
72496
+ resumeCommand: buildResumeCommandPreview(resolved, storedSessionId)
72174
72497
  };
72175
72498
  }
72176
72499
  async listActiveSessionRuntimes() {
72500
+ await this.activeRuns.clearLostPersistedActiveRuns();
72177
72501
  return this.sessionState.listActiveSessionRuntimes();
72178
72502
  }
72179
72503
  async setConversationFollowUpMode(target, mode) {
@@ -72593,30 +72917,16 @@ function renderStartupSteeringUnavailableMessage() {
72593
72917
  ].join(`
72594
72918
  `);
72595
72919
  }
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";
72610
- }
72611
72920
  function renderWhoAmIMessage(params) {
72921
+ const storedSessionId = params.sessionDiagnostics.storedSessionId ?? params.sessionDiagnostics.sessionId;
72612
72922
  const lines = [
72613
72923
  "Who am I",
72614
72924
  "",
72615
72925
  `platform: \`${params.identity.platform}\``,
72616
72926
  `conversationKind: \`${params.identity.conversationKind}\``,
72617
72927
  `agentId: \`${params.route.agentId}\``,
72618
- `sessionKey: \`${params.sessionTarget.sessionKey}\``,
72619
- `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
72928
+ `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
72929
+ `storedSessionId: \`${storedSessionId ?? "(not captured yet)"}\``
72620
72930
  ];
72621
72931
  if (params.identity.senderId) {
72622
72932
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -72633,19 +72943,20 @@ function renderWhoAmIMessage(params) {
72633
72943
  if (params.identity.topicId) {
72634
72944
  lines.push(`topicId: \`${params.identity.topicId}\``);
72635
72945
  }
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}\``);
72946
+ 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
72947
  return lines.join(`
72638
72948
  `);
72639
72949
  }
72640
72950
  function renderRouteStatusMessage(params) {
72951
+ const storedSessionId = params.sessionDiagnostics.storedSessionId ?? params.sessionDiagnostics.sessionId;
72641
72952
  const lines = [
72642
72953
  "Status",
72643
72954
  "",
72644
72955
  `platform: \`${params.identity.platform}\``,
72645
72956
  `conversationKind: \`${params.identity.conversationKind}\``,
72646
72957
  `agentId: \`${params.route.agentId}\``,
72647
- `sessionKey: \`${params.sessionTarget.sessionKey}\``,
72648
- `storedSessionId: \`${params.sessionDiagnostics.sessionId ?? "(not captured yet)"}\``
72958
+ `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
72959
+ `storedSessionId: \`${storedSessionId ?? "(not captured yet)"}\``
72649
72960
  ];
72650
72961
  if (params.identity.senderId) {
72651
72962
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -72662,7 +72973,7 @@ function renderRouteStatusMessage(params) {
72662
72973
  if (params.identity.topicId) {
72663
72974
  lines.push(`topicId: \`${params.identity.topicId}\``);
72664
72975
  }
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}\``);
72976
+ 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
72977
  if (params.runtimeState.startedAt) {
72667
72978
  lines.push(`run.startedAt: \`${new Date(params.runtimeState.startedAt).toISOString()}\``);
72668
72979
  }
@@ -73823,6 +74134,7 @@ ${renderLoopUsage()}`);
73823
74134
  canonicalPromptText: resolvedLoopPrompt.text,
73824
74135
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
73825
74136
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
74137
+ loopStart: slashCommand.params.loopStart,
73826
74138
  surfaceBinding: buildLoopSurfaceBinding(params.identity),
73827
74139
  cadence: slashCommand.params.cadence,
73828
74140
  dayOfWeek: slashCommand.params.dayOfWeek,
@@ -73862,6 +74174,7 @@ ${renderLoopUsage()}`);
73862
74174
  canonicalPromptText: resolvedLoopPrompt.text,
73863
74175
  promptSummary: summarizeLoopPrompt(resolvedLoopPrompt.text, resolvedLoopPrompt.maintenancePrompt),
73864
74176
  promptSource: resolvedLoopPrompt.maintenancePrompt ? "LOOP.md" : "custom",
74177
+ loopStart: slashCommand.params.loopStart,
73865
74178
  surfaceBinding: buildLoopSurfaceBinding(params.identity),
73866
74179
  intervalMs: effectiveIntervalMs,
73867
74180
  maxRuns: maxRunsPerLoop,
@@ -77955,6 +78268,7 @@ var TELEGRAM_FULL_COMMANDS = [
77955
78268
  { command: "detach", description: "Stop live updates for this thread" },
77956
78269
  { command: "watch", description: "Watch the active run on an interval" },
77957
78270
  { command: "stop", description: "Interrupt current run" },
78271
+ { command: "new", description: "Start new session" },
77958
78272
  { command: "nudge", description: "Send one extra Enter to the session" },
77959
78273
  { command: "followup", description: "Show or change follow-up mode" },
77960
78274
  { command: "mention", description: "Require explicit mention for later turns" },
@@ -81143,6 +81457,7 @@ function renderLoopsCreateHelp() {
81143
81457
  " - `--new-thread` creates a Slack thread anchor before persisting the loop",
81144
81458
  " - `--timezone <iana>` freezes a one-off wall-clock timezone on the loop record",
81145
81459
  " - `--confirm` persists the first wall-clock loop after reviewing the confirmation output",
81460
+ ` - advanced: \`${LOOP_START_FLAG} <none|brief|full>\` overrides the default scheduled loop-start notification behavior for that recurring loop`,
81146
81461
  "",
81147
81462
  "Examples:",
81148
81463
  ` ${renderCliCommand("loops create --channel slack --target group:C1234567890 --thread-id 1712345678.123456 --sender slack:U1234567890 every day at 07:00 check CI")}`,
@@ -81749,6 +82064,7 @@ function buildRecurringLoopPromptMetadata(request) {
81749
82064
  canonicalPromptText: request.resolvedPrompt.text,
81750
82065
  promptSummary: summarizeLoopPrompt(request.resolvedPrompt.text, request.resolvedPrompt.maintenancePrompt),
81751
82066
  promptSource: request.resolvedPrompt.maintenancePrompt ? "LOOP.md" : "custom",
82067
+ loopStart: request.parsed.mode === "times" ? undefined : request.parsed.loopStart,
81752
82068
  maintenancePrompt: request.resolvedPrompt.maintenancePrompt,
81753
82069
  createdBy: request.creator.providerId,
81754
82070
  sender: request.creator,
@@ -82254,6 +82570,11 @@ async function runMessageCli(args, dependencies = defaultMessageCliDependencies)
82254
82570
  var QUEUE_SENDER_FLAG = "--sender";
82255
82571
  var QUEUE_SENDER_NAME_FLAG = "--sender-name";
82256
82572
  var QUEUE_SENDER_HANDLE_FLAG = "--sender-handle";
82573
+ var defaultQueueCliDependencies = {
82574
+ print: (text) => console.log(text),
82575
+ warn: (text) => console.warn(text),
82576
+ sendQueueCreatedNotification: sendQueueCreatedNotificationToSurface
82577
+ };
82257
82578
  function getEditableConfigPath9() {
82258
82579
  return process.env.CLISBOT_CONFIG_PATH;
82259
82580
  }
@@ -82419,6 +82740,57 @@ function createQueueItemForContext(params) {
82419
82740
  surfaceBinding: buildQueueSurfaceBinding(params.context)
82420
82741
  });
82421
82742
  }
82743
+ function renderQueueCreatedNotification(params) {
82744
+ const queueLine = params.positionAhead > 0 ? `Queued: ${params.positionAhead} ahead.` : "Queued.";
82745
+ return `${queueLine}
82746
+
82747
+ Prompt:
82748
+ ${params.promptText.trim()}`;
82749
+ }
82750
+ async function getQueuePositionAhead(state, sessionKey, itemId) {
82751
+ const queues = await state.sessionState.listQueuedItems({
82752
+ sessionKey,
82753
+ statuses: ["pending", "running"]
82754
+ });
82755
+ const index = queues.findIndex((item) => item.id === itemId);
82756
+ return index >= 0 ? index : 0;
82757
+ }
82758
+ function buildQueueCreatedMessageCommand(params) {
82759
+ return {
82760
+ action: "send",
82761
+ channel: params.context.channel,
82762
+ account: params.context.botId,
82763
+ target: params.context.target,
82764
+ message: params.text,
82765
+ threadId: params.context.threadId,
82766
+ remove: false,
82767
+ pollOptions: [],
82768
+ forceDocument: false,
82769
+ silent: false,
82770
+ progress: false,
82771
+ final: false,
82772
+ json: false,
82773
+ inputFormat: "plain",
82774
+ renderMode: "none"
82775
+ };
82776
+ }
82777
+ async function sendQueueCreatedNotificationToSurface(params) {
82778
+ if (!params.item.surfaceBinding) {
82779
+ return;
82780
+ }
82781
+ const loadedConfig = await loadConfig(params.state.configPath, {
82782
+ materializeChannels: [params.context.channel]
82783
+ });
82784
+ const plugin = listChannelPlugins().find((entry) => entry.id === params.context.channel);
82785
+ if (!plugin) {
82786
+ throw new Error(`Unsupported queue notification channel: ${params.context.channel}`);
82787
+ }
82788
+ await plugin.runMessageCommand(loadedConfig, buildQueueCreatedMessageCommand({
82789
+ context: params.context,
82790
+ text: params.text
82791
+ }));
82792
+ await params.state.sessionState.recordConversationReply(params.resolved);
82793
+ }
82422
82794
  function renderQueueInventory(params) {
82423
82795
  const lines = [
82424
82796
  `Queue ${params.commandLabel}`,
@@ -82433,20 +82805,20 @@ function renderQueueInventory(params) {
82433
82805
  return lines.join(`
82434
82806
  `);
82435
82807
  }
82436
- async function listQueues(state, addressing, commandLabel) {
82808
+ async function listQueues(state, addressing, commandLabel, deps) {
82437
82809
  const context = addressing.channel || addressing.target ? resolveScopedContext(state, addressing) : undefined;
82438
82810
  const sessionKey = context?.sessionTarget.sessionKey;
82439
82811
  const queues = await state.sessionState.listQueuedItems({
82440
82812
  sessionKey,
82441
82813
  statuses: commandLabel === "list" ? ["pending"] : ["pending", "running"]
82442
82814
  });
82443
- console.log(renderQueueInventory({
82815
+ deps.print(renderQueueInventory({
82444
82816
  commandLabel,
82445
82817
  sessionStorePath: state.sessionStorePath,
82446
82818
  queues
82447
82819
  }));
82448
82820
  }
82449
- async function createQueue(state, args) {
82821
+ async function createQueue(state, args, deps) {
82450
82822
  const addressing = parseQueueCliAddressing(args);
82451
82823
  const promptText = stripQueueArgs(args.slice(1)).join(" ").trim();
82452
82824
  if (!promptText) {
@@ -82467,18 +82839,30 @@ async function createQueue(state, args) {
82467
82839
  sender
82468
82840
  });
82469
82841
  await state.sessionState.setQueuedItem(resolved, item);
82470
- console.log(`Queued prompt \`${item.id}\` for \`${context.sessionTarget.sessionKey}\`.`);
82842
+ const positionAhead = await getQueuePositionAhead(state, context.sessionTarget.sessionKey, item.id);
82843
+ const text = renderQueueCreatedNotification({ positionAhead, promptText });
82844
+ await deps.sendQueueCreatedNotification({
82845
+ state,
82846
+ context,
82847
+ resolved,
82848
+ item,
82849
+ positionAhead,
82850
+ text
82851
+ }).catch((error) => {
82852
+ deps.warn(`Queued prompt ${item.id}, but surface acknowledgement failed: ${String(error)}`);
82853
+ });
82854
+ deps.print(`Queued prompt \`${item.id}\` for \`${context.sessionTarget.sessionKey}\`.`);
82471
82855
  }
82472
- async function clearQueues(state, addressing) {
82856
+ async function clearQueues(state, addressing, deps) {
82473
82857
  if (addressing.all) {
82474
82858
  const cleared2 = await state.sessionState.clearAllPendingQueuedItems();
82475
- console.log(`Cleared ${cleared2.length} pending queued prompt${cleared2.length === 1 ? "" : "s"} across the whole app.`);
82859
+ deps.print(`Cleared ${cleared2.length} pending queued prompt${cleared2.length === 1 ? "" : "s"} across the whole app.`);
82476
82860
  return;
82477
82861
  }
82478
82862
  const context = resolveScopedContext(state, addressing);
82479
82863
  const sessionKey = context.sessionTarget.sessionKey;
82480
82864
  const cleared = await state.sessionState.clearPendingQueuedItemsForSessionKey(sessionKey);
82481
- console.log(`Cleared ${cleared.length} pending queued prompt${cleared.length === 1 ? "" : "s"} for \`${sessionKey}\`.`);
82865
+ deps.print(`Cleared ${cleared.length} pending queued prompt${cleared.length === 1 ? "" : "s"} for \`${sessionKey}\`.`);
82482
82866
  }
82483
82867
  function renderQueuesHelp() {
82484
82868
  return [
@@ -82499,23 +82883,24 @@ function renderQueuesHelp() {
82499
82883
  ].join(`
82500
82884
  `);
82501
82885
  }
82502
- async function runQueuesCli(args) {
82886
+ async function runQueuesCli(args, dependencies = {}) {
82887
+ const deps = { ...defaultQueueCliDependencies, ...dependencies };
82503
82888
  if (args[0] === "--help" || args[0] === "help" || args.length === 0) {
82504
- console.log(renderQueuesHelp());
82889
+ deps.print(renderQueuesHelp());
82505
82890
  return;
82506
82891
  }
82507
82892
  const command = args[0];
82508
82893
  const state = await loadQueueControlState();
82509
82894
  if (command === "list" || command === "status") {
82510
- await listQueues(state, parseQueueCliAddressing(args.slice(1)), command);
82895
+ await listQueues(state, parseQueueCliAddressing(args.slice(1)), command, deps);
82511
82896
  return;
82512
82897
  }
82513
82898
  if (command === "create") {
82514
- await createQueue(state, args);
82899
+ await createQueue(state, args, deps);
82515
82900
  return;
82516
82901
  }
82517
82902
  if (command === "clear") {
82518
- await clearQueues(state, parseQueueCliAddressing(args.slice(1)));
82903
+ await clearQueues(state, parseQueueCliAddressing(args.slice(1)), deps);
82519
82904
  return;
82520
82905
  }
82521
82906
  throw new Error(`Unknown queues subcommand: ${command}`);
@@ -83604,10 +83989,9 @@ function renderWatchFrame(params) {
83604
83989
  "",
83605
83990
  `session: ${params.sessionName}`,
83606
83991
  params.agentId ? `agent: ${params.agentId}` : null,
83607
- params.sessionKey ? `sessionKey: ${params.sessionKey}` : null,
83992
+ `sessionId: ${params.sessionId?.trim() || "none"}`,
83608
83993
  `lines: ${params.lines}`,
83609
- `intervalMs: ${params.intervalMs}`,
83610
- `status: ${params.status}`,
83994
+ `state: ${params.state}`,
83611
83995
  "",
83612
83996
  params.snapshot.trimEnd() || "(empty pane)"
83613
83997
  ].filter((line) => Boolean(line)).join(`
@@ -83645,18 +84029,15 @@ function renderRunnerListSession(session) {
83645
84029
  return [
83646
84030
  prefix,
83647
84031
  " sessionId: none",
83648
- " state: unmanaged",
83649
- " live: yes"
84032
+ " state: unmanaged"
83650
84033
  ].join(`
83651
84034
  `);
83652
84035
  }
83653
84036
  return [
83654
84037
  prefix,
83655
84038
  ` agent: ${session.entry.agentId}`,
83656
- ` sessionKey: ${session.entry.sessionKey}`,
83657
84039
  ` sessionId: ${session.entry.sessionId?.trim() || "none"}`,
83658
84040
  ` state: ${session.entry.runtime?.state ?? "no-runtime"}`,
83659
- ` live: ${session.live ? "yes" : "no"}`,
83660
84041
  ` lastAdmittedPromptAt: ${formatTimestamp(session.entry.lastAdmittedPromptAt)}`
83661
84042
  ].join(`
83662
84043
  `);
@@ -83767,11 +84148,10 @@ async function runWatchCli(args) {
83767
84148
  }
83768
84149
  const frame = renderWatchFrame({
83769
84150
  sessionName: selection.sessionName,
83770
- sessionKey: selection.metadata?.entry.sessionKey,
84151
+ sessionId: selection.metadata?.entry.sessionId,
83771
84152
  agentId: selection.metadata?.entry.agentId,
83772
84153
  lines: options.lines,
83773
- intervalMs: options.intervalMs,
83774
- status,
84154
+ state: status,
83775
84155
  snapshot
83776
84156
  });
83777
84157
  if (process.stdout.isTTY) {
@@ -85727,11 +86107,25 @@ async function stop(hard = false) {
85727
86107
  console.log("clisbot stopped");
85728
86108
  printCommandOutcomeFooter("success");
85729
86109
  }
85730
- async function restart() {
85731
- await stopDetachedRuntime({
85732
- configPath: getOperatorConfigPath(),
85733
- hard: false
85734
- });
86110
+ async function restart(dependencies = {
86111
+ stopDetachedRuntime,
86112
+ getRuntimeStatus,
86113
+ warn: (message) => console.error(message)
86114
+ }) {
86115
+ const configPath = getOperatorConfigPath();
86116
+ try {
86117
+ await dependencies.stopDetachedRuntime({
86118
+ configPath,
86119
+ hard: false
86120
+ });
86121
+ } catch (error) {
86122
+ const status = await dependencies.getRuntimeStatus({ configPath });
86123
+ if (status.running) {
86124
+ throw error;
86125
+ }
86126
+ const message = error instanceof Error ? error.message : String(error);
86127
+ dependencies.warn(`warning: clisbot stop reported an error, but status now shows the service is stopped; continuing with start. Stop error: ${message}`);
86128
+ }
85735
86129
  }
85736
86130
  async function status() {
85737
86131
  await printStatusSummary();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.46-beta.0",
3
+ "version": "0.1.46-beta.1",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",