Skip to main content

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}