agentboss 0.1.0 → 0.1.2

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.
@@ -27,27 +27,77 @@ const { aggregateDailySummary } = require('./daily-aggregator');
27
27
  /**
28
28
  * Build a list of YYYY-MM-DD strings starting from today going back
29
29
  * `days` days, ordered most-recent first. Today is included so the
30
- * current day's sessions get scored too; sessions that keep growing
31
- * after being scored are re-picked by getUnanalyzedSessions (ended_at
32
- * newer than analyzed_at).
30
+ * Today is EXCLUDED — sessions on the current calendar day are still
31
+ * actively being held (judge / advice analysing them would race against
32
+ * the user typing more messages, churn the cache, and waste LLM calls).
33
+ * Yesterday + N earlier days only. Callers who really want to (re)
34
+ * analyze today must pass `dates: ['YYYY-MM-DD']` explicitly to
35
+ * runAnalysisJob, or use the per-session reanalyze endpoint
36
+ * (`POST /api/analysis/session/:id`) which bypasses this path.
33
37
  *
34
- * @param {number} days
38
+ * Default `days = 7` therefore produces 7 dates (yesterday through 7
39
+ * days ago), not 8.
40
+ *
41
+ * @param {number} days number of past days to include (yesterday-anchored)
35
42
  * @returns {string[]}
36
43
  */
