@@ -7,6 +7,323 @@ section with a `⚠ breaking` prefix so they're easy to spot.
77
88## [ Unreleased]
99
10+ ## [ 0.2.0-pre.3] — 2026-04-27
11+
12+ The "projects + Ask command surface" release. Sessions now belong to
13+ first-class projects, templates inherit project defaults with
14+ per-field overrides, and Ask becomes a full command line for the
15+ dashboard — searching, summarizing, launching, editing, and watching,
16+ all through one approval pipeline. No breaking changes.
17+
18+ ### Added
19+
20+ #### Projects registry — first-class concept
21+
22+ - New ` projects ` table with name, cwd, optional GitHub URL, and
23+ default agentType / model / launchMode. Sessions get a nullable
24+ ` project_id ` FK that auto-resolves on event ingest via
25+ longest-prefix cwd match (path-segment-aware so ` /foo/bar `
26+ doesn't match ` /foo/barbaz ` ). An in-process cache loaded eagerly
27+ at boot keeps resolution off the DB hot path.
28+ - New ` /projects ` UI with create / edit / delete drawer, badges
29+ on ` SessionCard ` and ` SessionDetailPage ` header, and a Project
30+ filter on the dashboard. Endpoints: ` GET/POST /api/v1/projects ` ,
31+ ` GET/PUT/DELETE /api/v1/projects/:id ` ,
32+ ` GET /api/v1/projects/:id/sessions ` .
33+ - One-shot boot backfill stamps ` project_id ` on pre-existing
34+ sessions whose cwd matches an existing project — no manual
35+ re-resolution needed.
36+
37+ #### Template ↔ project linkage with live inheritance
38+
39+ - New ` session_templates.project_id ` FK + a
40+ ` template_project_overrides ` JSON sentinel so individual fields
41+ can be overridden without making ` agentType ` / ` cwd ` columns
42+ nullable. Project values flow live: change the project's
43+ ` defaultAgentType ` and every linked template renders the new
44+ value on next read. Override semantics: stored value wins where
45+ the user explicitly overrode, project value fills the rest.
46+ - Templates list endpoint resolves project values via a single
47+ IN-batch query so resolution stays O(1) extra queries no matter
48+ the list size.
49+ - Deleting a project nulls ` session_templates.project_id ` AND
50+ ` sessions.project_id ` AND removes the project row in one
51+ Drizzle transaction. A partial failure rolls all three back.
52+ - Auto-create-project on template save: if a template is saved
53+ without an explicit ` projectId ` , the server finds a project at
54+ the template's cwd or creates a new one (basename-derived name,
55+ numeric suffix on collision). The dropdown's first option now
56+ reads "Auto (match by directory)" to communicate the new
57+ default behavior.
58+
59+ #### AI Ask — read-only patterns (no approval, no mutation)
60+
61+ - ** NL session search.** "show me failed sessions",
62+ "stuck sessions", "find sessions about auth on agentpulse" —
63+ heuristic keyword gate (no LLM), pure-synchronous filter
64+ derivation for status / time / project, FTS query with `mode:
65+ "or"` and a direct-query fallback when the user message is
66+ all filter words. Each hit is enriched with status + agentType
67+ via a single batched session lookup.
68+ - ** Cross-cutting digest.** "what happened today",
69+ "give me a digest" — wraps the existing ` buildDigest ` service
70+ with a 5s ` Promise.race ` timeout and a "still loading" footer
71+ for instances with many live sessions whose intelligence
72+ classifiers are slow.
73+ - ** Per-session Q&A.** "summarize session X", "why did session Y
74+ fail" — bounded transcript (10k-token tail-truncated, oldest
75+ events dropped, provenance footer in every reply), spend-cap
76+ preflight + postflight, response cache keyed on
77+ ` (sessionId, sha256(normalizedQuestion)) ` invalidated by new
78+ events. New ` ai_qa_cache ` table; sweep purges expired rows.
79+
80+ #### AI Ask — mutations through ` ai_action_requests ` approval
81+
82+ - New ` ai_action_requests ` table with kinds: ` launch_request ` ,
83+ ` add_project ` , ` session_stop ` , ` session_archive ` ,
84+ ` session_delete ` , ` edit_project ` , ` delete_project ` ,
85+ ` edit_template ` , ` delete_template ` , ` add_channel ` ,
86+ ` create_alert_rule ` , ` create_freeform_alert_rule ` ,
87+ ` bulk_session_action ` . Atomic claim via conditional UPDATE
88+ (` awaiting_reply → applying ` ) prevents double-execution on
89+ concurrent web + Telegram approvals; `applying → applied /
90+ failed / expired` lifecycle with ` failure_reason`.
91+ - ** AI-initiated session launches.** "open a Claude session for
92+ agentpulse" — keyword gate + LLM classifier resolve project
93+ and mode, validate against connected supervisors via pure
94+ helpers (` pickFirstCapableSupervisor ` , ` buildLaunchSpec `
95+ extracted from existing impure code), then create an
96+ approval card. On approve the executor re-validates the
97+ supervisor and dispatches through the existing ` /launches `
98+ pipeline. Reroute path rebuilds the launch spec when the
99+ originally-validated host is gone.
100+ - ** AI-driven add-project (multi-turn drafting).** "add a
101+ project myapp at /tmp/myapp" — new
102+ ` ai_pending_project_drafts ` table holds in-flight drafts
103+ keyed on ` ask_thread_id ` . The AI walks the user through
104+ numbered questions for missing fields one turn at a time;
105+ parsing each reply is pure synchronous so continuation
106+ turns make no LLM call. ` cancel ` /` abort ` /` stop drafting ` /
107+ ` never mind ` aborts a draft from any question; retry cap
108+ of 3 per field expires the draft cleanly.
109+ - ** Quick session actions.** Pin / note / rename run direct
110+ with the resolved session name embedded in the reply for
111+ verification; stop / archive / delete go through approval.
112+ Notes append now (existing notes preserved with ` \n `
113+ separator); rename replies include an explicit undo hint.
114+ Stop pre-flight rejects hook-only sessions before creating
115+ an action_request — ` queueStopAction ` only works on
116+ managed sessions.
117+ - ** Resume / continue with a new prompt.** "continue
118+ brave-falcon with: refactor the auth module" — builds a new
119+ managed launch inheriting the parent session's cwd /
120+ agentType / model with the user's text as ` taskPrompt ` .
121+ Reuses the existing ` launch_request ` kind; the inbox card
122+ reads ` payload.parentSessionId ` and renders "Resume of
123+ * parentName* " when present so approvers see the resume
124+ context instead of a generic "New launch" title.
125+ - ** Edit / delete project + template via Ask.** Four new
126+ action_request kinds. Delete cards include affected-template
127+ and affected-session counts so the approver sees the blast
128+ radius. Project deletion still uses the transactional
129+ cleanup so linked templates and sessions are nulled
130+ atomically.
131+ - ** Notification channel setup via Ask.** "set up a Telegram
132+ channel called personal" — heuristic kind detection
133+ (` telegram ` / ` webhook ` / ` email ` ); the executor calls
134+ ` createPendingChannel ` and sends per-kind enrollment
135+ instructions back through ` notifyOriginUser ` .
136+ - ** Bulk session operations.** "archive all completed
137+ sessions on agentpulse" — classifier picks one of two
138+ resolution strategies (attribute-based SQL or hint-based
139+ FTS). Pre-flight excludes incompatible targets per action
140+ (stop excludes hook-only; delete excludes active sessions);
141+ cap at 50 targets, 20-name preview with "+N more" footer.
142+ Per-target try/catch keeps a single failure from poisoning
143+ the rest of the batch; outcome summary message reports
144+ per-target results.
145+
146+ #### Project-level watcher alert rules
147+
148+ - New ` project_alert_rules ` table with `REFERENCES projects(id)
149+ ON DELETE CASCADE` , plus ` project_alert_rule_fires` for
150+ de-bounce. Rule types: ` status_failed ` , ` status_completed ` ,
151+ ` status_stuck ` , ` no_activity_minutes ` , ` freeform_match ` .
152+ ` WatcherRunner ` gains a 60-second sweep with re-entry guard
153+ (` alertSweepBusy ` flag matching the ` RunLeaser ` precedent);
154+ evaluation extracted to ` alert-rule-evaluator.ts ` .
155+ - ** First-run backfill** at rule creation inserts fire rows for
156+ every session that already matches the rule's predicate, with
157+ no notification dispatched. Without this, a freshly-created
158+ ` status_stuck ` rule on a project with thirty already-stuck
159+ sessions would notification-storm the user.
160+ - ** Freeform watcher rules.** Natural-language conditions like
161+ "alert when the agent mentions a security concern" run a small
162+ yes/no LLM classifier per qualifying event. Per-rule daily
163+ token budget stored on the rule row, atomic daily reset via
164+ SQL ` CASE ` so a process restart can't read a stale zero,
165+ per-rule ` last_evaluated_event_id ` cursor + 100-event-per-sweep
166+ cap so a backlog can't blow the budget in one tick. Sample rate
167+ with cursor advance before sampling so 0.5 still bounds work.
168+ Spend recorded only on successful classification.
169+
170+ #### Search highlight + event-context
171+
172+ - Search-result event hits now scroll the activity timeline to
173+ the matching event and apply a 2.2s amber flash. A ` useRef `
174+ guard ensures the flash fires exactly once per
175+ ` (sessionId, eventId) ` pair even as new events stream in via
176+ WebSocket; the ref also marks 404 / network failures as
177+ terminal so the effect can't loop on deleted events.
178+ - New ` GET /api/v1/sessions/:sessionId/events/:eventId/context?around=N `
179+ endpoint returns the target event ± a window (default 20, max
180+ 100). Used by the frontend to splice older events into the
181+ timeline state when the search hit references an event outside
182+ the loaded window.
183+
184+ #### Telemetry classification + diagnostics
185+
186+ - Telemetry pings now include an ` install_class ` field
187+ (` production ` / ` self_hosted_real ` / ` dev ` / ` test ` / ` ci ` )
188+ inferred from build channel, with explicit overrides via
189+ ` AGENTPULSE_TELEMETRY_MODE ` and ` AGENTPULSE_TELEMETRY_TEST=1 ` .
190+ Local and CI runs no longer pollute real-world install counts.
191+ - Added a ` first_boot ` vs ` heartbeat ` event_kind so the homepage
192+ adoption number can show distinct installs vs activity.
193+ - New ` GET /api/v1/settings/telemetry/status ` returns last-attempt
194+ diagnostics; ` POST /api/v1/settings/telemetry/ping ` triggers an
195+ immediate send. Both gated by ` requireAuth ` .
196+
197+ ### Changed
198+
199+ - ` resolveActionRequest ` dispatches via a ` KIND_EXECUTORS `
200+ registry object instead of an if-chain. Each new action kind is
201+ a one-line registry entry; an unsupported kind fails cleanly
202+ with ` Unsupported action kind: <kind> ` .
203+ - Inbox card rendering split into per-kind components under
204+ ` src/web/components/inbox/ ` . The dispatch is a single
205+ exhaustive ` switch ` on ` item.kind ` so TypeScript flags any
206+ missing case at compile time.
207+ - ` decideActionRequest ` route now labels failures by action kind
208+ ("Project edit failed: …", "Bulk session action failed: …",
209+ "Freeform alert rule failed: …") instead of always saying
210+ "Launch failed: …". The 422 / 409 split distinguishes terminal
211+ failure during this approval (expired / failed) from a real
212+ race-lost (another approval claimed first).
213+ - ` evaluateAlertRules ` extracted from ` event-processor.ts ` into a
214+ dedicated ` alert-rule-evaluator.ts ` so the four rule-type
215+ evaluators share one home with the shared
216+ ` dispatchAlertRuleNotification ` helper.
217+ - ` resolveSession ` extracted from ` ask-session-action-handler.ts `
218+ to a shared ` ask-resolver.ts ` so Slice B's Q&A handler and
219+ Slice C's bulk handler can use the same FTS-backed
220+ ambiguity-protocol session picker without depending on the
221+ session-action handler.
222+ - ` sendTelegramActionRequest ` extracted from
223+ ` ask-launch-handler.ts ` into ` telegram-helpers.ts ` since four
224+ handlers now need it.
225+ - ` updateTemplate ` and ` deleteTemplate ` extracted from inline
226+ route logic into a ` templates-service.ts ` module so the new
227+ edit / delete executors can call service functions instead of
228+ duplicating route logic.
229+ - Ask ` runAskTurn ` chain now processes intent gates in this
230+ order: open-draft continuation → digest gate → search gate →
231+ add-project gate → session-action gate → resume gate → CRUD
232+ gate → channel gate → alert-rule gate → bulk gate → launch
233+ gate → normal LLM completion. Multi-turn drafting always wins
234+ over a fresh intent on the same thread.
235+ - ` SearchFilters.sessionStatus ` now accepts ` "failed" ` . Closes a
236+ previously-undocumented gap where ` failed ` was a valid
237+ ` Session.status ` value but couldn't be filtered against in
238+ the search UI or NL search resolver.
239+ - ` sessions ` table gains a ` is_archived ` boolean column,
240+ orthogonal to ` status ` so a failed or completed session can be
241+ archived without losing its terminal status. The new
242+ ` PUT /api/v1/sessions/:id/archive ` route flips this flag; the
243+ CLAUDE.md route reference is now backed by an actual handler.
244+ - ` launch_requests ` table gains a nullable ` parent_session_id `
245+ column for traceability of resume launches. The launch
246+ pipeline does not read it; future "session tree" UI will.
247+ - ` CreateActionRequestInput.kind ` widened to the full eleven
248+ kinds the plan introduces, in one schema-less union edit, so
249+ each new slice's executor branch fails compilation cleanly
250+ until its handler lands.
251+ - ` notes ` semantics for the AI's add-note path are now append
252+ (read existing, concat with ` \n ` , write back) so the AI can't
253+ silently destroy prior notes. The direct
254+ ` PUT /sessions/:id/notes ` route is unchanged (full replace);
255+ this only differs in the ` add_note ` Ask path.
256+ - Local OpenAI-compatible providers (Ollama, vLLM, llama.cpp)
257+ now receive ` reasoning_effort: "none" ` on classifier calls so
258+ qwen3 and similar reasoning models return clean JSON instead
259+ of burying the response in chain-of-thought. The existing
260+ ` think: false ` and `chat_template_kwargs.enable_thinking:
261+ false` flags were silently dropped by Ollama's
262+ ` /v1/chat/completions ` endpoint — kept for back-compat but
263+ ` reasoning_effort ` is what does the work. Anthropic / OpenAI /
264+ Google / OpenRouter providers receive the prompt unchanged.
265+
266+ ### Fixed
267+
268+ - ** Default-projectId stamping race.** ` bumpVersion() ` originally
269+ fired the cache reload as ` void reloadCache() ` — non-blocking.
270+ ` createProject ` immediately fed ` getCachedProjects() ` into
271+ ` resolveAllSessionsForProject ` , so a freshly-created project
272+ could miss its own session-stamp pass and leave matching
273+ sessions unstamped until the next event-ingest. Now
274+ ` bumpVersionAndReload ` awaits the reload before returning.
275+ - ** Search-highlight context fetch could loop on deleted events.**
276+ When the target event id no longer existed (` 404 ` from the
277+ context endpoint), the effect's ` loadingContext ` flip retriggered
278+ the same fetch on the next render — infinite 404s. The catch
279+ branch now marks ` (sessionId, eventId) ` as terminal in
280+ ` flashedRef ` so the early-return chain short-circuits the
281+ effect on subsequent re-runs.
282+ - ** Misleading "race_lost" message** when a ` /decide ` call's own
283+ approval transitioned the action_request to ` expired ` or
284+ ` failed ` during execution. The route conflated genuine race
285+ losses with terminal-during-this-attempt outcomes. The resolver
286+ now returns a discriminated ` ResolveResult ` and the route
287+ branches on ` reason ` — race-lost is 409, terminal failure is
288+ 422 with the real reason.
289+ - ** ` sessions.status = "failed" ` was never written.** The value
290+ existed in the type union and schema comment but had no
291+ producer in the codebase. Launch dispatch now invokes
292+ ` markSessionFailed ` when a launch transitions to failed —
293+ required before the ` status_failed ` alert rule could fire on
294+ anything.
295+ - ** Periodic alert-rule sweep had no re-entry guard.** A slow
296+ sweep (50 sessions × Telegram round-trip) overlapping with
297+ the next 60s tick could produce two concurrent Telegram
298+ messages for the same rule/session before the UNIQUE
299+ constraint stopped the second DB insert. Added an
300+ ` alertSweepBusy ` flag matching the ` RunLeaser ` precedent.
301+ - ** ` no_activity_minutes ` filter missed idle-but-not-stopped
302+ sessions.** Original spec used ` isWorking = true ` which
303+ doesn't catch sessions that emitted ` Stop ` but haven't
304+ started a new task. Filter now uses ` endedAt IS NULL ` .
305+ - ** Daily token-budget reset for freeform rules was a
306+ read-modify-write race.** A process restart mid-day could
307+ re-read a stale ` 0 ` from the previous reset and classify
308+ events that should have been blocked. Reset is now an atomic
309+ SQL ` CASE ` UPDATE per row so concurrent processes can't
310+ diverge.
311+ - ** Spend counter incremented on LLM errors.** Freeform-rule
312+ classification now records spend only on successful
313+ classification — ` classifyFreeformCondition ` returns a
314+ discriminated ` ClassifyResult ` and the caller skips spend
315+ recording on the error path.
316+ - ** Telegram approve-callback identifier mismatch fixed in the
317+ add-project flow.** Action_requests now persist
318+ ` notification_channels.id ` (UUID) on the ` channelId ` column,
319+ not the raw Telegram chat id; inbound callbacks look up the
320+ channel by chat id and match on the persisted UUID — same
321+ pattern HITL already uses.
322+ - ** Lint formatter drift on telemetry classification commit**
323+ fixed before the merge so the project's ` bun run check `
324+ stays at zero errors.
325+
326+
10327## [ 0.2.0-pre.2] — 2026-04-25
11328
12329The "find any past conversation" release. Three new layers stack on
@@ -459,6 +776,8 @@ All AI features ship gated behind per-feature Labs flags and a master
459776- Local install: ` docker run -d -p 3000:3000 -v agentpulse-data:/app/data -e DISABLE_AUTH=true ` .
460777- Remote hook relay: ` curl -sSL https://server/setup-relay.sh | bash -s -- --key ap_xxx ` .
461778
462- [ Unreleased ] : https://github.com/jstuart0/agentpulse/compare/v0.2.0-pre.1...HEAD
779+ [ Unreleased ] : https://github.com/jstuart0/agentpulse/compare/v0.2.0-pre.3...HEAD
780+ [ 0.2.0-pre.3 ] : https://github.com/jstuart0/agentpulse/releases/tag/v0.2.0-pre.3
781+ [ 0.2.0-pre.2 ] : https://github.com/jstuart0/agentpulse/releases/tag/v0.2.0-pre.2
463782[ 0.2.0-pre.1 ] : https://github.com/jstuart0/agentpulse/releases/tag/v0.2.0-pre.1
464783[ 0.1.0 ] : https://github.com/jstuart0/agentpulse/releases/tag/v0.1.0
0 commit comments