clementine-agent 1.18.190 → 1.18.191

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.
@@ -85,6 +85,13 @@ export interface BuildTurnContextOptions {
85
85
  * source/output file inventory, and deploy.json summary (if any).
86
86
  * Set by the router's resolver before the chat call. */
87
87
  activeProject?: ProjectMeta | null;
88
+ /** 1.18.191 — message shape from intent-classifier. When 'simple',
89
+ * we skip the heavyweight sections (memory recall, bg-task headlines,
90
+ * dispute gate) and only emit identity + live state. Saves ~3-4KB
91
+ * of injected tokens per routine chat turn. 'multi-step' and
92
+ * 'unknown' keep the full block. Defaults to 'unknown' = full block
93
+ * for back-compat. */
94
+ messageShape?: 'simple' | 'multi-step' | 'unknown';
88
95
  }
89
96
  export interface BuildTurnContextResult {
90
97
  /** The full ready-to-prepend context block, INCLUDING outer
@@ -68,12 +68,26 @@ export function buildClementineTurnContext(opts) {
68
68
  const parts = [];
69
69
  const nowMs = (opts.now ?? Date.now)();
70
70
  const nowDate = new Date(nowMs);
71
+ // 1.18.191 — intent-aware section gating. For 'simple' messages
72
+ // (one-line questions, casual chat, brief asks), the heavy sections
73
+ // (memory recall, bg-task headlines, dispute gate) are skipped so
74
+ // every routine turn doesn't pay 3-4KB of injected tokens for
75
+ // context the model doesn't need. Identity + live state still
76
+ // render — they're cheap and always useful. Active project still
77
+ // renders when the message references it. 'multi-step' and
78
+ // 'unknown' keep the full block.
79
+ const messageShape = opts.messageShape ?? 'unknown';
80
+ const isSimpleMessage = messageShape === 'simple';
71
81
  // 1.18.187 — detect dispute pattern (Part E). When the owner is
72
82
  // reporting a failure of prior work, we want to suppress "past
73
83
  // success" recall items (they bias the model toward defending its
74
84
  // memory instead of verifying reality) and add a verification
75
85
  // directive at the top of the block.
76
- const disputeDetected = detectDisputePattern(opts.userMessage);
86
+ // 1.18.191 also suppressed for 'simple' shape since dispute
87
+ // patterns rarely co-occur with simple messages, and the gate
88
+ // produces a multi-paragraph directive we don't want to inject
89
+ // when the message is just "what time is it".
90
+ const disputeDetected = !isSimpleMessage && detectDisputePattern(opts.userMessage);
77
91
  sections.disputeDetected = disputeDetected;
78
92
  if (disputeDetected) {
79
93
  parts.push('### Dispute mode — verification posture\n' +
@@ -103,7 +117,14 @@ export function buildClementineTurnContext(opts) {
103
117
  // hits from the SQLite memory store, scored against the user's
104
118
  // current message. Without this, Clementine has no automatic recall
105
119
  // — she'd have to spontaneously call memory_search every turn.
106
- if (opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
120
+ //
121
+ // 1.18.191 — skipped for 'simple' messages. "What time is it" doesn't
122
+ // need 6 memory hits injected; she has the recall tools and can call
123
+ // them on demand when the message warrants it. Saves ~2KB/turn on
124
+ // routine chat. The owner's own posture directive
125
+ // (BEHAVIORAL_POSTURE in run-agent-context.ts) tells the model to
126
+ // call memory_search proactively when relevant — that still works.
127
+ if (!isSimpleMessage && opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
107
128
  try {
108
129
  const hits = opts.memoryStore.searchContext(opts.userMessage, {
109
130
  limit: MAX_MEMORY_HITS,
@@ -139,7 +160,12 @@ export function buildClementineTurnContext(opts) {
139
160
  // that bias the model toward "but my memory says it succeeded."
140
161
  // Failed/aborted/interrupted tasks STAY because they're useful
141
162
  // signal for the verification posture.
142
- if (opts.listBackgroundTasks) {
163
+ //
164
+ // 1.18.191 — skipped for 'simple' messages. Routine chat doesn't
165
+ // need to know what bg tasks landed in the last 24h. If the user
166
+ // asks "what happened with X" — that's a multi-step / unknown
167
+ // shape, this section will fire normally.
168
+ if (!isSimpleMessage && opts.listBackgroundTasks) {
143
169
  try {
144
170
  const TERMINAL = disputeDetected
145
171
  ? ['failed', 'interrupted', 'aborted']
@@ -31,6 +31,51 @@ export declare function classifyIntent(text: string, recentExchanges?: Array<{
31
31
  * Injected into the system prompt to steer the agent's response style.
32
32
  */
33
33
  export declare function getStrategyGuidance(strategy: ResponseStrategy): string;
34
+ export type MessageShape =
35
+ /** Single ask, single response. "what time is it", "remind me to call X". */
36
+ 'simple'
37
+ /** Multiple distinct actions across phases. "send 25 emails after
38
+ * scraping data from Salesforce and SEO sources, then summarize". */
39
+ | 'multi-step'
40
+ /** Ambiguous — falls through to today's full chat path (safe default). */
41
+ | 'unknown';
42
+ export interface MessageShapeResult {
43
+ shape: MessageShape;
44
+ score: number;
45
+ reasons: string[];
46
+ }
47
+ /**
48
+ * Classify a chat message's structural shape (simple / multi-step).
49
+ *
50
+ * Scoring (sum of triggered signals):
51
+ * - 2+ shape-action verbs: +1
52
+ * - 3+ shape-action verbs: +2 cumulatively
53
+ * - each sequence marker ("and then", "after that"): +1 (up to +2)
54
+ * - batch marker ("for each", "25 emails"): +1
55
+ * - numbered list: +2
56
+ * - 2+ distinct integration domains in same message: +1
57
+ * - length > 200 chars: +1
58
+ * - length > 500 chars: +2 cumulatively
59
+ *
60
+ * Decision:
61
+ * - score >= threshold (default 3) → 'multi-step'
62
+ * - score === 0 AND <= 1 action verb AND length <= 200 → 'simple'
63
+ * - otherwise → 'unknown' (today's chat path, no change)
64
+ */
65
+ export declare function classifyMessageShape(text: string, opts?: {
66
+ threshold?: number;
67
+ }): MessageShapeResult;
68
+ /**
69
+ * Detect whether the user's message is approving / revising / canceling
70
+ * a pending plan. Used by the chat-side plan-mode state machine when
71
+ * `sess.planAwaitingApproval` is set.
72
+ *
73
+ * Conservative: only short, clearly-affirmative messages qualify as
74
+ * approval. "yes but also do X" is NOT approval — it's a revision
75
+ * request and the state machine should re-plan with the feedback.
76
+ */
77
+ export type PlanApprovalSignal = 'approve' | 'revise' | 'cancel' | 'other';
78
+ export declare function detectPlanApproval(message: string): PlanApprovalSignal;
34
79
  /**
35
80
  * Generate a follow-up suggestion prompt suffix based on completed work.
36
81
  *
@@ -188,6 +188,150 @@ No tool calls needed. Just be conversational.
188
188
  If there's relevant context from recent work or pending items, briefly mention it.`;
189
189
  }
190
190
  }
191
+ /** Action verbs that strongly suggest "do work" rather than "answer". */
192
+ const SHAPE_ACTION_VERBS = [
193
+ 'send', 'create', 'build', 'generate', 'write', 'draft', 'compose',
194
+ 'publish', 'deploy', 'upload', 'post', 'push',
195
+ 'scrape', 'fetch', 'pull', 'extract', 'gather', 'collect',
196
+ 'convert', 'merge', 'combine', 'transform', 'consolidate', 'aggregate',
197
+ 'schedule', 'queue', 'run', 'execute', 'process',
198
+ 'email', 'message', 'notify', 'alert', 'reply', 'forward',
199
+ 'import', 'export', 'sync', 'backup',
200
+ ];
201
+ const SHAPE_SEQUENCE_MARKERS = [
202
+ /\band\s+then\b/i,
203
+ /\b(?:after|once|when)\s+(?:that|you|done|finished|complete)/i,
204
+ /\b(?:then|next|finally|last)\s*[,]?\s+\w+/i,
205
+ /\bfollowed\s+by\b/i,
206
+ /\b(?:step|phase)\s+\d+/i,
207
+ ];
208
+ const SHAPE_BATCH_MARKERS = [
209
+ /\b(?:for|on|to)\s+each\b/i,
210
+ /\b\d{2,}\s+\w+/, // "25 emails", "100 records"
211
+ /\beach\s+of\s+(?:them|the)\b/i,
212
+ /\b(?:all|every)\s+(?:of\s+)?(?:them|the\s+\w+)/i,
213
+ /\b(?:bulk|batch|mass)\b/i,
214
+ ];
215
+ const SHAPE_NUMBERED_LIST = /\n\s*\d+[.)]\s+\w+/;
216
+ const SHAPE_DOMAIN_MARKERS = [
217
+ /\bsalesforce\b/i, /\bgmail\b/i, /\boutlook\b/i, /\bslack\b/i,
218
+ /\bdiscord\b/i, /\bnetlify\b/i, /\bvercel\b/i, /\bgithub\b/i,
219
+ /\bsupabase\b/i, /\bairtable\b/i, /\bhubspot\b/i, /\bnotion\b/i,
220
+ /\blinkedin\b/i, /\bcalendar\b/i, /\bdrive\b/i, /\bsheets\b/i,
221
+ ];
222
+ /**
223
+ * Classify a chat message's structural shape (simple / multi-step).
224
+ *
225
+ * Scoring (sum of triggered signals):
226
+ * - 2+ shape-action verbs: +1
227
+ * - 3+ shape-action verbs: +2 cumulatively
228
+ * - each sequence marker ("and then", "after that"): +1 (up to +2)
229
+ * - batch marker ("for each", "25 emails"): +1
230
+ * - numbered list: +2
231
+ * - 2+ distinct integration domains in same message: +1
232
+ * - length > 200 chars: +1
233
+ * - length > 500 chars: +2 cumulatively
234
+ *
235
+ * Decision:
236
+ * - score >= threshold (default 3) → 'multi-step'
237
+ * - score === 0 AND <= 1 action verb AND length <= 200 → 'simple'
238
+ * - otherwise → 'unknown' (today's chat path, no change)
239
+ */
240
+ export function classifyMessageShape(text, opts = {}) {
241
+ const reasons = [];
242
+ let score = 0;
243
+ if (!text || !text.trim()) {
244
+ return { shape: 'simple', score: 0, reasons: ['empty'] };
245
+ }
246
+ const trimmed = text.trim();
247
+ const lower = trimmed.toLowerCase();
248
+ const words = new Set(lower.replace(/[^\w\s-]/g, ' ').split(/\s+/).filter(Boolean));
249
+ // Action verbs
250
+ const matchedVerbs = [];
251
+ for (const verb of SHAPE_ACTION_VERBS) {
252
+ if (words.has(verb))
253
+ matchedVerbs.push(verb);
254
+ }
255
+ if (matchedVerbs.length >= 2) {
256
+ score += 1;
257
+ reasons.push(`2+ action verbs (${matchedVerbs.slice(0, 4).join(', ')})`);
258
+ }
259
+ if (matchedVerbs.length >= 3) {
260
+ score += 1;
261
+ reasons.push('3+ action verbs');
262
+ }
263
+ // Sequence markers
264
+ for (const rx of SHAPE_SEQUENCE_MARKERS) {
265
+ const matches = lower.match(new RegExp(rx.source, 'gi'));
266
+ if (matches && matches.length > 0) {
267
+ score += Math.min(matches.length, 2);
268
+ reasons.push(`sequence marker: ${matches[0]}`);
269
+ break;
270
+ }
271
+ }
272
+ // Batch markers
273
+ for (const rx of SHAPE_BATCH_MARKERS) {
274
+ if (rx.test(trimmed)) {
275
+ score += 1;
276
+ reasons.push('batch marker (for each / N items / bulk)');
277
+ break;
278
+ }
279
+ }
280
+ // Numbered list
281
+ if (SHAPE_NUMBERED_LIST.test(trimmed)) {
282
+ score += 2;
283
+ reasons.push('numbered list');
284
+ }
285
+ // Cross-domain
286
+ let domainCount = 0;
287
+ for (const rx of SHAPE_DOMAIN_MARKERS) {
288
+ if (rx.test(trimmed))
289
+ domainCount += 1;
290
+ }
291
+ if (domainCount >= 2) {
292
+ score += 1;
293
+ reasons.push(`${domainCount} integration domains mentioned`);
294
+ }
295
+ // Length
296
+ if (trimmed.length > 200) {
297
+ score += 1;
298
+ reasons.push(`length > 200 (${trimmed.length})`);
299
+ }
300
+ if (trimmed.length > 500) {
301
+ score += 1;
302
+ reasons.push(`length > 500`);
303
+ }
304
+ // Decision
305
+ const threshold = opts.threshold ?? 3;
306
+ let shape;
307
+ if (score >= threshold) {
308
+ shape = 'multi-step';
309
+ }
310
+ else if (score === 0 && matchedVerbs.length <= 1 && trimmed.length <= 200) {
311
+ shape = 'simple';
312
+ }
313
+ else {
314
+ shape = 'unknown';
315
+ }
316
+ return { shape, score, reasons };
317
+ }
318
+ const APPROVE_RE = /^(?:yes|y|yep|yeah|yup|sure|ok|okay|approve|approved|go|go ahead|run it|do it|sounds good|lgtm|ship it|👍|✅)[\s.!]*$/i;
319
+ const CANCEL_RE = /^(?:cancel|stop|nvm|nevermind|never\s*mind|forget it|don['']?t|abort|kill it)\b/i;
320
+ const REVISE_RE = /\b(?:but|except|instead|change|modify|add(?:\s+also)?|remove|skip|swap|wait|actually|hold on)\b/i;
321
+ export function detectPlanApproval(message) {
322
+ if (!message)
323
+ return 'other';
324
+ const text = message.trim();
325
+ if (!text)
326
+ return 'other';
327
+ if (CANCEL_RE.test(text))
328
+ return 'cancel';
329
+ if (text.length <= 30 && APPROVE_RE.test(text))
330
+ return 'approve';
331
+ if (text.length > 30 || REVISE_RE.test(text))
332
+ return 'revise';
333
+ return 'other';
334
+ }
191
335
  /**
192
336
  * Generate a follow-up suggestion prompt suffix based on completed work.
193
337
  *
@@ -88,6 +88,31 @@ export declare class Gateway {
88
88
  private queueBackgroundOffer;
89
89
  private formatBackgroundQueuedResponse;
90
90
  private queueBackgroundTaskAfterContextOverflow;
91
+ /**
92
+ * 1.18.191 — chat-side plan mode state machine.
93
+ *
94
+ * Two paths it handles:
95
+ *
96
+ * A. Approval-pending path. If sess.planAwaitingApproval is set,
97
+ * the user's message NOW is either approval / revision / cancel.
98
+ * Approval → dispatch the chain. Revision → re-plan with feedback.
99
+ * Cancel → clear pending state.
100
+ *
101
+ * B. Multi-step entry path. shape='multi-step' AND no pending plan:
102
+ * run planRequest synchronously, post the plan to chat asking
103
+ * for approval, set pending state. The owner's NEXT message
104
+ * advances via path A.
105
+ *
106
+ * Returns `{ handled: true, response }` when plan mode owns this turn
107
+ * (caller should return the response without running normal chat).
108
+ * Returns `{ handled: false }` to fall through to normal chat.
109
+ *
110
+ * Defensive on every external call — failures degrade to normal chat
111
+ * rather than blocking the owner's conversation.
112
+ */
113
+ private _maybeHandlePlanMode;
114
+ /** Format a plan for owner approval in chat. */
115
+ private _formatPlanForApproval;
91
116
  acceptBackgroundOffer(sessionKey: string, id: string): {
92
117
  ok: boolean;
93
118
  response: string;
@@ -469,6 +469,163 @@ export class Gateway {
469
469
  ].join('\n'),
470
470
  };
