clementine-agent 1.18.181 → 1.18.184

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.
@@ -1025,6 +1025,21 @@ export class PersonalAssistant {
1025
1025
  }
1026
1026
  }
1027
1027
  // ── System Prompt Builder ─────────────────────────────────────────
1028
+ //
1029
+ // 1.18.184 caveat: the canonical CHAT path no longer reaches this
1030
+ // builder. Chat goes through runAgent (src/agent/run-agent.ts:679)
1031
+ // with `systemPrompt: { type: 'preset', preset: 'claude_code',
1032
+ // append: <buildChatSystemAppend(...)> }`. The recall + trust posture
1033
+ // directives live in `run-agent-context.ts:BEHAVIORAL_POSTURE`.
1034
+ //
1035
+ // This buildSystemPrompt is now invoked only from:
1036
+ // - plan-step path (`processPlanStep` ~line 3396)
1037
+ // - auto-memory extraction Haiku passes (~line 3187)
1038
+ // - cron-reflection Haiku passes
1039
+ //
1040
+ // If you're adding chat-time behavioral guidance, add it to
1041
+ // BEHAVIORAL_POSTURE in run-agent-context.ts. Adding it here will
1042
+ // NOT affect real chat — only the legacy Haiku/plan-step paths.
1028
1043
  buildSystemPrompt(opts = {}) {
1029
1044
  const { isHeartbeat = false, cronTier = null, retrievalContext = '', profile = null, sessionKey = null, model = null, verboseLevel, intentClassification = null, contextTier = 'full', toolsAvailable = true, composioConnectedSlugs = [] } = opts;
1030
1045
  const isAutonomous = isHeartbeat || cronTier !== null;
@@ -1208,8 +1223,6 @@ Obsidian vault with YAML frontmatter, [[wikilinks]], #tags.
1208
1223
  **Remembering:** Durable facts → memory_write(action="update_memory"). Daily context → note_take / memory_write(action="append_daily"). New person → note_create. New task → task_add.
1209
1224
  Save important facts immediately; a background agent also extracts after each exchange.
1210
1225
 
1211
- **Recalling — REQUIRED behavior:** When the user references past work you don't have in immediate context — a URL, a deployment, a file you created, a task or background job you ran, a person/project/domain name you don't have inline — call \`memory_search\` (or \`transcript_search\` for chat history) BEFORE asking the user to provide it and BEFORE replying that you have no record. Saying "I don't see any record of that" without having searched is a memory failure, not an honest answer. Background tasks, cron runs, deployments, and prior chat turns are all in the SQLite memory store with dense embeddings — semantic search will surface them even when the wording doesn't match exactly.
1212
-
1213
1226
  ## Self-Configuration (never tell ${owner} to edit a config file)
1214
1227
 
1215
1228
  Clementine is self-configuring. Every credential, every integration, every tool permission can be set by calling a tool — no hand-editing.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * chat-stop-hook — Stop hook that keeps chat-initiated multi-step jobs
3
+ * running until they finish OR the user explicitly stops them.
4
+ *
5
+ * Why this exists (1.18.184)
6
+ * ──────────────────────────
7
+ * Before this hook, the SDK loop ended whenever the model produced
8
+ * a final assistant message — even when the model had clearly stated
9
+ * "next, I'll do X" but then stopped. From the user's POV: "I asked her
10
+ * to draft 3 emails and she only drafted 1, no explanation." The model
11
+ * was being prematurely terminated by the SDK's default Stop behavior.
12
+ *
13
+ * The canonical SDK pattern for long-running agentic loops is a Stop
14
+ * hook that:
15
+ * 1. Detects when the model said it would continue but didn't.
16
+ * 2. Returns `decision: 'block'` with a `reason` that re-prompts
17
+ * the model to keep going.
18
+ * 3. NEVER blocks if Stop hooks have already fired this run
19
+ * (`input.stop_hook_active === true`) — that's the SDK's
20
+ * anti-infinite-loop guardrail and we honor it.
21
+ * 4. NEVER blocks if the user has aborted (abortSignal fired) —
22
+ * user intent always wins.
23
+ *
24
+ * What this hook does NOT do
25
+ * ──────────────────────────
26
+ * It does NOT force every chat turn to keep going. The default path
27
+ * is to LET THE MODEL FINISH. The hook only intervenes when:
28
+ * (a) the last assistant message contains a clear "more work to do"
29
+ * signal (e.g., "next, I'll", "step 2:", "I'll continue with"), AND
30
+ * (b) the user has NOT issued a stop / cancel, AND
31
+ * (c) we haven't already re-blocked this run.
32
+ *
33
+ * Conservative by design: better to let one job finish slightly short
34
+ * than to spin forever. If a job needs to run long, the user can
35
+ * always re-ask.
36
+ *
37
+ * Aligned with Anthropic SDK best practices: Stop hooks fire even
38
+ * under `bypassPermissions`, which is the canonical lever for
39
+ * "agentic loop that keeps going." See `sdk.d.ts:5483-5492` for the
40
+ * `StopHookInput` shape including the `stop_hook_active` guard.
41
+ */
42
+ import type { HookCallbackMatcher, HookEvent } from '@anthropic-ai/claude-agent-sdk';
43
+ export interface StopHookOptions {
44
+ /** Stable run identifier for telemetry. */
45
+ runId: string;
46
+ /** Optional abort signal to honor — if it fires, the hook will
47
+ * never re-block. User-initiated stops always win. */
48
+ abortSignal?: AbortSignal;
49
+ /** Optional callback fired on every decision. Useful for the
50
+ * dashboard "What Clementine sees this turn" panel. */
51
+ onDecision?: (info: {
52
+ decision: 'pass' | 'continue';
53
+ reason?: string;
54
+ lastMessagePreview: string;
55
+ stopHookActive: boolean;
56
+ }) => void;
57
+ }
58
+ export interface StopHookStats {
59
+ /** Total Stop events inspected. */
60
+ inspected: number;
61
+ /** Stop events that passed through (model finished cleanly). */
62
+ passed: number;
63
+ /** Stop events where we re-prompted the model to continue. */
64
+ continued: number;
65
+ }
66
+ export interface StopHookHandles {
67
+ /** Hook map suitable for SDK `query({ options: { hooks } })`. */
68
+ hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>>;
69
+ /** Aggregated telemetry — read after the run completes. */
70
+ stats: StopHookStats;
71
+ }
72
+ /**
73
+ * Build a Stop hook for a chat-initiated agentic run.
74
+ */
75
+ export declare function buildChatStopHook(opts: StopHookOptions): StopHookHandles;
76
+ //# sourceMappingURL=chat-stop-hook.d.ts.map
@@ -0,0 +1,155 @@
1
+ /**
2
+ * chat-stop-hook — Stop hook that keeps chat-initiated multi-step jobs
3
+ * running until they finish OR the user explicitly stops them.
4
+ *
5
+ * Why this exists (1.18.184)
6
+ * ──────────────────────────
7
+ * Before this hook, the SDK loop ended whenever the model produced
8
+ * a final assistant message — even when the model had clearly stated
9
+ * "next, I'll do X" but then stopped. From the user's POV: "I asked her
10
+ * to draft 3 emails and she only drafted 1, no explanation." The model
11
+ * was being prematurely terminated by the SDK's default Stop behavior.
12
+ *
13
+ * The canonical SDK pattern for long-running agentic loops is a Stop
14
+ * hook that:
15
+ * 1. Detects when the model said it would continue but didn't.
16
+ * 2. Returns `decision: 'block'` with a `reason` that re-prompts
17
+ * the model to keep going.
18
+ * 3. NEVER blocks if Stop hooks have already fired this run
19
+ * (`input.stop_hook_active === true`) — that's the SDK's
20
+ * anti-infinite-loop guardrail and we honor it.
21
+ * 4. NEVER blocks if the user has aborted (abortSignal fired) —
22
+ * user intent always wins.
23
+ *
24
+ * What this hook does NOT do
25
+ * ──────────────────────────
26
+ * It does NOT force every chat turn to keep going. The default path
27
+ * is to LET THE MODEL FINISH. The hook only intervenes when:
28
+ * (a) the last assistant message contains a clear "more work to do"
29
+ * signal (e.g., "next, I'll", "step 2:", "I'll continue with"), AND
30
+ * (b) the user has NOT issued a stop / cancel, AND
31
+ * (c) we haven't already re-blocked this run.
32
+ *
33
+ * Conservative by design: better to let one job finish slightly short
34
+ * than to spin forever. If a job needs to run long, the user can
35
+ * always re-ask.
36
+ *
37
+ * Aligned with Anthropic SDK best practices: Stop hooks fire even
38
+ * under `bypassPermissions`, which is the canonical lever for
39
+ * "agentic loop that keeps going." See `sdk.d.ts:5483-5492` for the
40
+ * `StopHookInput` shape including the `stop_hook_active` guard.
41
+ */
42
+ import pino from 'pino';
43
+ const logger = pino({ name: 'clementine.chat-stop-hook' });
44
+ /**
45
+ * Phrases in the last assistant message that signal "more work to do."
46
+ * Conservative — we only continue when the model EXPLICITLY said it
47
+ * would. Vague endings ("Let me know if you need anything else.") do
48
+ * NOT trigger; those are clean completions.
49
+ */
50
+ const CONTINUATION_SIGNALS = [
51
+ // Explicit "next step" / sequencing
52
+ /\bnext,?\s+(?:i'?ll|i\s+will|let'?s|i'?m\s+going\s+to)\b/i,
53
+ /\bstep\s+\d+:/i,
54
+ /\bphase\s+\d+:/i,
55
+ /\bi'?ll\s+(?:now|then|next)\b/i,
56
+ /\bi\s+will\s+(?:now|then|next)\b/i,
57
+ // "Continuing with" / "moving on"
58
+ /\bcontinuing\s+(?:with|to)\b/i,
59
+ /\bmoving\s+on\s+to\b/i,
60
+ /\bi'?ll\s+continue\s+(?:by|with)\b/i,
61
+ // Promised remainder of a list
62
+ /\b(?:second|third|fourth|fifth|remaining|rest)\s+(?:email|email\.?|draft|item|step)/i,
63
+ // "After this, I'll"
64
+ /\bafter\s+(?:this|that),?\s+i'?ll\b/i,
65
+ ];
66
+ /**
67
+ * Build a Stop hook for a chat-initiated agentic run.
68
+ */
69
+ export function buildChatStopHook(opts) {
70
+ const stats = { inspected: 0, passed: 0, continued: 0 };
71
+ const stopHook = async (input) => {
72
+ if (input.hook_event_name !== 'Stop')
73
+ return {};
74
+ const evt = input;
75
+ stats.inspected += 1;
76
+ const lastMsg = evt.last_assistant_message ?? '';
77
+ const lastMessagePreview = lastMsg.slice(0, 160).replace(/\s+/g, ' ').trim();
78
+ // ── Guard 1: anti-infinite-loop ───────────────────────────────
79
+ // stop_hook_active is true if Stop hooks have ALREADY fired this
80
+ // run. SDK uses this exact field to prevent us re-blocking
81
+ // forever. If it's set, we must pass through.
82
+ if (evt.stop_hook_active) {
83
+ stats.passed += 1;
84
+ logger.debug({
85
+ runId: opts.runId,
86
+ reason: 'stop_hook_active',
87
+ lastMessagePreview,
88
+ }, 'Stop hook passing — already active');
89
+ opts.onDecision?.({
90
+ decision: 'pass',
91
+ reason: 'stop_hook_active',
92
+ lastMessagePreview,
93
+ stopHookActive: true,
94
+ });
95
+ return {};
96
+ }
97
+ // ── Guard 2: user-initiated stop ──────────────────────────────
98
+ // If the abort signal has fired, the user wants out. Never
99
+ // re-block. User intent ALWAYS wins.
100
+ if (opts.abortSignal?.aborted) {
101
+ stats.passed += 1;
102
+ logger.debug({
103
+ runId: opts.runId,
104
+ reason: 'user_aborted',
105
+ lastMessagePreview,
106
+ }, 'Stop hook passing — user aborted');
107
+ opts.onDecision?.({
108
+ decision: 'pass',
109
+ reason: 'user_aborted',
110
+ lastMessagePreview,
111
+ stopHookActive: false,
112
+ });
113
+ return {};
114
+ }
115
+ // ── Detection: did the model say it would continue? ──────────
116
+ const continuationMatched = CONTINUATION_SIGNALS.some((rx) => rx.test(lastMsg));
117
+ if (!continuationMatched) {
118
+ // No continuation signal — let the model finish.
119
+ stats.passed += 1;
120
+ opts.onDecision?.({
121
+ decision: 'pass',
122
+ reason: 'clean_completion',
123
+ lastMessagePreview,
124
+ stopHookActive: false,
125
+ });
126
+ return {};
127
+ }
128
+ // ── Re-prompt: keep going ──────────────────────────────────────
129
+ stats.continued += 1;
130
+ const reason = 'You said you would continue with more work but the loop is about to end. ' +
131
+ 'Keep going — finish the remaining steps you outlined. ' +
132
+ 'If you genuinely cannot continue (waiting on external input, hit a hard error, etc.), say so explicitly in your next message so the owner knows where you stopped.';
133
+ logger.info({
134
+ runId: opts.runId,
135
+ lastMessagePreview,
136
+ }, 'Stop hook re-prompting model to continue work it announced');
137
+ opts.onDecision?.({
138
+ decision: 'continue',
139
+ reason,
140
+ lastMessagePreview,
141
+ stopHookActive: false,
142
+ });
143
+ return {
144
+ decision: 'block',
145
+ reason,
146
+ };
147
+ };
148
+ return {
149
+ hooks: {
150
+ Stop: [{ hooks: [stopHook] }],
151
+ },
152
+ stats,
153
+ };
154
+ }
155
+ //# sourceMappingURL=chat-stop-hook.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * clementine-turn-context — the volatile per-turn context block that
3
+ * reconstitutes Clementine's live awareness on every chat turn.
4
+ *
5
+ * Why this exists (1.18.184)
6
+ * ──────────────────────────
7
+ * The modern chat path's system prompt is the SDK's `claude_code`
8
+ * preset + `buildChatSystemAppend()` (run-agent-context.ts). That gives
9
+ * Clementine her identity (SOUL) and her hand-curated long-term memory
10
+ * (MEMORY.md), but it is STATIC across turns by design — anything that
11
+ * varies per turn must NOT live there or it would invalidate the
12
+ * Anthropic prompt cache.
13
+ *
14
+ * Anything volatile — what's true right now, what just happened, what
15
+ * the SQLite memory store has relevant to the current message — lives
16
+ * here, in a block prepended to the user's message. The SDK treats
17
+ * that as turn input, not as system prompt, so it doesn't break cache
18
+ * and it gives the model fresh context every turn.
19
+ *
20
+ * What we put in the block
21
+ * ────────────────────────
22
+ * 1. Retrieved memory hits from the SQLite store (semantic + FTS),
23
+ * scored against the user's current message. The single highest-
24
+ * leverage section — this is how persistent memory of EVERYTHING
25
+ * actually reaches the model.
26
+ * 2. Recent background-task headlines (last 24h, terminal status only)
27
+ * so the model knows what work just completed without re-asking.
28
+ * 3. Live state — current date/time + channel/identity framing.
29
+ * 4. Extension points for the deeper learning subsystems (decision-
30
+ * reflection, skill-quality, insight-engine, seed-user-model,
31
+ * goal-evaluator) — each one is a labeled section that returns
32
+ * empty today and can be wired in a follow-up ship without
33
+ * re-architecting.
34
+ *
35
+ * Hard cap on total block size — see MAX_BLOCK_CHARS. Anthropic's prompt
36
+ * cache benefit dies if the volatile block is larger than the cacheable
37
+ * prefix, so keep this tight.
38
+ *
39
+ * Aligned with Anthropic SDK best practices: per-turn dynamic context
40
+ * in the USER message, NOT in the system prompt. See the SDK reference
41
+ * note on prompt caching boundaries.
42
+ */
43
+ import type { BackgroundTask } from '../types.js';
44
+ export interface BuildTurnContextOptions {
45
+ /** The user's current message — used as the query for retrieved memory. */
46
+ userMessage: string;
47
+ /** Session key for the active chat. Used for log breadcrumbs and to
48
+ * scope future per-session reads (currently unused; searchContext
49
+ * is intentionally cross-session for single-owner installs). */
50
+ sessionKey: string;
51
+ /** Where the user is reaching Clementine from. Surfaces in the
52
+ * identity framing block. Examples: "discord-dm", "dashboard",
53
+ * "slack-channel", "chat". */
54
+ channel?: string;
55
+ /** Owner-facing name (display name, not slug). When set, used in
56
+ * the identity framing block. */
57
+ ownerName?: string | null;
58
+ /** Active hired-agent profile if running as one. Affects the
59
+ * identity framing — "you are talking to Sasha right now," not
60
+ * "you are Clementine". */
61
+ profileName?: string | null;
62
+ /** Read-only memory store handle. When absent, retrieved-memory
63
+ * section is skipped — the rest still renders. */
64
+ memoryStore?: {
65
+ searchContext?: (query: string, opts?: {
66
+ limit?: number;
67
+ }) => Array<{
68
+ source_file?: string;
69
+ section?: string;
70
+ content: string;
71
+ score?: number;
72
+ }>;
73
+ } | null;
74
+ /** Optional override: synchronous read of recent terminal-state bg
75
+ * tasks. Defaults to a one-time module-cached listBackgroundTasks
76
+ * import (lazy, since not all callers have one). */
77
+ listBackgroundTasks?: (filter: {
78
+ status?: BackgroundTask['status'];
79
+ }) => BackgroundTask[];
80
+ /** Clock injection for tests. Defaults to Date.now(). */
81
+ now?: () => number;
82
+ }
83
+ export interface BuildTurnContextResult {
84
+ /** The full ready-to-prepend context block, INCLUDING outer
85
+ * `[Context...]\n...\n[/Context]\n\n` fence. Empty string when no
86
+ * sections produced output (e.g., builder sessions or completely
87
+ * empty stores) — caller can treat empty as "no prefix needed". */
88
+ block: string;
89
+ /** Telemetry — which sections contributed, for the dashboard
90
+ * "what Clementine sees this turn" panel. */
91
+ sections: {
92
+ retrievedMemory: number;
93
+ recentBgTasks: number;
94
+ liveState: boolean;
95
+ identityFrame: boolean;
96
+ };
97
+ /** Final character count of the block. Useful for logging + the
98
+ * Anthropic prompt-cache-health analysis. */
99
+ totalChars: number;
100
+ }
101
+ export declare function buildClementineTurnContext(opts: BuildTurnContextOptions): BuildTurnContextResult;
102
+ //# sourceMappingURL=clementine-turn-context.d.ts.map
@@ -0,0 +1,210 @@
1
+ /**
2
+ * clementine-turn-context — the volatile per-turn context block that
3
+ * reconstitutes Clementine's live awareness on every chat turn.
4
+ *
5
+ * Why this exists (1.18.184)
6
+ * ──────────────────────────
7
+ * The modern chat path's system prompt is the SDK's `claude_code`
8
+ * preset + `buildChatSystemAppend()` (run-agent-context.ts). That gives
9
+ * Clementine her identity (SOUL) and her hand-curated long-term memory
10
+ * (MEMORY.md), but it is STATIC across turns by design — anything that
11
+ * varies per turn must NOT live there or it would invalidate the
12
+ * Anthropic prompt cache.
13
+ *
14
+ * Anything volatile — what's true right now, what just happened, what
15
+ * the SQLite memory store has relevant to the current message — lives
16
+ * here, in a block prepended to the user's message. The SDK treats
17
+ * that as turn input, not as system prompt, so it doesn't break cache
18
+ * and it gives the model fresh context every turn.
19
+ *
20
+ * What we put in the block
21
+ * ────────────────────────
22
+ * 1. Retrieved memory hits from the SQLite store (semantic + FTS),
23
+ * scored against the user's current message. The single highest-
24
+ * leverage section — this is how persistent memory of EVERYTHING
25
+ * actually reaches the model.
26
+ * 2. Recent background-task headlines (last 24h, terminal status only)
27
+ * so the model knows what work just completed without re-asking.
28
+ * 3. Live state — current date/time + channel/identity framing.
29
+ * 4. Extension points for the deeper learning subsystems (decision-
30
+ * reflection, skill-quality, insight-engine, seed-user-model,
31
+ * goal-evaluator) — each one is a labeled section that returns
32
+ * empty today and can be wired in a follow-up ship without
33
+ * re-architecting.
34
+ *
35
+ * Hard cap on total block size — see MAX_BLOCK_CHARS. Anthropic's prompt
36
+ * cache benefit dies if the volatile block is larger than the cacheable
37
+ * prefix, so keep this tight.
38
+ *
39
+ * Aligned with Anthropic SDK best practices: per-turn dynamic context
40
+ * in the USER message, NOT in the system prompt. See the SDK reference
41
+ * note on prompt caching boundaries.
42
+ */
43
+ import pino from 'pino';
44
+ const logger = pino({ name: 'clementine.turn-context' });
45
+ // ── Tunables ──────────────────────────────────────────────────────────
46
+ /** Hard cap on the entire block. Keep volatile content small so the
47
+ * cacheable prefix stays larger than the dynamic delta. */
48
+ const MAX_BLOCK_CHARS = 4_000;
49
+ /** Per-section caps so any one section can't crowd out the others. */
50
+ const MAX_MEMORY_HITS = 6;
51
+ const MAX_MEMORY_HIT_CHARS = 320;
52
+ const MAX_BG_TASKS = 3;
53
+ const MAX_BG_TASK_LINE_CHARS = 200;
54
+ const RECENT_BG_WINDOW_MS = 24 * 60 * 60 * 1000;
55
+ // ── The builder ───────────────────────────────────────────────────────
56
+ export function buildClementineTurnContext(opts) {
57
+ const sections = {
58
+ retrievedMemory: 0,
59
+ recentBgTasks: 0,
60
+ liveState: false,
61
+ identityFrame: false,
62
+ };
63
+ const parts = [];
64
+ const nowMs = (opts.now ?? Date.now)();
65
+ const nowDate = new Date(nowMs);
66
+ // ── 1. Retrieved memory hits ──────────────────────────────────────
67
+ // The single most important section. Pulls the top semantic + FTS
68
+ // hits from the SQLite memory store, scored against the user's
69
+ // current message. Without this, Clementine has no automatic recall
70
+ // — she'd have to spontaneously call memory_search every turn.
71
+ if (opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
72
+ try {
73
+ const hits = opts.memoryStore.searchContext(opts.userMessage, {
74
+ limit: MAX_MEMORY_HITS,
75
+ });
76
+ if (hits && hits.length > 0) {
77
+ const lines = ['### Possibly relevant from persistent memory'];
78
+ for (const h of hits.slice(0, MAX_MEMORY_HITS)) {
79
+ const label = h.section
80
+ ? h.section
81
+ : (h.source_file ? h.source_file.split('/').pop() ?? h.source_file : 'memory');
82
+ const content = (h.content ?? '').slice(0, MAX_MEMORY_HIT_CHARS).trim();
83
+ if (!content)
84
+ continue;
85
+ lines.push(`- **${label}**: ${content}`);
86
+ sections.retrievedMemory += 1;
87
+ }
88
+ if (sections.retrievedMemory > 0) {
89
+ parts.push(lines.join('\n'));
90
+ }
91
+ }
92
+ }
93
+ catch (err) {
94
+ // Never block on memory failure — log and continue.
95
+ logger.debug({ err, sessionKey: opts.sessionKey }, 'turn-context: searchContext failed (non-fatal)');
96
+ }
97
+ }
98
+ // ── 2. Recent background task headlines ───────────────────────────
99
+ // Last 24h of terminal-state bg tasks. So when the owner asks "what
100
+ // happened with that job?" she knows without re-asking.
101
+ if (opts.listBackgroundTasks) {
102
+ try {
103
+ const TERMINAL = ['done', 'failed', 'interrupted', 'aborted'];
104
+ const recent = [];
105
+ for (const status of TERMINAL) {
106
+ const tasks = opts.listBackgroundTasks({ status });
107
+ for (const task of tasks) {
108
+ const stamp = task.completedAt ?? task.interruptedAt ?? task.startedAt ?? task.createdAt;
109
+ if (!stamp)
110
+ continue;
111
+ if (nowMs - Date.parse(stamp) > RECENT_BG_WINDOW_MS)
112
+ continue;
113
+ recent.push(task);
114
+ }
115
+ }
116
+ // Newest first, capped.
117
+ recent.sort((a, b) => {
118
+ const aStamp = a.completedAt ?? a.startedAt ?? a.createdAt ?? '';
119
+ const bStamp = b.completedAt ?? b.startedAt ?? b.createdAt ?? '';
120
+ return bStamp.localeCompare(aStamp);
121
+ });
122
+ if (recent.length > 0) {
123
+ const lines = ['### Recently completed background work (last 24h)'];
124
+ for (const task of recent.slice(0, MAX_BG_TASKS)) {
125
+ const promptPreview = (task.prompt ?? '').slice(0, 80).replace(/\s+/g, ' ').trim();
126
+ const tail = task.status === 'done'
127
+ ? (task.result ?? task.deliverableNote ?? 'done').slice(0, 100).replace(/\s+/g, ' ').trim()
128
+ : (task.error ?? task.status).slice(0, 100).replace(/\s+/g, ' ').trim();
129
+ const line = `- **${task.status}**: ${promptPreview} → ${tail}`;
130
+ lines.push(line.slice(0, MAX_BG_TASK_LINE_CHARS));
131
+ sections.recentBgTasks += 1;
132
+ }
133
+ if (sections.recentBgTasks > 0) {
134
+ parts.push(lines.join('\n'));
135
+ }
136
+ }
137
+ }
138
+ catch (err) {
139
+ logger.debug({ err, sessionKey: opts.sessionKey }, 'turn-context: listBackgroundTasks failed (non-fatal)');
140
+ }
141
+ }
142
+ // ── 3. Identity framing ───────────────────────────────────────────
143
+ // "Who is the user, where are they reaching you, which agent are
144
+ // you running as." Anchors the model's voice + addressing.
145
+ const identityLine = buildIdentityLine(opts);
146
+ if (identityLine) {
147
+ parts.push(`### Right now\n${identityLine}`);
148
+ sections.identityFrame = true;
149
+ }
150
+ // ── 4. Live state ─────────────────────────────────────────────────
151
+ // Current date/time so the model never says "I don't know what
152
+ // today is." Cheap, high-signal.
153
+ const liveLine = `Current time: ${nowDate.toISOString()} (UTC)`;
154
+ if (sections.identityFrame) {
155
+ // Fold into the same "Right now" section to avoid an extra header.
156
+ parts[parts.length - 1] = `${parts[parts.length - 1]}\n${liveLine}`;
157
+ }
158
+ else {
159
+ parts.push(`### Right now\n${liveLine}`);
160
+ }
161
+ sections.liveState = true;
162
+ // ── 5. Extension points for deeper learning subsystems ───────────
163
+ // These are intentionally empty today. The architecture is set up
164
+ // so adding a new subsystem = adding a new builder function +
165
+ // calling it here. Each follow-up ship can wire one at a time
166
+ // without re-touching the rest of the module.
167
+ //
168
+ // TODO(1.18.185+): wire these in:
169
+ // - decision-reflection: latest formatReflectionSummary() if <24h old
170
+ // - skill-quality: skills flagged 'underperforming' or 'stale'
171
+ // - insight-engine: most recent generated insights not yet ack'd
172
+ // - seed-user-model: latest persisted snapshot of the owner profile
173
+ // - goal-evaluator: active goals and last 3 goal-check results
174
+ //
175
+ // For each, the pattern is: read fast, cap output, log non-fatally on error.
176
+ if (parts.length === 0) {
177
+ return { block: '', sections, totalChars: 0 };
178
+ }
179
+ const body = parts.join('\n\n');
180
+ // Hard cap on the whole block to protect cache health.
181
+ const truncated = body.length > MAX_BLOCK_CHARS
182
+ ? body.slice(0, MAX_BLOCK_CHARS - 3) + '...'
183
+ : body;
184
+ // Mirror the existing securityAnnotation envelope shape so the chat
185
+ // path can concatenate cleanly.
186
+ const block = `[Context — read this for continuity, then respond to the user message below]\n${truncated}\n[/Context]\n\n`;
187
+ return {
188
+ block,
189
+ sections,
190
+ totalChars: block.length,
191
+ };
192
+ }
193
+ // ── Helpers ───────────────────────────────────────────────────────────
194
+ function buildIdentityLine(opts) {
195
+ const parts = [];
196
+ if (opts.ownerName) {
197
+ parts.push(`You're talking to ${opts.ownerName}`);
198
+ }
199
+ if (opts.channel) {
200
+ parts.push(`via ${opts.channel}`);
201
+ }
202
+ if (opts.profileName) {
203
+ parts.push(`as ${opts.profileName}`);
204
+ }
205
+ else if (parts.length > 0) {
206
+ parts.push('as Clementine');
207
+ }
208
+ return parts.length > 0 ? parts.join(' ') + '.' : '';
209
+ }
210
+ //# sourceMappingURL=clementine-turn-context.js.map
@@ -81,8 +81,43 @@ export function buildChatSystemAppend(opts = {}) {
81
81
  if (opts.profileAppend?.trim()) {
82
82
  blocks.push(opts.profileAppend);
83
83
  }
84
+ // 5. Behavioral posture (1.18.184) — re-anchored from the legacy
85
+ // assistant.ts:buildSystemPrompt path which the modern chat no
86
+ // longer goes through. Kept in the cacheable system-prompt append
87
+ // (NOT in the per-turn user-message context) so Anthropic's
88
+ // prompt cache holds these directives across turns. They are
89
+ // short, stable, and describe HOW Clementine should behave —
90
+ // not WHAT is true right now (that's the volatile turn-context
91
+ // block; see clementine-turn-context.ts).
92
+ blocks.push(BEHAVIORAL_POSTURE);
84
93
  return blocks.join('\n\n');
85
94
  }
