@yeaft/webchat-agent 0.1.496 → 0.1.497

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.496",
3
+ "version": "0.1.497",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -518,6 +518,44 @@ export class ThreadStore {
518
518
  this.#idleArchiveDays = normaliseIdleDays(days);
519
519
  }
520
520
 
521
+ /**
522
+ * task-317: auto-archive pass. Scans every non-archived thread (except
523
+ * `main`, which is never auto-archived) and archives those whose
524
+ * `lastMessageAt` (fallback: `lastActivityAt`, fallback: `createdAt`)
525
+ * is older than `now - idleArchiveDays * 86400000 ms`.
526
+ *
527
+ * Returns the list of newly-archived thread ids so callers can decide
528
+ * whether to broadcast a UI update (no archived → no broadcast).
529
+ *
530
+ * Constraints:
531
+ * - `idleArchiveDays === 0` disables the feature entirely (returns []).
532
+ * - The main thread is NEVER archived regardless of its activity.
533
+ * - Already-archived threads are skipped (idempotent).
534
+ * - Threads with no recorded activity fall back to `createdAt`; a
535
+ * thread created 100 days ago with zero messages IS archived when
536
+ * idleArchiveDays ≤ 100 — silent threads aren't a special case.
537
+ *
538
+ * @param {number} [now] — override the clock for tests
539
+ * @returns {{ archived: string[] }}
540
+ */
541
+ runArchivePass(now = Date.now()) {
542
+ if (this.#idleArchiveDays <= 0) return { archived: [] };
543
+ const cutoff = now - this.#idleArchiveDays * 86400000;
544
+ const archived = [];
545
+ for (const t of this.#threads.values()) {
546
+ if (t.id === MAIN_THREAD_ID) continue;
547
+ if (t.archived || t.status === 'archived') continue;
548
+ const ref = t.lastMessageAt ?? t.lastActivityAt ?? t.createdAt ?? now;
549
+ if (ref > cutoff) continue;
550
+ t.status = 'archived';
551
+ t.archived = true;
552
+ t.updatedAt = now;
553
+ this.#markDirty(t.id);
554
+ archived.push(t.id);
555
+ }
556
+ return { archived };
557
+ }
558
+
521
559
  get(id) { return this.#threads.get(id) || null; }
522
560
  list() { return [...this.#threads.values()]; }
523
561
  has(id) { return this.#threads.has(id); }
@@ -101,10 +101,78 @@ export function installUnifyRuntimeBridge(s) {
101
101
  if (typeof s.threadStore?.setIdleArchiveDays === 'function') {
102
102
  s.threadStore.setIdleArchiveDays(v);
103
103
  }
104
+ // task-317: re-sweep right after the cap changes so a user who
105
+ // lowers the threshold sees stale threads disappear immediately
106
+ // rather than having to wait for the hourly tick.
107
+ runAutoArchiveSweep(s);
104
108
  },
105
109
  };
106
110
  }
107
111
 
112
+ /**
113
+ * task-317: idle thread auto-archive.
114
+ *
115
+ * A single sweep = ask the ThreadStore to archive every non-main,
116
+ * non-archived thread whose last activity predates the configured idle
117
+ * window. When any thread is archived we push a fresh `thread_list_updated`
118
+ * so the sidebar reflects reality within the same tick.
119
+ *
120
+ * Safe on stores with `idleArchiveDays === 0` (returns no-op) and on
121
+ * sessions missing a threadStore handle (defensive; should never happen
122
+ * once `installUnifyRuntimeBridge` has run).
123
+ *
124
+ * @param {import('./session.js').Session|null} s
125
+ * @returns {string[]} archived thread ids (empty when nothing changed)
126
+ */
127
+ export function runAutoArchiveSweep(s) {
128
+ try {
129
+ const store = s?.threadStore ?? (typeof getThreadStore === 'function' ? getThreadStore() : null);
130
+ if (!store || typeof store.runArchivePass !== 'function') return [];
131
+ const { archived } = store.runArchivePass();
132
+ if (archived && archived.length > 0) {
133
+ sendThreadListUpdate();
134
+ }
135
+ return archived || [];
136
+ } catch (err) {
137
+ console.warn('[Unify] runAutoArchiveSweep failed:', err?.message || err);
138
+ return [];
139
+ }
140
+ }
141
+
142
+ /**
143
+ * task-317: schedule the hourly auto-archive tick bound to the given
144
+ * session. Returns the `Timeout` handle so tests can assert / clear it.
145
+ * Re-calling replaces any prior timer (idempotent per-session).
146
+ *
147
+ * The timer is `unref()`'d so a pending tick never keeps the Node loop
148
+ * alive during shutdown; an explicit `clearAutoArchiveSchedule()` is
149
+ * provided for tests.
150
+ */
151
+ let autoArchiveTimer = null;
152
+ const AUTO_ARCHIVE_TICK_MS = 60 * 60 * 1000; // 1h
153
+
154
+ export function scheduleAutoArchive(s, { intervalMs = AUTO_ARCHIVE_TICK_MS } = {}) {
155
+ if (autoArchiveTimer) {
156
+ clearInterval(autoArchiveTimer);
157
+ autoArchiveTimer = null;
158
+ }
159
+ if (!s) return null;
160
+ autoArchiveTimer = setInterval(() => {
161
+ runAutoArchiveSweep(s);
162
+ }, intervalMs);
163
+ if (autoArchiveTimer && typeof autoArchiveTimer.unref === 'function') {
164
+ autoArchiveTimer.unref();
165
+ }
166
+ return autoArchiveTimer;
167
+ }
168
+
169
+ export function clearAutoArchiveSchedule() {
170
+ if (autoArchiveTimer) {
171
+ clearInterval(autoArchiveTimer);
172
+ autoArchiveTimer = null;
173
+ }
174
+ }
175
+
108
176
  /**
109
177
  * task-301 Part 2: push the full thread list snapshot to the web client.
110
178
  * Called after any ThreadStore-mutating tool completes and at turn_end so
@@ -436,6 +504,10 @@ export async function handleUnifyChat(msg) {
436
504
  // code — the config file was updated on disk but the running
437
505
  // session continued with the old caps until next restart.
438
506
  installUnifyRuntimeBridge(session);
507
+ // task-317: run one idle-archive sweep at bootstrap, then schedule
508
+ // the hourly tick bound to this session.
509
+ runAutoArchiveSweep(session);
510
+ scheduleAutoArchive(session);
439
511
 
440
512
  // Create a stable conversationId for the Unify session
441
513
  unifyConversationId = `unify-${Date.now()}`;
@@ -785,6 +857,9 @@ export async function handleUnifyLoadHistory(msg) {
785
857
  });
786
858
  // task-318 rev-1 fix: wire live setters; see handleUnifyChat.
787
859
  installUnifyRuntimeBridge(session);
860
+ // task-317: sweep + schedule auto-archive on history-load path too.
861
+ runAutoArchiveSweep(session);
862
+ scheduleAutoArchive(session);
788
863
 
789
864
  unifyConversationId = `unify-${Date.now()}`;
790
865
 
@@ -861,6 +936,11 @@ export async function resetUnifySession() {
861
936
  });
862
937
  // task-318 rev-1 fix: wire live setters; see handleUnifyChat.
863
938
  installUnifyRuntimeBridge(session);
939
+ // task-317: sweep + re-schedule auto-archive on reset too (the old
940
+ // interval was bound to the previous session; reschedule against the
941
+ // fresh one so timer references don't dangle).
942
+ runAutoArchiveSweep(session);
943
+ scheduleAutoArchive(session);
864
944
 
865
945
  unifyConversationId = `unify-${Date.now()}`;
866
946