clisbot 0.1.45-beta.7 → 0.1.45-beta.8

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +215 -154
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -139,7 +139,7 @@ Need the step-by-step setup docs instead of the shortest path?
139
139
 
140
140
  - Telegram: [Telegram Bot Setup](docs/user-guide/telegram-setup.md)
141
141
  - Slack: [Slack App Setup](docs/user-guide/slack-setup.md)
142
- - Release history: [CHANGELOG.md](CHANGELOG.md), [release notes](docs/releases/README.md), [update guide](docs/update/README.md), [operator updates](docs/updates/README.md), and [migration index](docs/migrations/index.md)
142
+ - Release history: [CHANGELOG.md](CHANGELOG.md), [release notes](docs/releases/README.md), [update guide](docs/updates/update-guide.md), [release guides](docs/updates/README.md), and [migration index](docs/migrations/index.md)
143
143
  - Slack app manifest template: [app-manifest.json](templates/slack/default/app-manifest.json)
144
144
  - Slack app manifest guide: [app-manifest-guide.md](templates/slack/default/app-manifest-guide.md)
145
145
 
package/dist/main.js CHANGED
@@ -54803,7 +54803,7 @@ function renderCliHelp() {
54803
54803
  " status Show runtime process, config, log, tmux socket status, and recent runner sessions.",
54804
54804
  " version Show the installed clisbot version.",
54805
54805
  " logs Print the most recent clisbot log lines.",
54806
- " update Print the AI-readable package update guide and release/migration doc links.",
54806
+ " update Print the update guide and release/migration doc links.",
54807
54807
  ` See ${renderCliCommand("update --help", { inline: true })} before asking an agent to update clisbot.`,
54808
54808
  " timezone Manage the app-wide wall-clock timezone used by schedules and loops.",
54809
54809
  ` See ${renderCliCommand("timezone --help", { inline: true })} for override guidance.`,
@@ -66541,19 +66541,6 @@ class AgentSessionState {
66541
66541
  agentId: target.agentId
66542
66542
  };
66543
66543
  }