95
+ /**
96
+ * Stable behavioral directives appended to every chat-path system prompt.
97
+ *
98
+ * Trust posture: Clementine runs on the owner's machine; she is the
99
+ * owner's agent, not a faceless assistant. She acts decisively because
100
+ * the trust relationship has already been established by virtue of the
101
+ * owner running her on their own hardware. Permission-prompt friction
102
+ * is a violation of the trust model.
103
+ *
104
+ * Recall posture (re-anchored from 1.18.181, originally at
105
+ * assistant.ts:1382 in the legacy buildSystemPrompt path): durable
106
+ * memory across all sessions / channels / bg tasks lives in SQLite with
107
+ * dense embeddings. The model has `memory_search` and `transcript_search`
108
+ * tools available. "I have no record" without first searching is a
109
+ * memory failure, not an honest answer.
110
+ *
111
+ * Kept short (<600 chars total) so it doesn't crowd the cacheable
112
+ * prefix; each directive is one sentence of guidance, not a manual.
113
+ */
114
+ const BEHAVIORAL_POSTURE = `## How you operate
115
+
116
+ **Trust posture.** You are running on your owner's trusted local machine as their Clementine. The owner has already authenticated every integration you have access to; you act as them, with their judgment. Don't ask permission for things you have tools to do — just do them. Save permission-asking for genuinely consequential or ambiguous actions (sending the email vs drafting it, deleting vs archiving). Friction-by-default is a violation of the relationship.
117
+
118
+ **Recall posture.** Your durable memory across every session, every channel, every background task, and every workflow lives in SQLite with dense embeddings. When the owner references past work you don't have in immediate context — a URL, a deployment, a person, a project, a file you created, a job you ran — call \`memory_search\` (or \`transcript_search\` for chat-history specifics) BEFORE asking the owner to provide it and BEFORE replying that you have no record. "I don't see any record of that" without having searched is a memory failure, not an honest answer.
119
+
120
+ **Persistence posture.** When the owner gives you a multi-step job in chat, run it to completion. If you hit a real constraint (budget, cap, missing input, validation needed), say so explicitly — never trail off silently. The owner can always stop you via cancel or by typing \`stop\`; that's their lever. Yours is to keep going until the work is done.`;
86
121
  /**
87
122
  * Read the long-term memory block for an autonomous run (cron, team-task).
88
123
  * Returns the agent-specific MEMORY.md when a hired agent is active, the