37
44
  function buildDateList(days) {
38
45
  const dates = [];
39
46
  const now = new Date();
40
- for (let i = 0; i <= days; i++) {
47
+ // Start at i=1 → yesterday; end at i=days inclusive → `days` total dates.
48
+ for (let i = 1; i <= days; i++) {
41
49
  const d = new Date(now);
42
50
  d.setDate(d.getDate() - i);
43
- const yyyy = d.getFullYear();
44
- const mm = String(d.getMonth() + 1).padStart(2, '0');
45
- const dd = String(d.getDate()).padStart(2, '0');
46
- dates.push(`${yyyy}-${mm}-${dd}`);
51
+ dates.push(formatDate(d));
47
52
  }
48
53
  return dates; // already most-recent first
49
54
  }
50
55
 
56
+ /**
57
+ * Local-time YYYY-MM-DD for a Date. Must match buildDateList so the
58
+ * "today" string we compute lines up with the date strings we iterate.
59
+ */
60
+ function formatDate(d) {
61
+ const yyyy = d.getFullYear();
62
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
63
+ const dd = String(d.getDate()).padStart(2, '0');
64
+ return `${yyyy}-${mm}-${dd}`;
65
+ }
66
+
67
+ /**
68
+ * Pick the id of the "currently active" session for `date` — defined
69
+ * (per user spec) as the today-bucket session with the largest
70
+ * ended_at. Returns null if `date` isn't today or the bucket is empty.
71
+ * We skip this session in the job so we don't analyze a still-growing
72
+ * conversation; the next job pass will pick it up once it settles.
73
+ *
74
+ * Note: the default invocation of runAnalysisJob no longer includes
75
+ * today at all (see buildDateList). This function only fires when a
76
+ * caller explicitly passes `dates` that includes today — in which case
77
+ * we still shield the actively-typed-in session.
78
+ *
79
+ * @param {object} db
80
+ * @param {string} date YYYY-MM-DD
81
+ * @param {object[]} sessions candidate session rows for that date
82
+ * @returns {string|null} session_id to skip, or null
83
+ */
84
+ function pickCurrentSessionId(db, date, sessions) {
85
+ const today = formatDate(new Date());
86
+ if (date !== today) return null;
87
+ // Compare against ALL today's sessions (not just unanalyzed) so a
88
+ // fully-analyzed but freshly-touched session still counts as "current"
89
+ // and shields a slightly older still-running session from being
90
+ // wrongly considered the active one.
91
+ const allToday = getSessionsByDate(db, today);
92
+ const pool = allToday.length > 0 ? allToday : sessions;
93
+ let best = null;
94
+ for (const s of pool) {
95
+ if (!s.ended_at) continue;
96
+ if (!best || s.ended_at > best.ended_at) best = s;
97
+ }
98
+ return best ? best.id : null;
99
+ }
100
+
51
101
  // ---------------------------------------------------------------------------
52
102
  // Per-session analyze + persist (shared by the job loop and the
53
103
  // per-session reanalyze endpoint)
@@ -119,7 +169,24 @@ async function analyzeAndStoreSession(db, session, opts = {}) {
119
169
 
120
170
  /**
121
171
  * Run analysis job: analyze unanalyzed sessions in reverse chronological order.
122
- * Default: last 7 days. Processes one date at a time, most recent first.
172
+ *
173
+ * # Today policy
174
+ *
175
+ * Default invocation ({days}) skips TODAY entirely — sessions on the
176
+ * current calendar day are likely still being held, and analysing
177
+ * them now means churning the LLM cache for results that will be stale
178
+ * within minutes. The boot path (bin/aboss.js) uses this default, so
179
+ * "open aboss → background scan" never touches today.
180
+ *
181
+ * Two escape hatches keep today analysable when the user really asks:
182
+ * - `dates: ['YYYY-MM-DD']` explicit list → not filtered. Used by
183
+ * manual triggers that pass a specific date set.
184
+ * - `POST /api/analysis/session/:id` → goes straight through
185
+ * `analyzeAndStoreSession`, doesn't use this job loop, so today's
186
+ * "Re-analyze" button in the UI keeps working.
187
+ *
188
+ * Default: last 7 days (yesterday → 7 days ago). Processes one date at
189
+ * a time, most recent first.
123
190
  *
124
191
  * @param {object} db - sql.js boss.db instance
125
192
  * @param {object} options - {
@@ -127,6 +194,7 @@ async function analyzeAndStoreSession(db, session, opts = {}) {
127
194
  * onProgress: fn,
128
195
  * forceReanalyze: false,
129
196
  * dates: string[] // optional explicit YYYY-MM-DD list; overrides `days`
197
+ * // AND bypasses the "skip today" rule
130
198
  * }
131
199
  * @returns {Promise<{analyzed: number, errors: number, skipped: number}>}
132
200
  */
@@ -154,13 +222,15 @@ async function runAnalysisJob(db, options = {}) {
154
222
  : buildDateList(days);
155
223
 
156
224
  try {
157
- // Pre-calculate total count for progress reporting
225
+ // Pre-calculate total count for progress reporting. Subtract the
226
+ // "current" (still-running) session if it falls in the candidate set.
158
227
  let totalSessions = 0;
159
228
  for (const date of dates) {
160
229
  const sessions = forceReanalyze
161
230
  ? getSessionsByDate(db, date)
162
231
  : getUnanalyzedSessions(db, date);
163
- totalSessions += sessions.length;
232
+ const skipId = pickCurrentSessionId(db, date, sessions);
233
+ totalSessions += sessions.filter((s) => s.id !== skipId).length;
164
234
  }
165
235
 
166
236
  updateAnalysisState(db, {
@@ -181,10 +251,17 @@ async function runAnalysisJob(db, options = {}) {
181
251
  last_analyzed_at: null,
182
252
  });
183
253
 
184
- // 2a. Get unanalyzed sessions for this date
185
- const sessions = forceReanalyze
254
+ // 2a. Get unanalyzed sessions for this date, minus the currently-
255
+ // active session (today's session with the largest ended_at). We
256
+ // skip it so the job doesn't score a conversation that's still
257
+ // being written to — the next pass will catch it.
258
+ const rawSessions = forceReanalyze
186
259
  ? getSessionsByDate(db, date)
187
260
  : getUnanalyzedSessions(db, date);
261
+ const skipId = pickCurrentSessionId(db, date, rawSessions);
262
+ const sessions = skipId
263
+ ? rawSessions.filter((s) => s.id !== skipId)
264
+ : rawSessions;
188
265
 
189
266
  if (sessions.length === 0) {
190
267
  result.skipped++;