@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 +1 -1
- package/unify/threads/store.js +38 -0
- package/unify/web-bridge.js +80 -0
package/package.json
CHANGED
package/unify/threads/store.js
CHANGED
|
@@ -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); }
|
package/unify/web-bridge.js
CHANGED
|
@@ -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
|
|