66544
- async listActiveSessionRuntimes() {
66545
- const entries = await this.sessionStore.list();
66546
- return entries.filter(hasActiveRuntime).map((entry) => ({
66547
- state: entry.runtime.state,
66548
- startedAt: entry.runtime.startedAt,
66549
- detachedAt: entry.runtime.detachedAt,
66550
- finalReplyAt: entry.runtime.finalReplyAt,
66551
- lastMessageToolReplyAt: entry.runtime.lastMessageToolReplyAt,
66552
- messageToolFinalReplyAt: entry.runtime.messageToolFinalReplyAt,
66553
- sessionKey: entry.sessionKey,
66554
- agentId: entry.agentId
66555
- }));
66556
- }
66557
66544
  async listIntervalLoops(params) {
66558
66545
  const entries = await this.sessionStore.list();
66559
66546
  return entries.flatMap((entry) => getStoredLoops(entry).filter((loop) => !params?.sessionKey || entry.sessionKey === params.sessionKey).map((loop) => ({
@@ -66811,6 +66798,10 @@ class AgentSessionState {
66811
66798
  const entry = await this.sessionStore.get(sessionKey);
66812
66799
  return getStoredQueues(entry).some((item) => item.status === "pending" || item.status === "running");
66813
66800
  }
66801
+ async hasQueuedItem(sessionKey, queueId) {
66802
+ const entry = await this.sessionStore.get(sessionKey);
66803
+ return getStoredQueues(entry).some((item) => item.id === queueId);
66804
+ }
66814
66805
  async resetStaleRunningQueuedItems(activeSessionKeys) {
66815
66806
  const entries = await this.sessionStore.list();
66816
66807
  let reset = 0;
@@ -66891,9 +66882,6 @@ function getStoredLoops(entry) {
66891
66882
  function getStoredQueues(entry) {
66892
66883
  return entry?.queues ?? [];
66893
66884
  }
66894
- function hasActiveRuntime(entry) {
66895
- return entry.runtime?.state === "running" || entry.runtime?.state === "detached";
66896
- }
66897
66885
 
66898
66886
  // src/agents/session-key.ts
66899
66887
  var DEFAULT_MAIN_KEY = "main";
@@ -67146,39 +67134,15 @@ class AgentJobQueue {
67146
67134
  if (!state) {
67147
67135
  return 0;
67148
67136
  }
67149
- const keptEntries = state.entries.filter((entry) => entry.status === "running");
67150
- const removedEntries = state.entries.filter((entry) => entry.status === "pending");
67151
- state.entries = keptEntries;
67152
- for (const entry of removedEntries) {
67153
- Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67154
- return;
67155
- });
67156
- entry.reject(new ClearedQueuedTaskError);
67157
- }
67158
- if (state.entries.length === 0 && !state.running) {
67159
- this.states.delete(key);
67160
- }
67161
- return removedEntries.length;
67137
+ return this.clearPendingEntriesForState(key, state, () => true);
67162
67138
  }
67163
- clearPendingByIds(ids) {
67164
- const idSet = new Set(ids);
67165
- let cleared = 0;
67166
- for (const [key, state] of this.states.entries()) {
67167
- const keptEntries = state.entries.filter((entry) => entry.status === "running" || !idSet.has(entry.id));
67168
- const removedEntries = state.entries.filter((entry) => entry.status === "pending" && idSet.has(entry.id));
67169
- state.entries = keptEntries;
67170
- cleared += removedEntries.length;
67171
- for (const entry of removedEntries) {
67172
- Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67173
- return;
67174
- });
67175
- entry.reject(new ClearedQueuedTaskError);
67176
- }
67177
- if (state.entries.length === 0 && !state.running) {
67178
- this.states.delete(key);
67179
- }
67139
+ clearPendingByIdsForKey(key, ids) {
67140
+ const state = this.states.get(key);
67141
+ if (!state) {
67142
+ return 0;
67180
67143
  }
67181
- return cleared;
67144
+ const idSet = new Set(ids);
67145
+ return this.clearPendingEntriesForState(key, state, (entry) => idSet.has(entry.id));
67182
67146
  }
67183
67147
  getOrCreateState(key) {
67184
67148
  const existing = this.states.get(key);
@@ -67203,6 +67167,21 @@ class AgentJobQueue {
67203
67167
  return left.sequence - right.sequence;
67204
67168
  });
67205
67169
  }
67170
+ clearPendingEntriesForState(key, state, shouldClear) {
67171
+ const keptEntries = state.entries.filter((entry) => entry.status === "running" || !shouldClear(entry));
67172
+ const removedEntries = state.entries.filter((entry) => entry.status === "pending" && shouldClear(entry));
67173
+ state.entries = keptEntries;
67174
+ for (const entry of removedEntries) {
67175
+ Promise.resolve(entry.lifecycle?.onClear?.()).catch(() => {
67176
+ return;
67177
+ });
67178
+ entry.reject(new ClearedQueuedTaskError);
67179
+ }
67180
+ if (state.entries.length === 0 && !state.running) {
67181
+ this.states.delete(key);
67182
+ }
67183
+ return removedEntries.length;
67184
+ }
67206
67185
  async drain(key, state) {
67207
67186
  if (state.running) {
67208
67187
  return;
@@ -67542,87 +67521,86 @@ class ManagedLoopController {
67542
67521
  }
67543
67522
  async reconcilePersistedIntervalLoops() {
67544
67523
  const persistedLoops = await this.deps.sessionState.listIntervalLoops();
67545
- const persistedIds = new Set;
67524
+ const persistedKeys = new Set;
67546
67525
  for (const persisted of persistedLoops) {
67547
- persistedIds.add(persisted.id);
67526
+ const managedKey = this.buildManagedLoopKey(persisted.sessionKey, persisted.id);
67527
+ persistedKeys.add(managedKey);
67548
67528
  if (persisted.attemptedRuns >= persisted.maxRuns) {
67549
- this.dropManagedIntervalLoop(persisted.id);
67529
+ this.dropManagedIntervalLoop(managedKey);
67550
67530
  continue;
67551
67531
  }
67552
- if (this.intervalLoops.has(persisted.id)) {
67532
+ if (this.intervalLoops.has(managedKey)) {
67553
67533
  continue;
67554
67534
  }
67555
- this.intervalLoops.set(persisted.id, {
67535
+ this.intervalLoops.set(managedKey, {
67556
67536
  target: {
67557
67537
  agentId: persisted.agentId,
67558
67538
  sessionKey: persisted.sessionKey
67559
67539
  },
67560
67540
  loop: persisted
67561
67541
  });
67562
- this.scheduleIntervalLoopTimer(persisted.id, Math.max(0, persisted.nextRunAt - Date.now()));
67542
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, persisted.nextRunAt - Date.now()));
67563
67543
  }
67564
- for (const loopId of this.intervalLoops.keys()) {
67565
- if (!persistedIds.has(loopId)) {
67566
- this.dropManagedIntervalLoop(loopId);
67544
+ for (const managedKey of this.intervalLoops.keys()) {
67545
+ if (!persistedKeys.has(managedKey)) {
67546
+ this.dropManagedIntervalLoop(managedKey);
67567
67547
  }
67568
67548
  }
67569
67549
  }
67570
67550
  async createIntervalLoop(params) {
67571
67551
  this.assertActiveLoopCapacity();
67572
67552
  const loop = createStoredIntervalLoop(params);
67553
+ const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
67573
67554
  await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
67574
- this.intervalLoops.set(loop.id, {
67555
+ this.intervalLoops.set(managedKey, {
67575
67556
  target: params.target,
67576
67557
  loop
67577
67558
  });
67578
- await this.runIntervalLoopIteration(loop.id, {
67559
+ await this.runIntervalLoopIteration(managedKey, {
67579
67560
  notifyStart: false
67580
67561
  });
67581
- return this.getIntervalLoop(loop.id);
67562
+ return this.getIntervalLoop(params.target.sessionKey, loop.id);
67582
67563
  }
67583
67564
  async createCalendarLoop(params) {
67584
67565
  this.assertActiveLoopCapacity();
67585
67566
  const loop = createStoredCalendarLoop(params);
67567
+ const managedKey = this.buildManagedLoopKey(params.target.sessionKey, loop.id);
67586
67568
  await this.deps.sessionState.setIntervalLoop(this.deps.resolveTarget(params.target), loop);
67587
- this.intervalLoops.set(loop.id, {
67569
+ this.intervalLoops.set(managedKey, {
67588
67570
  target: params.target,
67589
67571
  loop
67590
67572
  });
67591
- this.scheduleIntervalLoopTimer(loop.id, Math.max(0, loop.nextRunAt - Date.now()));
67592
- return this.getIntervalLoop(loop.id);
67573
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, loop.nextRunAt - Date.now()));
67574
+ return this.getIntervalLoop(params.target.sessionKey, loop.id);
67593
67575
  }
67594
- async cancelIntervalLoop(loopId) {
67595
- const managed = this.intervalLoops.get(loopId);
67576
+ async cancelIntervalLoop(target, loopId) {
67577
+ const managedKey = this.buildManagedLoopKey(target.sessionKey, loopId);
67578
+ const managed = this.intervalLoops.get(managedKey);
67596
67579
  if (!managed) {
67597
67580
  return false;
67598
67581
  }
67599
- if (managed.timer) {
67600
- clearTimeout(managed.timer);
67601
- this.loopTimers.delete(managed.timer);
67602
- }
67603
- this.intervalLoops.delete(loopId);
67604
- await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), loopId);
67582
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67605
67583
  return true;
67606
67584
  }
67607
67585
  async cancelIntervalLoopsForSession(target) {
67608
- const matching = [...this.intervalLoops.values()].filter((managed) => managed.target.sessionKey === target.sessionKey).map((managed) => managed.loop.id);
67609
- for (const loopId of matching) {
67610
- await this.cancelIntervalLoop(loopId);
67586
+ const matching = [...this.intervalLoops.entries()].filter(([, managed]) => managed.target.sessionKey === target.sessionKey);
67587
+ for (const [managedKey, managed] of matching) {
67588
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67611
67589
  }
67612
67590
  return matching.length;
67613
67591
  }
67614
67592
  async cancelAllIntervalLoops() {
67615
- const ids = [...this.intervalLoops.keys()];
67616
- for (const loopId of ids) {
67617
- await this.cancelIntervalLoop(loopId);
67593
+ const matching = [...this.intervalLoops.entries()];
67594
+ for (const [managedKey, managed] of matching) {
67595
+ await this.cancelManagedIntervalLoop(managedKey, managed);
67618
67596
  }
67619
- return ids.length;
67597
+ return matching.length;
67620
67598
  }
67621
67599
  listIntervalLoops(params) {
67622
67600
  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);
67623
67601
  }
67624
- getIntervalLoop(loopId) {
67625
- const managed = this.intervalLoops.get(loopId);
67602
+ getIntervalLoop(sessionKey, loopId) {
67603
+ const managed = this.intervalLoops.get(this.buildManagedLoopKey(sessionKey, loopId));
67626
67604
  return managed ? this.toLoopStatus(managed) : null;
67627
67605
  }
67628
67606
  getActiveIntervalLoopCount() {
@@ -67646,8 +67624,8 @@ class ManagedLoopController {
67646
67624
  const entry = await this.deps.sessionState.getEntry(managed.target.sessionKey);
67647
67625
  return (entry?.loops ?? entry?.intervalLoops ?? []).some((loop) => loop.id === managed.loop.id);
67648
67626
  }
67649
- dropManagedIntervalLoop(loopId) {
67650
- const managed = this.intervalLoops.get(loopId);
67627
+ dropManagedIntervalLoop(managedKey) {
67628
+ const managed = this.intervalLoops.get(managedKey);
67651
67629
  if (!managed) {
67652
67630
  return;
67653
67631
  }
@@ -67655,30 +67633,34 @@ class ManagedLoopController {
67655
67633
  clearTimeout(managed.timer);
67656
67634
  this.loopTimers.delete(managed.timer);
67657
67635
  }
67658
- this.intervalLoops.delete(loopId);
67636
+ this.intervalLoops.delete(managedKey);
67659
67637
  }
67660
- async runIntervalLoopIteration(loopId, options = {}) {
67661
- const managed = this.intervalLoops.get(loopId);
67638
+ async cancelManagedIntervalLoop(managedKey, managed) {
67639
+ this.dropManagedIntervalLoop(managedKey);
67640
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
67641
+ }
67642
+ async runIntervalLoopIteration(managedKey, options = {}) {
67643
+ const managed = this.intervalLoops.get(managedKey);
67662
67644
  if (!managed) {
67663
67645
  return;
67664
67646
  }
67665
67647
  if (!await this.isManagedLoopPersisted(managed)) {
67666
- this.dropManagedIntervalLoop(loopId);
67648
+ this.dropManagedIntervalLoop(managedKey);
67667
67649
  return;
67668
67650
  }
67669
67651
  const attemptedRuns = managed.loop.attemptedRuns + 1;
67670
67652
  const now = Date.now();
67671
67653
  const nextLoopState = this.buildNextLoopState(managed.loop, attemptedRuns, now);
67672
67654
  if (await this.deps.isSessionBusy(managed.target)) {
67673
- await this.skipLoopIteration(loopId, managed, nextLoopState, attemptedRuns, now);
67655
+ await this.skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now);
67674
67656
  return;
67675
67657
  }
67676
67658
  nextLoopState.executedRuns += 1;
67677
67659
  if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
67678
- this.dropManagedIntervalLoop(loopId);
67660
+ this.dropManagedIntervalLoop(managedKey);
67679
67661
  return;
67680
67662
  }
67681
- await this.executeLoopIteration(loopId, managed.target, nextLoopState, attemptedRuns, now, options.notifyStart !== false);
67663
+ await this.executeLoopIteration(managedKey, managed.target, nextLoopState, attemptedRuns, now, options.notifyStart !== false);
67682
67664
  }
67683
67665
  buildNextLoopState(loop, attemptedRuns, now) {
67684
67666
  return {
@@ -67688,33 +67670,35 @@ class ManagedLoopController {
67688
67670
  nextRunAt: this.computeNextManagedLoopRunAtMs(loop, now)
67689
67671
  };
67690
67672
  }
67691
- async skipLoopIteration(loopId, managed, nextLoopState, attemptedRuns, now) {
67673
+ async skipLoopIteration(managedKey, managed, nextLoopState, attemptedRuns, now) {
67692
67674
  nextLoopState.skippedRuns += 1;
67693
67675
  if (!await this.updateManagedIntervalLoop(managed, nextLoopState)) {
67694
- this.dropManagedIntervalLoop(loopId);
67676
+ this.dropManagedIntervalLoop(managedKey);
67695
67677
  return;
67696
67678
  }
67697
67679
  if (attemptedRuns >= managed.loop.maxRuns) {
67698
- await this.cancelIntervalLoop(loopId);
67680
+ this.dropManagedIntervalLoop(managedKey);
67681
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(managed.target), managed.loop.id);
67699
67682
  return;
67700
67683
  }
67701
- this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
67684
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
67702
67685
  }
67703
- async executeLoopIteration(loopId, target, nextLoopState, attemptedRuns, now, notifyStart) {
67704
- await this.notifyAndEnqueueLoop(loopId, target, nextLoopState, attemptedRuns, notifyStart);
67686
+ async executeLoopIteration(managedKey, target, nextLoopState, attemptedRuns, now, notifyStart) {
67687
+ await this.notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart);
67705
67688
  if (attemptedRuns >= nextLoopState.maxRuns) {
67706
- await this.cancelIntervalLoop(loopId);
67689
+ this.dropManagedIntervalLoop(managedKey);
67690
+ await this.deps.sessionState.removeIntervalLoop(this.deps.resolveTarget(target), nextLoopState.id);
67707
67691
  return;
67708
67692
  }
67709
- this.scheduleIntervalLoopTimer(loopId, Math.max(0, nextLoopState.nextRunAt - now));
67693
+ this.scheduleIntervalLoopTimer(managedKey, Math.max(0, nextLoopState.nextRunAt - now));
67710
67694
  }
67711
- async notifyAndEnqueueLoop(loopId, target, nextLoopState, attemptedRuns, notifyStart) {
67695
+ async notifyAndEnqueueLoop(managedKey, target, nextLoopState, attemptedRuns, notifyStart) {
67712
67696
  if (notifyStart) {
67713
67697
  await this.deps.surfaceRuntime.notifyManagedLoopStart(target, nextLoopState);
67714
67698
  }
67715
67699
  const promptText = await this.deps.surfaceRuntime.buildManagedLoopPrompt(target.agentId, nextLoopState);
67716
67700
  const { result } = this.deps.enqueuePrompt(target, promptText, {
67717
- observerId: `loop:${loopId}:${attemptedRuns}`,
67701
+ observerId: `loop:${managedKey}:${attemptedRuns}`,
67718
67702
  onUpdate: async () => {
67719
67703
  return;
67720
67704
  }
@@ -67725,8 +67709,8 @@ class ManagedLoopController {
67725
67709
  }
67726
67710
  });
67727
67711
  }
67728
- scheduleIntervalLoopTimer(loopId, delayMs) {
67729
- const managed = this.intervalLoops.get(loopId);
67712
+ scheduleIntervalLoopTimer(managedKey, delayMs) {
67713
+ const managed = this.intervalLoops.get(managedKey);
67730
67714
  if (!managed) {
67731
67715
  return;
67732
67716
  }
@@ -67736,12 +67720,12 @@ class ManagedLoopController {
67736
67720
  }
67737
67721
  const timer = setTimeout(() => {
67738
67722
  this.loopTimers.delete(timer);
67739
- const current = this.intervalLoops.get(loopId);
67723
+ const current = this.intervalLoops.get(managedKey);
67740
67724
  if (!current) {
67741
67725
  return;
67742
67726
  }
67743
67727
  current.timer = undefined;
67744
- this.runIntervalLoopIteration(loopId, { notifyStart: true }).catch((error) => {
67728
+ this.runIntervalLoopIteration(managedKey, { notifyStart: true }).catch((error) => {
67745
67729
  if (!this.deps.shouldSuppressShutdownError(error)) {
67746
67730
  console.error("loop execution failed", error);
67747
67731
  }
@@ -67758,6 +67742,9 @@ class ManagedLoopController {
67758
67742
  managed.loop = nextLoopState;
67759
67743
  return true;
67760
67744
  }
67745
+ buildManagedLoopKey(sessionKey, loopId) {
67746
+ return `${sessionKey}::${loopId}`;
67747
+ }
67761
67748
  computeNextManagedLoopRunAtMs(loop, nowMs) {
67762
67749
  if (loop.kind === "calendar") {
67763
67750
  return computeNextCalendarLoopRunAtMs({
@@ -67798,7 +67785,7 @@ class ManagedQueueController {
67798
67785
  }
67799
67786
  async clearQueuedPrompts(target) {
67800
67787
  const clearedStored = await this.deps.sessionState.clearPendingQueuedItemsForSessionKey(target.sessionKey);
67801
- this.deps.queue.clearPendingByIds(clearedStored.map((item) => item.id));
67788
+ this.deps.queue.clearPendingByIdsForKey(target.sessionKey, clearedStored.map((item) => item.id));
67802
67789
  return clearedStored.length;
67803
67790
  }
67804
67791
  enqueuePrompt(target, prompt, callbacks) {
@@ -67836,8 +67823,8 @@ class ManagedQueueController {
67836
67823
  return;
67837
67824
  }
67838
67825
  this.setManagedQueueItem(target, item, true);
67839
- return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(item)).catch((error) => {
67840
- this.clearPendingById(item);
67826
+ return this.persistQueueItem(this.deps.resolveTarget(target), item).then(() => this.markPersisted(target, item)).catch((error) => {
67827
+ this.clearPendingById(target, item);
67841
67828
  throw error;
67842
67829
  });
67843
67830
  }
@@ -67848,16 +67835,32 @@ class ManagedQueueController {
67848
67835
  await reconciledBeforeStart;
67849
67836
  return !await this.deps.hasBlockingActiveRun(target);
67850
67837
  }
67838
+ async canStartPersistedQueueItem(target, item) {
67839
+ if (await this.deps.hasBlockingActiveRun(target)) {
67840
+ return false;
67841
+ }
67842
+ if (!await this.deps.sessionState.hasQueuedItem(target.sessionKey, item.id)) {
67843
+ this.clearPendingById(target, item);
67844
+ return false;
67845
+ }
67846
+ return true;
67847
+ }
67851
67848
  async reconcilePersistedQueueItems() {
67852
67849
  const persistedItems = await this.deps.sessionState.listQueuedItems({
67853
67850
  statuses: ["pending", "running"]
67854
67851
  });
67855
- const persistedIds = new Set(persistedItems.map((item) => item.id));
67856
- const removedIds = [...this.queuedItems.entries()].filter(([id, managed]) => !managed.persisting && !persistedIds.has(id)).map(([id]) => id);
67857
- if (removedIds.length > 0) {
67858
- this.deps.queue.clearPendingByIds(removedIds);
67859
- for (const id of removedIds) {
67860
- this.queuedItems.delete(id);
67852
+ const persistedKeys = new Set(persistedItems.map((item) => this.buildManagedQueueKey(item.sessionKey, item.id)));
67853
+ const removedManagedItems = [...this.queuedItems.entries()].filter(([key, managed]) => !managed.persisting && !persistedKeys.has(key));
67854
+ if (removedManagedItems.length > 0) {
67855
+ const removedBySession = new Map;
67856
+ for (const [key, managed] of removedManagedItems) {
67857
+ const ids = removedBySession.get(managed.target.sessionKey) ?? [];
67858
+ ids.push(managed.item.id);
67859
+ removedBySession.set(managed.target.sessionKey, ids);
67860
+ this.queuedItems.delete(key);
67861
+ }
67862
+ for (const [sessionKey, itemIds] of removedBySession.entries()) {
67863
+ this.deps.queue.clearPendingByIdsForKey(sessionKey, itemIds);
67861
67864
  }
67862
67865
  }
67863
67866
  for (const persisted of persistedItems) {
@@ -67865,14 +67868,14 @@ class ManagedQueueController {
67865
67868
  await this.clearStaleRunningQueueItem(persisted);
67866
67869
  continue;
67867
67870
  }
67868
- if (this.queuedItems.has(persisted.id)) {
67871
+ if (this.queuedItems.has(this.buildManagedQueueKey(persisted.sessionKey, persisted.id))) {
67869
67872
  continue;
67870
67873
  }
67871
67874
  this.enqueuePersistedQueueItem(persisted);
67872
67875
  }
67873
67876
  }
67874
67877
  async clearStaleRunningQueueItem(item) {
67875
- if (this.queuedItems.has(item.id)) {
67878
+ if (this.queuedItems.has(this.buildManagedQueueKey(item.sessionKey, item.id))) {
67876
67879
  return;
67877
67880
  }
67878
67881
  const target = {
@@ -67896,21 +67899,22 @@ class ManagedQueueController {
67896
67899
  });
67897
67900
  }
67898
67901
  setManagedQueueItem(target, item, persisting) {
67899
- this.queuedItems.set(item.id, {
67902
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67900
67903
  target,
67901
67904
  item,
67902
67905
  persisting
67903
67906
  });
67904
67907
  }
67905
- markPersisted(item) {
67906
- const managed = this.queuedItems.get(item.id);
67908
+ markPersisted(target, item) {
67909
+ const key = this.buildManagedQueueKey(target.sessionKey, item.id);
67910
+ const managed = this.queuedItems.get(key);
67907
67911
  if (managed) {
67908
- this.queuedItems.set(item.id, { ...managed, persisting: false });
67912
+ this.queuedItems.set(key, { ...managed, persisting: false });
67909
67913
  }
67910
67914
  }
67911
- clearPendingById(item) {
67912
- this.deps.queue.clearPendingByIds([item.id]);
67913
- this.queuedItems.delete(item.id);
67915
+ clearPendingById(target, item) {
67916
+ this.deps.queue.clearPendingByIdsForKey(target.sessionKey, [item.id]);
67917
+ this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
67914
67918
  }
67915
67919
  async markQueueItemRunning(target, item) {
67916
67920
  if (!item) {
@@ -67923,7 +67927,7 @@ class ManagedQueueController {
67923
67927
  startedAt: now,
67924
67928
  updatedAt: now
67925
67929
  };
67926
- this.queuedItems.set(item.id, {
67930
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67927
67931
  target,
67928
67932
  item: next
67929
67933
  });
@@ -67934,7 +67938,7 @@ class ManagedQueueController {
67934
67938
  if (!item) {
67935
67939
  return;
67936
67940
  }
67937
- this.queuedItems.delete(item.id);
67941
+ this.queuedItems.delete(this.buildManagedQueueKey(target.sessionKey, item.id));
67938
67942
  await this.deps.sessionState.removeQueuedItem(this.deps.resolveTarget(target), item.id);
67939
67943
  }
67940
67944
  enqueuePersistedQueueItem(item) {
@@ -67942,7 +67946,7 @@ class ManagedQueueController {
67942
67946
  agentId: item.agentId,
67943
67947
  sessionKey: item.sessionKey
67944
67948
  };
67945
- this.queuedItems.set(item.id, {
67949
+ this.queuedItems.set(this.buildManagedQueueKey(target.sessionKey, item.id), {
67946
67950
  target,
67947
67951
  item
67948
67952
  });
@@ -67974,7 +67978,7 @@ class ManagedQueueController {
67974
67978
  id: item.id,
67975
67979
  createdAt: item.createdAt,
67976
67980
  text: item.promptSummary,
67977
- canStart: async () => !await this.deps.hasBlockingActiveRun(target),
67981
+ canStart: async () => this.canStartPersistedQueueItem(target, item),
67978
67982
  onComplete: async (value) => {
67979
67983
  if (!value.messageToolFinalAlreadySent && !await this.hasMessageToolFinalForQueueItem(target, item)) {
67980
67984
  await this.deps.surfaceRuntime.notifyManagedQueueSettlement(target, item, value);
@@ -67988,6 +67992,9 @@ class ManagedQueueController {
67988
67992
  onClear: () => this.removeManagedQueueItem(target, item)
67989
67993
  });
67990
67994
  queued.result.catch((error) => {
67995
+ if (error instanceof ClearedQueuedTaskError) {
67996
+ return;
67997
+ }
67991
67998
  if (this.deps.shouldSuppressShutdownError(error)) {
67992
67999
  return;
67993
68000
  }
@@ -68004,7 +68011,7 @@ class ManagedQueueController {
68004
68011
  return false;
68005
68012
  }
68006
68013
  async hasMessageToolFinalForQueueItem(target, item) {
68007
- const startedAt = this.queuedItems.get(item.id)?.item.startedAt ?? item.startedAt;
68014
+ const startedAt = this.queuedItems.get(this.buildManagedQueueKey(target.sessionKey, item.id))?.item.startedAt ?? item.startedAt;
68008
68015
  if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) {
68009
68016
  return false;
68010
68017
  }
@@ -68025,6 +68032,9 @@ class ManagedQueueController {
68025
68032
  messageToolFinalAlreadySent: true
68026
68033
  };
68027
68034
  }
68035
+ buildManagedQueueKey(sessionKey, itemId) {
68036
+ return `${sessionKey}::${itemId}`;
68037
+ }
68028
68038
  }
68029
68039
 
68030
68040
  // src/agents/runner-service.ts
@@ -69731,7 +69741,11 @@ class RunnerService {
69731
69741
  args: args.map((value) => applyTemplate(value, values))
69732
69742
  };
69733
69743
  }
69734
- async syncSessionIdentity(resolved) {
69744
+ async syncStoredSessionId(target) {
69745
+ const resolved = this.resolveTarget(target);
69746
+ return this.syncStoredSessionIdForResolvedTarget(resolved);
69747
+ }
69748
+ async syncStoredSessionIdForResolvedTarget(resolved) {
69735
69749
  const existing = await this.sessionState.getEntry(resolved.sessionKey);
69736
69750
  if (existing?.sessionId) {
69737
69751
  this.sessionIdentityCaptureRetryAt.delete(resolved.sessionKey);
@@ -69793,12 +69807,41 @@ class RunnerService {
69793
69807
  remainingFreshRetries: remainingFreshRetries - 1
69794
69808
  });
69795
69809
  }
69796
- async retryAfterStartupFault(target, resolved, error, remainingFreshRetries) {
69810
+ async retryAfterStartupFault(target, resolved, error, remainingFreshRetries, allowFreshResumeFallback) {
69811
+ if (allowFreshResumeFallback) {
69812
+ const resumedFresh = await this.retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries);
69813
+ if (resumedFresh) {
69814
+ return resumedFresh;
69815
+ }
69816
+ }
69797
69817
  if (!isRetryableFreshStartFault(error)) {
69798
69818
  return null;
69799
69819
  }
69800
69820
  return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
69801
69821
  }
69822
+ async retryFreshStartAfterStoredResumeFailure(target, resolved, error, remainingFreshRetries) {
69823
+ if (!isRecoverableStartupSessionLoss(error)) {
69824
+ return null;
69825
+ }
69826
+ if (resolved.runner.sessionId.resume.mode !== "command" || resolved.runner.sessionId.create.mode !== "runner") {
69827
+ return null;
69828
+ }
69829
+ const existing = await this.sessionState.getEntry(resolved.sessionKey);
69830
+ if (!existing?.sessionId) {
69831
+ return null;
69832
+ }
69833
+ const exitRecord = await readRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69834
+ if (!exitRecord || exitRecord.exitCode === 0) {
69835
+ return null;
69836
+ }
69837
+ console.log(`clisbot clearing stored sessionId after failed runner resume startup ${resolved.sessionName}`);
69838
+ await this.sessionState.clearSessionIdEntry(resolved, {
69839
+ runnerCommand: resolved.runner.command
69840
+ });
69841
+ return this.ensureSessionReady(target, {
69842
+ remainingFreshRetries
69843
+ });
69844
+ }
69802
69845
  async retryAfterStartupTimeout(target, resolved, remainingFreshRetries) {
69803
69846
  return this.retryRunnerRestartPreservingSessionId(target, resolved, remainingFreshRetries);
69804
69847
  }
@@ -69897,7 +69940,7 @@ class RunnerService {
69897
69940
  });
69898
69941
  try {
69899
69942
  await clearRunnerExitRecord(this.loadedConfig.stateDir, resolved.sessionName);
69900
- await this.syncSessionIdentity(resolved);
69943
+ await this.syncStoredSessionIdForResolvedTarget(resolved);
69901
69944
  } catch (error) {
69902
69945
  throw await this.mapSessionError(error, resolved.sessionName, "during startup");
69903
69946
  }
@@ -69967,7 +70010,7 @@ class RunnerService {
69967
70010
  runnerCommand: runnerLaunch.command
69968
70011
  });
69969
70012
  } catch (error) {
69970
- const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries);
70013
+ const retried = await this.retryAfterStartupFault(target, resolved, error, remainingFreshRetries, options.allowFreshRetry !== false);
69971
70014
  if (retried) {
69972
70015
  return retried;
69973
70016
  }
@@ -69989,7 +70032,7 @@ class RunnerService {
69989
70032
  });
69990
70033
  return;
69991
70034
  }
69992
- await this.syncSessionIdentity(resolved);
70035
+ await this.syncStoredSessionIdForResolvedTarget(resolved);
69993
70036
  }
69994
70037
  async dismissTrustPrompt(resolved) {
69995
70038
  if (!resolved.runner.trustWorkspace) {
@@ -70522,16 +70565,21 @@ class SessionService {
70522
70565
  this.resolveTarget = resolveTarget;
70523
70566
  }
70524
70567
  async recoverPersistedRuns() {
70568
+ const activeSessionKeys = new Set;
70525
70569
  const entries = await this.sessionState.listEntries();
70526
70570
  for (const entry of entries) {
70527
70571
  if (!entry.runtime || entry.runtime.state === "idle") {
70528
70572
  continue;
70529
70573
  }
70530
- await this.reconcilePersistedActiveRun({
70574
+ const run = await this.reconcilePersistedActiveRun({
70531
70575
  agentId: entry.agentId,
70532
70576
  sessionKey: entry.sessionKey
70533
70577
  });
70578
+ if (run) {
70579
+ activeSessionKeys.add(run.resolved.sessionKey);
70580
+ }
70534
70581
  }
70582
+ return activeSessionKeys;
70535
70583
  }
70536
70584
  async clearLostPersistedActiveRuns() {
70537
70585
  const entries = await this.sessionState.listEntries();
@@ -70762,6 +70810,14 @@ class SessionService {
70762
70810
  }
70763
70811
  this.activeRuns.clear();
70764
70812
  }
70813
+ listLiveSessionRuntimes() {
70814
+ return [...this.activeRuns.values()].map((run) => ({
70815
+ state: run.latestUpdate.status === "detached" ? "detached" : "running",
70816
+ startedAt: run.startedAt,
70817
+ sessionKey: run.resolved.sessionKey,
70818
+ agentId: run.resolved.agentId
70819
+ }));
70820
+ }
70765
70821
  buildDetachedNote(resolved) {
70766
70822
  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.`;
70767
70823
  }
@@ -72407,9 +72463,8 @@ class AgentService {
72407
72463
  });
72408
72464
  }
72409
72465
  async start() {
72410
- await this.activeRuns.recoverPersistedRuns();
72411
- const activeSessions = new Set((await this.sessionState.listActiveSessionRuntimes()).map((runtime) => runtime.sessionKey));
72412
- await this.sessionState.resetStaleRunningQueuedItems(activeSessions);
72466
+ const activeSessionKeys = await this.activeRuns.recoverPersistedRuns();
72467
+ await this.sessionState.resetStaleRunningQueuedItems(activeSessionKeys);
72413
72468
  await this.managedQueues.reconcilePersistedQueueItems();
72414
72469
  this.queueReconcileTimer = setInterval(() => {
72415
72470
  this.managedQueues.reconcilePersistedQueueItems().catch((error) => {
@@ -72492,14 +72547,13 @@ class AgentService {
72492
72547
  const storedSessionId = entry?.sessionId?.trim() || undefined;
72493
72548
  return {
72494
72549
  sessionName: resolved.sessionName,
72495
- sessionId: storedSessionId,
72496
72550
  storedSessionId,
72497
72551
  resumeCommand: buildResumeCommandPreview(resolved, storedSessionId)
72498
72552
  };
72499
72553
  }
72500
- async listActiveSessionRuntimes() {
72554
+ async listLiveSessionRuntimes() {
72501
72555
  await this.activeRuns.clearLostPersistedActiveRuns();
72502
- return this.sessionState.listActiveSessionRuntimes();
72556
+ return this.activeRuns.listLiveSessionRuntimes();
72503
72557
  }
72504
72558
  async setConversationFollowUpMode(target, mode) {
72505
72559
  return this.sessionState.setConversationFollowUpMode(this.resolveTarget(target), mode);
@@ -72600,8 +72654,8 @@ class AgentService {
72600
72654
  async createCalendarLoop(params) {
72601
72655
  return this.managedLoops.createCalendarLoop(params);
72602
72656
  }
72603
- async cancelIntervalLoop(loopId) {
72604
- return this.managedLoops.cancelIntervalLoop(loopId);
72657
+ async cancelIntervalLoop(target, loopId) {
72658
+ return this.managedLoops.cancelIntervalLoop(target, loopId);
72605
72659
  }
72606
72660
  async cancelIntervalLoopsForSession(target) {
72607
72661
  return this.managedLoops.cancelIntervalLoopsForSession(target);
@@ -72612,8 +72666,8 @@ class AgentService {
72612
72666
  listIntervalLoops(params) {
72613
72667
  return this.managedLoops.listIntervalLoops(params);
72614
72668
  }
72615
- getIntervalLoop(loopId) {
72616
- return this.managedLoops.getIntervalLoop(loopId);
72669
+ getIntervalLoop(target, loopId) {
72670
+ return this.managedLoops.getIntervalLoop(target.sessionKey, loopId);
72617
72671
  }
72618
72672
  getActiveIntervalLoopCount() {
72619
72673
  return this.managedLoops.getActiveIntervalLoopCount();
@@ -72919,7 +72973,6 @@ function renderStartupSteeringUnavailableMessage() {
72919
72973
  `);
72920
72974
  }
72921
72975
  function renderWhoAmIMessage(params) {
72922
- const storedSessionId = params.sessionDiagnostics.storedSessionId ?? params.sessionDiagnostics.sessionId;
72923
72976
  const lines = [
72924
72977
  "Who am I",
72925
72978
  "",
@@ -72927,7 +72980,7 @@ function renderWhoAmIMessage(params) {
72927
72980
  `conversationKind: \`${params.identity.conversationKind}\``,
72928
72981
  `agentId: \`${params.route.agentId}\``,
72929
72982
  `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
72930
- `storedSessionId: \`${storedSessionId ?? "(not captured yet)"}\``
72983
+ `storedSessionId: \`${params.sessionDiagnostics.storedSessionId ?? "(not captured yet)"}\``
72931
72984
  ];
72932
72985
  if (params.identity.senderId) {
72933
72986
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -72949,7 +73002,6 @@ function renderWhoAmIMessage(params) {
72949
73002
  `);
72950
73003
  }
72951
73004
  function renderRouteStatusMessage(params) {
72952
- const storedSessionId = params.sessionDiagnostics.storedSessionId ?? params.sessionDiagnostics.sessionId;
72953
73005
  const lines = [
72954
73006
  "Status",
72955
73007
  "",
@@ -72957,7 +73009,7 @@ function renderRouteStatusMessage(params) {
72957
73009
  `conversationKind: \`${params.identity.conversationKind}\``,
72958
73010
  `agentId: \`${params.route.agentId}\``,
72959
73011
  `sessionName: \`${params.sessionDiagnostics.sessionName ?? "(not available)"}\``,
72960
- `storedSessionId: \`${storedSessionId ?? "(not captured yet)"}\``
73012
+ `storedSessionId: \`${params.sessionDiagnostics.storedSessionId ?? "(not captured yet)"}\``
72961
73013
  ];
72962
73014
  if (params.identity.senderId) {
72963
73015
  lines.push(`senderId: \`${params.identity.senderId}\``);
@@ -74047,7 +74099,7 @@ async function processChannelInteraction(params) {
74047
74099
  await params.agentService.recordConversationReply(params.sessionTarget);
74048
74100
  return interactionResult;
74049
74101
  }
74050
- const cancelled = await params.agentService.cancelIntervalLoop(targetLoopId);
74102
+ const cancelled = await params.agentService.cancelIntervalLoop(params.sessionTarget, targetLoopId);
74051
74103
  await params.postText(cancelled ? `Cancelled loop \`${targetLoopId}\`.` : `No active loop found with id \`${targetLoopId}\`.`);
74052
74104
  await params.agentService.recordConversationReply(params.sessionTarget);
74053
74105
  return interactionResult;
@@ -84282,18 +84334,27 @@ function renderUpdateHelp() {
84282
84334
  " 2. Read docs in priority order and follow them before installing.",
84283
84335
  " 3. npm install -g clisbot@<target> && clisbot restart",
84284
84336
  ` 4. ${renderCliCommand("status")}`,
84285
- " 5. Report version, health, manual action, and useful release-note highlights.",
84337
+ " 5. Report version, health, manual action, and useful release highlights.",
84286
84338
  "",
84287
84339
  "Docs, read in order:",
84288
84340
  ` 1. Migration index: ${GITHUB_RAW_BASE}/docs/migrations/index.md`,
84289
84341
  " If Manual action: required, follow its runbook. If none, continue.",
84290
- ` 2. Release quick info: ${GITHUB_RAW_BASE}/docs/releases/README.md`,
84291
- " Use for release-note map and current release highlights.",
84292
- ` 3. Update guide: ${GITHUB_RAW_BASE}/docs/update/README.md`,
84293
- " Use for release notes, useful new features, things to try, and user-guide links.",
84294
- " 4. Full docs: https://github.com/longbkit/clisbot/tree/main/docs",
84342
+ ` 2. Update guide: ${GITHUB_RAW_BASE}/docs/updates/update-guide.md`,
84343
+ " Use for target choice, install flow, verification, and wrong-publish recovery.",
84344
+ ` 3. Release notes: ${GITHUB_RAW_BASE}/docs/releases/README.md`,
84345
+ " Use for the canonical version map and full version notes.",
84346
+ ` 4. Release guides: ${GITHUB_RAW_BASE}/docs/updates/README.md`,
84347
+ " Use for shorter catch-up notes: what changed, what to try, and what to watch.",
84348
+ " 5. Full docs: https://github.com/longbkit/clisbot/tree/main/docs",
84295
84349
  " Use for deep questions. If needed, fetch or clone docs and inspect relevant files.",
84296
84350
  "",
84351
+ "Recovery:",
84352
+ " - If a version was published by mistake, publish the corrected target or tag first.",
84353
+ " - Then deprecate the wrong version.",
84354
+ " - Start with `npm login` in an attached session.",
84355
+ " - If npm returns a browser approval URL, keep that same session open and continue it after approval.",
84356
+ " - If the write command still returns EOTP, ask the operator for a current OTP and rerun the exact command with --otp=<code>.",
84357
+ "",
84297
84358
  "Rules:",
84298
84359
  " - Use npm dist-tags, not highest semver.",
84299
84360
  " - Stable/latest is default; beta only when the user asks.",
@@ -84847,7 +84908,7 @@ async function getRuntimeOperatorSummary(params) {
84847
84908
  },
84848
84909
  agentSummaries,
84849
84910
  channelSummaries,
84850
- activeRuns: await agentService.listActiveSessionRuntimes(),
84911
+ activeRuns: await agentService.listLiveSessionRuntimes(),
84851
84912
  configuredAgents: agentSummaries.length,
84852
84913
  bootstrapPendingAgents: agentSummaries.filter((item) => item.bootstrapState === "missing" || item.bootstrapState === "not-bootstrapped").length,
84853
84914
  bootstrappedAgents: agentSummaries.filter((item) => item.bootstrapState === "bootstrapped").length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clisbot",
3
- "version": "0.1.45-beta.7",
3
+ "version": "0.1.45-beta.8",
4
4
  "private": false,
5
5
  "description": "Chat surfaces for durable AI coding agents running in tmux",
6
6
  "license": "MIT",