471
471
  }
472
+ /**
473
+ * 1.18.191 — chat-side plan mode state machine.
474
+ *
475
+ * Two paths it handles:
476
+ *
477
+ * A. Approval-pending path. If sess.planAwaitingApproval is set,
478
+ * the user's message NOW is either approval / revision / cancel.
479
+ * Approval → dispatch the chain. Revision → re-plan with feedback.
480
+ * Cancel → clear pending state.
481
+ *
482
+ * B. Multi-step entry path. shape='multi-step' AND no pending plan:
483
+ * run planRequest synchronously, post the plan to chat asking
484
+ * for approval, set pending state. The owner's NEXT message
485
+ * advances via path A.
486
+ *
487
+ * Returns `{ handled: true, response }` when plan mode owns this turn
488
+ * (caller should return the response without running normal chat).
489
+ * Returns `{ handled: false }` to fall through to normal chat.
490
+ *
491
+ * Defensive on every external call — failures degrade to normal chat
492
+ * rather than blocking the owner's conversation.
493
+ */
494
+ async _maybeHandlePlanMode(opts) {
495
+ const sess = this.sessions.get(opts.sessionKey);
496
+ const { detectPlanApproval } = await import('../agent/intent-classifier.js');
497
+ const { planRequest, savePlan, loadPlan } = await import('../agent/bg-planner.js');
498
+ const { dispatchChain } = await import('../agent/bg-orchestrator.js');
499
+ // ── Path A: approval-pending ────────────────────────────────────
500
+ if (sess?.planAwaitingApproval) {
501
+ const pending = sess.planAwaitingApproval;
502
+ const signal = detectPlanApproval(opts.userMessage);
503
+ logger.info({
504
+ sessionKey: opts.sessionKey,
505
+ planId: pending.planId,
506
+ chainId: pending.chainId,
507
+ signal,
508
+ }, 'Plan mode: approval signal received');
509
+ const plan = loadPlan(pending.planId, opts.activeProject?.path);
510
+ if (!plan) {
511
+ // Plan disappeared from disk — clear the pending state and
512
+ // let the message fall through to normal chat.
513
+ delete sess.planAwaitingApproval;
514
+ logger.warn({ planId: pending.planId }, 'Plan mode: pending plan not found on disk — clearing');
515
+ return { handled: false };
516
+ }
517
+ if (signal === 'approve') {
518
+ try {
519
+ const firstTask = dispatchChain(plan);
520
+ delete sess.planAwaitingApproval;
521
+ const response = [
522
+ `**Plan approved — starting step 1: ${plan.steps[0]?.title ?? '(first step)'}**`,
523
+ '',
524
+ `Background task **${firstTask.id}** is now running.`,
525
+ 'I\'ll post step-by-step updates as each step completes.',
526
+ ].join('\n');
527
+ return { handled: true, response };
528
+ }
529
+ catch (err) {
530
+ logger.warn({ err, planId: plan.id }, 'Plan mode: dispatchChain failed on approval');
531
+ return { handled: true, response: `Couldn't start the chain: ${String(err).slice(0, 200)}. Tell me to retry or try a different approach.` };
532
+ }
533
+ }
534
+ if (signal === 'cancel') {
535
+ delete sess.planAwaitingApproval;
536
+ return { handled: true, response: 'Cancelled the plan. Tell me what you\'d like to do instead.' };
537
+ }
538
+ if (signal === 'revise') {
539
+ // Re-plan with the user's revision as additional context.
540
+ try {
541
+ const revisedRequest = `${plan.userRequest}\n\n[Revision from owner: ${opts.userMessage}]`;
542
+ const newPlan = await planRequest({
543
+ userRequest: revisedRequest,
544
+ originatingSessionKey: opts.sessionKey,
545
+ ...(opts.activeProject ? { project: opts.activeProject } : {}),
546
+ });
547
+ savePlan(newPlan, newPlan.projectPath);
548
+ sess.planAwaitingApproval = {
549
+ planId: newPlan.id,
550
+ chainId: newPlan.chainId,
551
+ proposedAt: Date.now(),
552
+ };
553
+ return {
554
+ handled: true,
555
+ response: this._formatPlanForApproval(newPlan, /* revised */ true),
556
+ };
557
+ }
558
+ catch (err) {
559
+ logger.warn({ err }, 'Plan mode: revision planRequest failed');
560
+ return { handled: true, response: `Couldn't revise the plan: ${String(err).slice(0, 200)}. Want me to start fresh or try something else?` };
561
+ }
562
+ }
563
+ // signal='other' — let it fall through to normal chat, but keep
564
+ // pending state. The model will see the user message normally.
565
+ return { handled: false };
566
+ }
567
+ // ── Path B: multi-step entry ────────────────────────────────────
568
+ if (opts.shape === 'multi-step' && sess) {
569
+ try {
570
+ // Stream a "thinking..." update so the user knows planning is
571
+ // happening rather than seeing 30s of silence.
572
+ if (opts.onText) {
573
+ try {
574
+ opts.onText('🤔 Planning the steps...');
575
+ }
576
+ catch { /* non-fatal */ }
577
+ }
578
+ const plan = await planRequest({
579
+ userRequest: opts.userMessage,
580
+ originatingSessionKey: opts.sessionKey,
581
+ ...(opts.activeProject ? { project: opts.activeProject } : {}),
582
+ });
583
+ savePlan(plan, plan.projectPath);
584
+ sess.planAwaitingApproval = {
585
+ planId: plan.id,
586
+ chainId: plan.chainId,
587
+ proposedAt: Date.now(),
588
+ };
589
+ return {
590
+ handled: true,
591
+ response: this._formatPlanForApproval(plan, /* revised */ false),
592
+ };
593
+ }
594
+ catch (err) {
595
+ logger.warn({ err, sessionKey: opts.sessionKey }, 'Plan mode: planRequest failed at entry');
596
+ // Fall through to normal chat. Better than blocking the owner.
597
+ return { handled: false };
598
+ }
599
+ }
600
+ // Not a plan-mode case — fall through to normal chat.
601
+ return { handled: false };
602
+ }
603
+ /** Format a plan for owner approval in chat. */
604
+ _formatPlanForApproval(plan, revised) {
605
+ const lines = [];
606
+ lines.push(revised
607
+ ? `**Revised plan (${plan.steps.length} steps)**`
608
+ : `**Here's how I'd do this (${plan.steps.length} steps)**`);
609
+ lines.push('');
610
+ for (const step of plan.steps) {
611
+ lines.push(`${step.index + 1}. **${step.title}**`);
612
+ if (step.scope)
613
+ lines.push(` ${step.scope}`);
614
+ if (step.deliverable)
615
+ lines.push(` → ${step.deliverable}`);
616
+ }
617
+ if (plan.notes) {
618
+ lines.push('');
619
+ lines.push(`_Notes_: ${plan.notes}`);
620
+ }
621
+ if (typeof plan.estimatedCostUsd === 'number') {
622
+ lines.push('');
623
+ lines.push(`Estimated cost: ~$${plan.estimatedCostUsd.toFixed(2)}`);
624
+ }
625
+ lines.push('');
626
+ lines.push('Say **yes / approve / go** to start, **cancel** to skip, or describe a revision (e.g., "swap step 3" or "add a verification step").');
627
+ return lines.join('\n');
628
+ }
472
629
  // Offer-message formatter was removed in the Saturday-feel restoration —
