kindling_service/context.rs
1//! Injection-context assembly — structured data for the SessionStart and
2//! PreCompact hooks.
3//!
4//! These types carry the *data* the hook needs; they contain no markdown. The
5//! daemon (`kindling-server`) owns the byte-for-byte markdown formatting (so the
6//! `toLocaleString`-equivalent date logic lives in exactly one place). This
7//! split keeps the service deterministic and trivially testable, and keeps the
8//! formatting parity surface contained to one server module.
9//!
10//! Mirrors the queries performed inline by the Node hooks in
11//! `plugins/kindling-claude-code/hooks/{session-start,pre-compact}.js`.
12
13use kindling_types::{Observation, Summary};
14
15/// A pin resolved to its target's content, for injection previews.
16///
17/// `note` is the pin's reason (Node: `pin.note`); `content` is the *target*
18/// observation/summary content the pin points at (Node: `pin.content`, which
19/// the TS `listActivePins` join hydrates). Either may be absent: a pin can have
20/// no reason, and its target may have been redacted or deleted.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ResolvedPin {
23 /// Pin reason / note (`pin.note` in the Node hook). `None` renders as the
24 /// literal `Pin` label.
25 pub note: Option<String>,
26 /// Target content (`pin.content` in the Node hook). `None` renders as
27 /// `(no content)`.
28 pub content: Option<String>,
29}
30
31/// Structured data backing the SessionStart injection.
32///
33/// Built by [`KindlingService::session_start_context`](crate::KindlingService::session_start_context).
34/// The server formats `pins` under a `## Pinned Items` heading and `recent`
35/// under `## Recent Activity`, then prepends the Prior-Context header.
36#[derive(Debug, Clone, PartialEq)]
37pub struct SessionStartContext {
38 /// Active pins for the scope, newest first, each resolved to target content.
39 pub pins: Vec<ResolvedPin>,
40 /// Recent non-redacted observations, newest first, capped at `max_results`.
41 pub recent: Vec<Observation>,
42}
43
44impl SessionStartContext {
45 /// Whether there is anything to inject. The server emits `null`
46 /// `additionalContext` when this is empty.
47 pub fn is_empty(&self) -> bool {
48 self.pins.is_empty() && self.recent.is_empty()
49 }
50}
51
52/// Structured data backing the PreCompact injection.
53///
54/// Built by [`KindlingService::pre_compact_context`](crate::KindlingService::pre_compact_context).
55/// The server formats `pins` under `## Pinned Items (preserve across
56/// compaction)` and the summary under `## Session Summary` (no top-level
57/// header).
58#[derive(Debug, Clone, PartialEq)]
59pub struct PreCompactContext {
60 /// Active pins for the scope, newest first, each resolved to target content.
61 pub pins: Vec<ResolvedPin>,
62 /// The single latest summary across the scope's capsules, if any.
63 pub latest_summary: Option<Summary>,
64}
65
66impl PreCompactContext {
67 /// Whether there is anything to inject. An empty-string summary still counts
68 /// as absent, matching the Node hook's `latestSummary.content` truthiness
69 /// check — the service guarantees `latest_summary` is `None` in that case.
70 pub fn is_empty(&self) -> bool {
71 self.pins.is_empty() && self.latest_summary.is_none()
72 }
73}