473
630
  // the chat path no longer asks "want me to run this in the background?".
474
631
  // Auto-queue on explicit user intent is silent; everything else just runs.
@@ -2183,6 +2340,56 @@ export class Gateway {
2183
2340
  const { buildClementineTurnContext } = await import('../agent/clementine-turn-context.js');
2184
2341
  const { listBackgroundTasks } = await import('../agent/background-tasks.js');
2185
2342
  const { resolveProjectFromMessage } = await import('../agent/project-resolver.js');
2343
+ const { classifyMessageShape } = await import('../agent/intent-classifier.js');
2344
+ // 1.18.191 — classify message shape early. Simple messages
2345
+ // get a lean turn-context block (no memory recall, no bg
2346
+ // headlines, no dispute gate); multi-step messages keep
2347
+ // the full block AND may trigger plan mode (below).
2348
+ // Builder sessions skip — they have their own routing.
2349
+ const shapeResult = !isBuilderSession
2350
+ ? classifyMessageShape(originalText)
2351
+ : { shape: 'simple', score: 0, reasons: ['builder-session'] };
2352
+ logger.debug({
2353
+ sessionKey: effectiveSessionKey,
2354
+ shape: shapeResult.shape,
2355
+ score: shapeResult.score,
2356
+ reasons: shapeResult.reasons,
2357
+ }, 'Message shape classified');
2358
+ // 1.18.191 — plan mode state machine.
2359
+ //
2360
+ // Two entry points for plan mode:
2361
+ //
2362
+ // A. Approval-pending path. If the previous turn proposed a
2363
+ // plan, the user's message NOW is either approval,
2364
+ // revision, or cancel. Handle and return without
2365
+ // running normal chat.
2366
+ //
2367
+ // B. Multi-step entry path. If shape='multi-step' AND no
2368
+ // pending plan, generate a plan, post it, set pending
2369
+ // state, return. The owner's NEXT message advances via
2370
+ // path A.
2371
+ //
2372
+ // Builder sessions skip both paths.
2373
+ if (!isBuilderSession) {
2374
+ const planMode = await this._maybeHandlePlanMode({
2375
+ sessionKey: effectiveSessionKey,
2376
+ userMessage: originalText,
2377
+ shape: shapeResult.shape,
2378
+ activeProject: this.getSessionProject(effectiveSessionKey) ?? null,
2379
+ onText: wrappedOnText,
2380
+ });
2381
+ if (planMode.handled) {
2382
+ clearTimeout(chatTimer);
2383
+ if (hardWallTimer)
2384
+ clearTimeout(hardWallTimer);
2385
+ {
2386
+ const cs = this.sessions.get(sessionKey);
2387
+ if (cs)
2388
+ delete cs.abortController;
2389
+ }
2390
+ return planMode.response;
2391
+ }
2392
+ }
2186
2393
  // 1.18.187 — auto-resolve project from the user's message.
2187
2394
  // If a linked project's name/keyword matches with high
2188
2395
  // confidence, set sess.project for this turn so cwd shifts,
@@ -2294,6 +2501,10 @@ export class Gateway {
2294
2501
  // 1.18.187 — pass active project so the turn-context block
2295
2502
  // can include path / STATUS.md / inventory / deploy config.
2296
2503
  activeProject: this.getSessionProject(effectiveSessionKey) ?? null,
2504
+ // 1.18.191 — pass message shape so simple messages get the
2505
+ // lean turn-context (skip memory recall, bg-task headlines,
2506
+ // dispute gate). Token-optimization for routine chat.
2507
+ messageShape: shapeResult.shape,
2297
2508
  });
2298
2509
  clementineContextBlock = turnCtx.block;
2299
2510
  logger.debug({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.190",
3
+ "version": "1.18.191",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",