@memnexus-ai/cli 1.7.222 → 1.7.224

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.
@@ -77,6 +77,7 @@ Object.defineProperty(exports, "isNameConflictError", { enumerable: true, get: f
77
77
  Object.defineProperty(exports, "getRetryAfterMs", { enumerable: true, get: function () { return retry_1.getRetryAfterMs; } });
78
78
  Object.defineProperty(exports, "sleep", { enumerable: true, get: function () { return retry_1.sleep; } });
79
79
  const formatters_1 = require("../lib/formatters");
80
+ const renderable_retrieval_1 = require("../lib/renderable-retrieval");
80
81
  const js_yaml_1 = __importDefault(require("js-yaml"));
81
82
  const spinner_1 = require("../lib/spinner");
82
83
  const date_utils_1 = require("../lib/date-utils");
@@ -2471,26 +2472,40 @@ function registerMemoriesCommands(program) {
2471
2472
  spinner?.stop();
2472
2473
  throw (0, errors_1.unusableResponseError)('Memory search', responseShape(searchData));
2473
2474
  }
2474
- // Normalize: API may return flat objects or {memory, score} wrappers.
2475
- // Ensure consistent {memory, score, matchedVia} shape for display code.
2476
- if (searchData?.data) {
2477
- searchData.data = searchData.data.map((r) => r.memory ? r : { memory: r, score: r.score, matchedVia: r.matchedVia });
2475
+ // Every render path below takes the renderable, never the raw body: the
2476
+ // factory normalizes {memory, score, matchedVia} AND maps meta.degraded
2477
+ // once, so no format can render a degraded result as healthy (#5124).
2478
+ const search = renderable_retrieval_1.RenderableSearchResult.fromApiResponse(searchData);
2479
+ // The headline is the first thing a caller reads and was unconditional:
2480
+ // "Found N results" over an incomplete gather reads as a full answer.
2481
+ if (search.health.degraded) {
2482
+ spinner?.warn(`Found ${search.results.length} results${(0, renderable_retrieval_1.degradationHeadlineSuffix)(search.health)}`);
2483
+ }
2484
+ else {
2485
+ spinner?.succeed(`Found ${search.results.length} results`);
2486
+ }
2487
+ // Degradation marker — stderr, before any format branch, so it reaches
2488
+ // the caller in every output mode including --id-only and --format json
2489
+ // where stdout must stay machine-parseable.
2490
+ const degradedLine = (0, renderable_retrieval_1.degradationWarningLine)(search.health);
2491
+ if (degradedLine) {
2492
+ console.error(chalk_1.default.yellow(degradedLine));
2478
2493
  }
2479
- spinner?.succeed(`Found ${searchData?.data?.length || 0} results`);
2480
2494
  // Surface extraction failures (#2862). Failed memories are invisible to
2481
2495
  // fact/entity search, so flag them on stderr where they won't pollute
2482
2496
  // piped JSON. Callers in table mode also see an inline indicator below.
2483
- const failedResultCount = (searchData?.data || []).filter((r) => {
2497
+ const failedResultCount = search.results.filter((r) => {
2484
2498
  const m = r?.memory ?? r;
2485
2499
  return m && m.extractionStatus === 'failed';
2486
2500
  }).length;
2487
2501
  if (failedResultCount > 0 && !options.idOnly) {
2488
- const total = searchData?.data?.length || 0;
2502
+ const total = search.results.length;
2489
2503
  console.error(chalk_1.default.yellow(`⚠ ${failedResultCount} of ${total} results have failed extraction (won\u2019t appear in fact/entity searches).`));
2490
2504
  }
2491
- // --id-only mode: output only IDs for piping
2505
+ // --id-only mode: output only IDs for piping. The degradation marker
2506
+ // already went to stderr above, so it survives the pipe.
2492
2507
  if (options.idOnly) {
2493
- const ids = (searchData?.data || [])
2508
+ const ids = search.results
2494
2509
  .map((result) => result.memory?.id)
2495
2510
  .filter((id) => id);
2496
2511
  console.log(ids.join('\n'));
@@ -2498,12 +2513,12 @@ function registerMemoriesCommands(program) {
2498
2513
  }
2499
2514
  // --timeline mode: chronological display with timestamps and effective state
2500
2515
  if (options.timeline) {
2501
- displayTimelineResults(searchData?.data || [], options.query, previewLength);
2516
+ displayTimelineResults(search.results, options.query, previewLength, search.health);
2502
2517
  return;
2503
2518
  }
2504
2519
  // --group-by conversation: group results by conversation ID
2505
2520
  if (options.groupBy === 'conversation') {
2506
- const results = searchData?.data || [];
2521
+ const results = search.results;
2507
2522
  const grouped = groupByConversation(results);
2508
2523
  if (effectiveFormat === 'json' || effectiveFormat === 'yaml') {
2509
2524
  // Convert Map to array for JSON/YAML serialization
@@ -2512,10 +2527,13 @@ function registerMemoriesCommands(program) {
2512
2527
  memoryCount: memories.length,
2513
2528
  memories: memories,
2514
2529
  }));
2530
+ // Array-shaped payload: adding a health object would change the
2531
+ // top-level shape, so the marker for this path is the stderr line
2532
+ // emitted above.
2515
2533
  (0, formatters_1.printOutput)(groupedArray, effectiveFormat);
2516
2534
  }
2517
2535
  else {
2518
- displayGroupedSearch(grouped, options.query);
2536
+ displayGroupedSearch(grouped, options.query, search.health);
2519
2537
  }
2520
2538
  return;
2521
2539
  }
@@ -2523,7 +2541,7 @@ function registerMemoriesCommands(program) {
2523
2541
  if (effectiveFormat === 'llm') {
2524
2542
  // LLM format: clean markdown without ANSI codes
2525
2543
  // Extract nested memory objects for formatting
2526
- const llmData = (searchData?.data || []).map((result) => {
2544
+ const llmData = search.results.map((result) => {
2527
2545
  const m = result.memory;
2528
2546
  return {
2529
2547
  id: m.id,
@@ -2532,15 +2550,19 @@ function registerMemoriesCommands(program) {
2532
2550
  createdAt: m.createdAt,
2533
2551
  };
2534
2552
  });
2553
+ const llmMarker = (0, renderable_retrieval_1.degradationSummaryLine)(search.health);
2554
+ if (llmMarker) {
2555
+ console.log(llmMarker);
2556
+ }
2535
2557
  (0, formatters_1.printOutput)(llmData, 'llm', undefined, {
2536
- totalCount: searchData?.meta?.total ?? llmData.length,
2558
+ totalCount: search.total,
2537
2559
  previewLength: Math.min(previewLength, 100), // Shorter preview for LLM
2538
2560
  title: 'Search Results',
2539
2561
  });
2540
2562
  }
2541
2563
  else if (effectiveFormat === 'compact') {
2542
2564
  // Compact format: ID + date + first line
2543
- const compactData = (searchData?.data || []).map((result) => {
2565
+ const compactData = search.results.map((result) => {
2544
2566
  const m = result.memory;
2545
2567
  return {
2546
2568
  id: m.id,
@@ -2548,13 +2570,17 @@ function registerMemoriesCommands(program) {
2548
2570
  createdAt: m.createdAt,
2549
2571
  };
2550
2572
  });
2573
+ const compactMarker = (0, renderable_retrieval_1.degradationSummaryLine)(search.health);
2574
+ if (compactMarker) {
2575
+ console.log(chalk_1.default.yellow(compactMarker));
2576
+ }
2551
2577
  (0, formatters_1.printOutput)(compactData, 'compact');
2552
2578
  }
2553
2579
  else if (effectiveFormat === 'table' || !effectiveFormat) {
2554
2580
  // Table format: default display with colors and borders
2555
2581
  // Extract nested memory object from search results
2556
2582
  // API returns: {data: [{memory: {...}, score: 0.66}]}
2557
- const tableData = (searchData?.data || []).map((result) => {
2583
+ const tableData = search.results.map((result) => {
2558
2584
  const m = result.memory; // Extract the nested memory object
2559
2585
  const score = result.score;
2560
2586
  // Smart truncation with term highlighting
@@ -2590,19 +2616,23 @@ function registerMemoriesCommands(program) {
2590
2616
  columnWidths: tableWidths,
2591
2617
  });
2592
2618
  // Show search metadata
2593
- const resultCount = searchData?.meta?.total || searchData?.data?.length || 0;
2594
- const searchMethodUsed = searchData?.meta?.searchMethod || effectiveMethod;
2619
+ const resultCount = search.total;
2620
+ const searchMethodUsed = search.searchMethod || effectiveMethod;
2595
2621
  // Detect graph branches used
2596
2622
  const allBranches = new Set();
2597
- for (const r of searchData?.data || []) {
2623
+ for (const r of search.results) {
2598
2624
  if (r.matchedVia) {
2599
2625
  r.matchedVia.forEach((b) => allBranches.add(b));
2600
2626
  }
2601
2627
  }
2602
2628
  const branchInfo = allBranches.size > 2 ? ` (branches: ${[...allBranches].join(', ')})` : '';
2603
2629
  console.log(chalk_1.default.gray(`\nFound ${resultCount} results using ${searchMethodUsed} search${branchInfo}`));
2630
+ const tableMarker = (0, renderable_retrieval_1.degradationSummaryLine)(search.health);
2631
+ if (tableMarker) {
2632
+ console.log(chalk_1.default.yellow(tableMarker));
2633
+ }
2604
2634
  // Show temporal filters if applied
2605
- const temporal = searchData?.meta?.temporalMetadata;
2635
+ const temporal = search.temporalMetadata;
2606
2636
  if (temporal?.temporalFieldsIncluded) {
2607
2637
  const filters = [];
2608
2638
  if (temporal.eventTimeRange?.from)
@@ -2617,7 +2647,9 @@ function registerMemoriesCommands(program) {
2617
2647
  else {
2618
2648
  // JSON/YAML formats: pass raw or content-only data
2619
2649
  if (options.contentOnly) {
2620
- const contentOnlyData = (searchData?.data || []).map((result) => {
2650
+ // Array-shaped payload by contract ({id, createdAt, topics,
2651
+ // content}); the marker for this path is the stderr line above.
2652
+ const contentOnlyData = search.results.map((result) => {
2621
2653
  const m = result.memory;
2622
2654
  const obj = {
2623
2655
  id: m.id,
@@ -2633,7 +2665,8 @@ function registerMemoriesCommands(program) {
2633
2665
  (0, formatters_1.printOutput)(contentOnlyData, effectiveFormat);
2634
2666
  }
2635
2667
  else {
2636
- (0, formatters_1.printOutput)(searchData, effectiveFormat);
2668
+ // Object-shaped payload: carries the health object when degraded.
2669
+ (0, formatters_1.printOutput)(search.toSerializable(), effectiveFormat);
2637
2670
  }
2638
2671
  }
2639
2672
  }
@@ -2737,7 +2770,13 @@ function escapeRegex(str) {
2737
2770
  /**
2738
2771
  * Display search results in timeline format (chronological with timestamps and state)
2739
2772
  */
2740
- function displayTimelineResults(results, query, previewLength) {
2773
+ function displayTimelineResults(results, query, previewLength, health) {
2774
+ // The marker prints before the "no results" return: an empty timeline over a
2775
+ // degraded gather is exactly the case that must not read as "nothing exists".
2776
+ const marker = (0, renderable_retrieval_1.degradationSummaryLine)(health);
2777
+ if (marker) {
2778
+ console.log(chalk_1.default.yellow(`\n${marker}`));
2779
+ }
2741
2780
  if (results.length === 0) {
2742
2781
  console.log(chalk_1.default.yellow('\nNo results found.'));
2743
2782
  return;
@@ -3399,20 +3438,28 @@ function registerDigestCommand(memories) {
3399
3438
  spinner.stop();
3400
3439
  throw (0, errors_1.unusableResponseError)('Digest generation', responseShape(data));
3401
3440
  }
3402
- const { digest, metadata, sources, keyFacts, entities, effectiveStateBreakdown } = data;
3441
+ // One construction path; the meta-vs-metadata mapping lives in the
3442
+ // factory (#5124).
3443
+ const renderable = renderable_retrieval_1.RenderableDigestResult.fromApiResponse(data);
3444
+ const { digest, metadata, sources, keyFacts, entities, effectiveStateBreakdown } = renderable;
3403
3445
  if (metadata.sourcesUsed === 0) {
3404
3446
  // Zero sources is a genuine empty result (exit 0), but it is not a
3405
3447
  // success: the old "Digest generated from 0 memories" success glyph
3406
3448
  // read as "here is your answer" when nothing was retrieved.
3407
3449
  spinner.info('No sources met the relevance threshold for this query');
3408
3450
  }
3451
+ else if (renderable.health.degraded) {
3452
+ spinner.warn(`Digest generated from ${metadata.sourcesUsed} memories across ` +
3453
+ `${metadata.conversationsSpanned} conversations` +
3454
+ (0, renderable_retrieval_1.degradationHeadlineSuffix)(renderable.health));
3455
+ }
3409
3456
  else {
3410
3457
  spinner.succeed(`Digest generated from ${metadata.sourcesUsed} memories across ${metadata.conversationsSpanned} conversations`);
3411
3458
  }
3412
- printRetrievalCaveats(metadata, data.meta);
3459
+ printRetrievalCaveats(renderable);
3413
3460
  // JSON/YAML output
3414
3461
  if (options.format === 'json' || options.format === 'yaml') {
3415
- (0, formatters_1.printOutput)(data, options.format);
3462
+ (0, formatters_1.printOutput)(renderable.toSerializable(), options.format);
3416
3463
  return;
3417
3464
  }
3418
3465
  // LLM output — raw digest text only
@@ -3421,7 +3468,7 @@ function registerDigestCommand(memories) {
3421
3468
  return;
3422
3469
  }
3423
3470
  // Default display with box formatting
3424
- displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown);
3471
+ displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown, renderable.health);
3425
3472
  }
3426
3473
  catch (error) {
3427
3474
  // KI-77: name the ceiling and give a narrower-query fix instead of the SDK's
@@ -3498,23 +3545,29 @@ function registerStatusCommand(memories) {
3498
3545
  spinner.stop();
3499
3546
  throw (0, errors_1.unusableResponseError)('Status report generation', responseShape(data));
3500
3547
  }
3501
- const { digest, metadata, sources, keyFacts, entities, effectiveStateBreakdown } = data;
3548
+ const renderable = renderable_retrieval_1.RenderableDigestResult.fromApiResponse(data);
3549
+ const { digest, metadata, sources, keyFacts, entities, effectiveStateBreakdown } = renderable;
3502
3550
  if (metadata.sourcesUsed === 0) {
3503
3551
  spinner.info('No sources met the relevance threshold for this query');
3504
3552
  }
3553
+ else if (renderable.health.degraded) {
3554
+ spinner.warn(`Status report from ${metadata.sourcesUsed} memories across ` +
3555
+ `${metadata.conversationsSpanned} conversations` +
3556
+ (0, renderable_retrieval_1.degradationHeadlineSuffix)(renderable.health));
3557
+ }
3505
3558
  else {
3506
3559
  spinner.succeed(`Status report from ${metadata.sourcesUsed} memories across ${metadata.conversationsSpanned} conversations`);
3507
3560
  }
3508
- printRetrievalCaveats(metadata, data.meta);
3561
+ printRetrievalCaveats(renderable);
3509
3562
  if (options.format === 'json' || options.format === 'yaml') {
3510
- (0, formatters_1.printOutput)(data, options.format);
3563
+ (0, formatters_1.printOutput)(renderable.toSerializable(), options.format);
3511
3564
  return;
3512
3565
  }
3513
3566
  if (options.format === 'llm') {
3514
3567
  console.log(digest);
3515
3568
  return;
3516
3569
  }
3517
- displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown);
3570
+ displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown, renderable.health);
3518
3571
  }
3519
3572
  catch (error) {
3520
3573
  (0, errors_1.handleError)(error);
@@ -3547,10 +3600,16 @@ function responseShape(body) {
3547
3600
  * including `--format json`, where the digest body must remain the only
3548
3601
  * top-level JSON on stdout.
3549
3602
  *
3550
- * @param metadata - DigestMetadata from the response
3551
- * @param meta - Optional response-level meta (forward-compat, see below)
3603
+ * #5124: the degraded branch below read `meta.degraded` on a response that
3604
+ * carries the flag at `metadata.degraded`, so it had never fired on any real
3605
+ * response. It now reads `renderable.health`, which the factory populates from
3606
+ * the field the wire actually uses. Taking the renderable rather than two loose
3607
+ * `any` bags is what stops the same mistake being made again.
3608
+ *
3609
+ * @param renderable - The digest response, health already mapped
3552
3610
  */
3553
- function printRetrievalCaveats(metadata, meta) {
3611
+ function printRetrievalCaveats(renderable) {
3612
+ const metadata = renderable.metadata;
3554
3613
  const avg = typeof metadata?.averageRelevance === 'number' ? metadata.averageRelevance : null;
3555
3614
  const avgLabel = avg === null ? 'unknown' : avg.toFixed(3);
3556
3615
  if (metadata?.narrowWindowFallback) {
@@ -3563,30 +3622,18 @@ function printRetrievalCaveats(metadata, meta) {
3563
3622
  'threshold and the sources span more than one project — this content may come ' +
3564
3623
  'from a different project than the one you are working in.'));
3565
3624
  }
3566
- // Forward-compat with the retrieval team's degraded-retrieval contract
3567
- // (meta.degraded / meta.branchFailures). Both fields are absent on today's
3568
- // responses, so every access is guarded.
3569
- if (meta?.degraded) {
3570
- const failures = Array.isArray(meta.branchFailures) ? meta.branchFailures : [];
3571
- // Branch names are server-supplied and land on stderr, which agents read.
3572
- // Constrain them to an identifier shape and cap the count rather than
3573
- // echoing arbitrary text: an unbounded string here is an injection channel
3574
- // into agent context (and can carry ANSI escapes from a hostile apiUrl).
3575
- const names = failures
3576
- .map((b) => (typeof b === 'string' ? b : b?.branch || b?.name))
3577
- .filter((n) => typeof n === 'string' && /^[A-Za-z0-9_-]{1,32}$/.test(n))
3578
- .slice(0, 8);
3579
- const detail = names.length
3580
- ? `failed branches: ${names.join(', ')}`
3581
- : 'one or more retrieval branches failed';
3582
- console.error(chalk_1.default.yellow(`⚠ Degraded retrieval — ${detail}. These results are incomplete; re-run before ` +
3583
- 'treating them as a full answer.'));
3625
+ // Degraded retrieval. Branch-name sanitising (identifier shape, capped count)
3626
+ // lives in the renderable factory now — branch names are server-supplied and
3627
+ // land on stderr, which agents read.
3628
+ const degradedLine = (0, renderable_retrieval_1.degradationWarningLine)(renderable.health);
3629
+ if (degradedLine) {
3630
+ console.error(chalk_1.default.yellow(degradedLine));
3584
3631
  }
3585
3632
  }
3586
3633
  /**
3587
3634
  * Display a formatted digest with header box and metadata footer
3588
3635
  */
3589
- function displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown) {
3636
+ function displayDigest(digest, metadata, sources, options, keyFacts, entities, effectiveStateBreakdown, health) {
3590
3637
  const width = 66;
3591
3638
  const border = '═'.repeat(width);
3592
3639
  // Format header info
@@ -3600,6 +3647,12 @@ function displayDigest(digest, metadata, sources, options, keyFacts, entities, e
3600
3647
  console.log(chalk_1.default.cyan(`║ ${headerLine1.padEnd(width - 2)}║`));
3601
3648
  console.log(chalk_1.default.cyan(`║ ${headerLine2.padEnd(width - 2)}║`));
3602
3649
  console.log(chalk_1.default.cyan(`╚${border}╝`));
3650
+ // Degradation marker inside the box output, so a reader of the digest text
3651
+ // itself sees it — not only the caller reading stderr.
3652
+ const digestMarker = (0, renderable_retrieval_1.degradationSummaryLine)(health);
3653
+ if (digestMarker) {
3654
+ console.log(chalk_1.default.yellow(digestMarker));
3655
+ }
3603
3656
  // Digest content
3604
3657
  console.log('');
3605
3658
  console.log(digest);
@@ -3688,7 +3741,7 @@ function registerRecapCommand(memories) {
3688
3741
  : undefined;
3689
3742
  // Fetch memories: list endpoint (time-range browse) when no query is given;
3690
3743
  // search endpoint (semantic ranking) when --query is provided.
3691
- let results;
3744
+ let renderable;
3692
3745
  if (options.query) {
3693
3746
  const searchRequest = {
3694
3747
  q: options.query,
@@ -3701,8 +3754,8 @@ function registerRecapCommand(memories) {
3701
3754
  if (excludeTopicsParam)
3702
3755
  searchRequest.excludeTopics = excludeTopicsParam;
3703
3756
  const response = await retryRead(() => (0, api_client_1.getSdk)().memories.searchMemoriesGet(searchRequest));
3704
- // Normalize: API may return flat objects or {memory, score} wrappers.
3705
- results = (response.data?.data || []).map((r) => r.memory ? r : { memory: r, score: r.score, matchedVia: r.matchedVia });
3757
+ // Normalisation and the meta.degraded mapping both live in the factory.
3758
+ renderable = renderable_retrieval_1.RenderableSearchResult.fromApiResponse(response.data);
3706
3759
  }
3707
3760
  else {
3708
3761
  // Issue #1628: search endpoint requires a query string; use list endpoint for
@@ -3717,9 +3770,12 @@ function registerRecapCommand(memories) {
3717
3770
  if (excludeTopicsParam)
3718
3771
  listRequest.excludeTopics = excludeTopicsParam;
3719
3772
  const response = await retryRead(() => (0, api_client_1.getSdk)().memories.listMemories(listRequest));
3720
- // Wrap in search-result shape expected by groupByConversation / displayRecap
3721
- results = (response.data?.data || []).map((m) => ({ memory: m }));
3773
+ // Wrap in search-result shape expected by groupByConversation / displayRecap.
3774
+ // The list endpoint runs no search branches, so it is healthy by construction.
3775
+ renderable = renderable_retrieval_1.RenderableSearchResult.fromListResponse(response.data);
3722
3776
  }
3777
+ const health = renderable.health;
3778
+ let results = [...renderable.results];
3723
3779
  // Client-side filtering as fallback (API exclude may not work with wildcard query)
3724
3780
  if (options.excludeTopics) {
3725
3781
  const excludeSet = new Set(options.excludeTopics.split(',').map((t) => t.trim().toLowerCase()));
@@ -3729,7 +3785,18 @@ function registerRecapCommand(memories) {
3729
3785
  });
3730
3786
  }
3731
3787
  const truncated = results.length >= fetchLimit;
3732
- spinner.succeed(`Found ${results.length} memories`);
3788
+ if (health.degraded) {
3789
+ spinner.warn(`Found ${results.length} memories${(0, renderable_retrieval_1.degradationHeadlineSuffix)(health)}`);
3790
+ }
3791
+ else {
3792
+ spinner.succeed(`Found ${results.length} memories`);
3793
+ }
3794
+ // stderr, before the zero-result return: "no memories in the last 24h"
3795
+ // over an incomplete gather is the exact wrong conclusion to record.
3796
+ const recapDegraded = (0, renderable_retrieval_1.degradationWarningLine)(health);
3797
+ if (recapDegraded) {
3798
+ console.error(chalk_1.default.yellow(recapDegraded));
3799
+ }
3733
3800
  if (results.length === 0) {
3734
3801
  console.log(chalk_1.default.yellow(`\nNo memories found in the last ${options.recent}.`));
3735
3802
  return;
@@ -3746,7 +3813,7 @@ function registerRecapCommand(memories) {
3746
3813
  // Group memories by conversation
3747
3814
  const conversations = groupByConversation(results);
3748
3815
  // Display recap
3749
- displayRecap(conversations, options.recent);
3816
+ displayRecap(conversations, options.recent, health);
3750
3817
  }
3751
3818
  catch (error) {
3752
3819
  (0, errors_1.handleError)(error);
@@ -3779,8 +3846,12 @@ function groupByConversation(results) {
3779
3846
  /**
3780
3847
  * Display recap output grouped by conversation
3781
3848
  */
3782
- function displayRecap(conversations, duration) {
3849
+ function displayRecap(conversations, duration, health) {
3783
3850
  console.log(chalk_1.default.cyan(`\n📅 Work Recap (last ${duration})`));
3851
+ const recapMarker = (0, renderable_retrieval_1.degradationSummaryLine)(health);
3852
+ if (recapMarker) {
3853
+ console.log(chalk_1.default.yellow(recapMarker));
3854
+ }
3784
3855
  console.log(chalk_1.default.gray('═'.repeat(60)));
3785
3856
  // Sort conversations by most recent activity
3786
3857
  const sortedConversations = Array.from(conversations.entries()).sort((a, b) => {
@@ -3876,8 +3947,12 @@ function extractReferences(memories) {
3876
3947
  /**
3877
3948
  * Display search results grouped by conversation
3878
3949
  */
3879
- function displayGroupedSearch(conversations, query) {
3950
+ function displayGroupedSearch(conversations, query, health) {
3880
3951
  console.log(chalk_1.default.cyan(`\n🔍 Search Results for "${query}" (grouped by conversation)`));
3952
+ const groupedMarker = (0, renderable_retrieval_1.degradationSummaryLine)(health);
3953
+ if (groupedMarker) {
3954
+ console.log(chalk_1.default.yellow(groupedMarker));
3955
+ }
3881
3956
  console.log(chalk_1.default.gray('═'.repeat(60)));
3882
3957
  // Sort conversations by most recent activity
3883
3958
  const sortedConversations = Array.from(conversations.entries()).sort((a, b) => {
@@ -3965,17 +4040,36 @@ function registerBuildContextCommand(memories) {
3965
4040
  spinner.stop();
3966
4041
  throw (0, errors_1.unusableResponseError)('Context briefing', responseShape(data));
3967
4042
  }
3968
- spinner.succeed('Context briefing ready');
4043
+ // The API has been sending meta.degraded (with timedOutSections /
4044
+ // failedSections) here all along; the CLI dropped it. A briefing missing
4045
+ // its gotchas section reads exactly like a project with no gotchas.
4046
+ const briefing = renderable_retrieval_1.RenderableContextBriefing.fromApiResponse(data);
4047
+ const briefingHealth = briefing.health;
4048
+ if (briefingHealth.degraded) {
4049
+ spinner.warn(`Context briefing ready${(0, renderable_retrieval_1.degradationHeadlineSuffix)(briefingHealth)}`);
4050
+ }
4051
+ else {
4052
+ spinner.succeed('Context briefing ready');
4053
+ }
4054
+ const briefingDegraded = (0, renderable_retrieval_1.degradationWarningLine)(briefingHealth);
4055
+ if (briefingDegraded) {
4056
+ console.error(chalk_1.default.yellow(briefingDegraded));
4057
+ }
3969
4058
  // JSON output
3970
4059
  if (options.format === 'json' || options.format === 'yaml') {
3971
- (0, formatters_1.printOutput)(data, options.format);
4060
+ (0, formatters_1.printOutput)(briefing.toSerializable(), options.format);
3972
4061
  return;
3973
4062
  }
3974
4063
  // Human-readable output
3975
4064
  console.log('');
4065
+ const briefingMarker = (0, renderable_retrieval_1.degradationSummaryLine)(briefingHealth);
4066
+ if (briefingMarker) {
4067
+ console.log(chalk_1.default.yellow(briefingMarker));
4068
+ console.log('');
4069
+ }
3976
4070
  // Active Work
3977
- if (data.activeWork) {
3978
- const aw = data.activeWork;
4071
+ if (briefing.data.activeWork) {
4072
+ const aw = briefing.data.activeWork;
3979
4073
  console.log(chalk_1.default.yellow.bold('Active Work'));
3980
4074
  console.log(` You were working on: ${chalk_1.default.white(aw.title || '(untitled)')} (${chalk_1.default.gray(aw.conversationId)})`);
3981
4075
  if (aw.lastMemory?.summary) {
@@ -3984,43 +4078,43 @@ function registerBuildContextCommand(memories) {
3984
4078
  console.log('');
3985
4079
  }
3986
4080
  // Key Facts
3987
- if (data.relevantFacts?.length) {
4081
+ if (briefing.data.relevantFacts?.length) {
3988
4082
  console.log(chalk_1.default.cyan.bold('Key Facts'));
3989
- for (const f of data.relevantFacts) {
4083
+ for (const f of briefing.data.relevantFacts) {
3990
4084
  console.log(` ${chalk_1.default.gray('•')} ${f.subject} ${f.predicate} ${f.object}`);
3991
4085
  }
3992
4086
  console.log('');
3993
4087
  }
3994
4088
  // Gotchas
3995
- if (data.gotchas?.length) {
4089
+ if (briefing.data.gotchas?.length) {
3996
4090
  console.log(chalk_1.default.red.bold('Gotchas'));
3997
- for (const g of data.gotchas) {
4091
+ for (const g of briefing.data.gotchas) {
3998
4092
  console.log(` ${chalk_1.default.red('⚠')} ${chalk_1.default.bold(g.fact)} ${chalk_1.default.gray(`(${g.sourceCount} sources)`)}`);
3999
4093
  }
4000
4094
  console.log('');
4001
4095
  }
4002
4096
  // Recent Activity
4003
- if (data.recentActivity?.length) {
4004
- const hours = data.meta?.recentHours || 24;
4097
+ if (briefing.data.recentActivity?.length) {
4098
+ const hours = briefing.data.meta?.recentHours || 24;
4005
4099
  console.log(chalk_1.default.green.bold(`Recent Activity (last ${hours}h)`));
4006
- for (const a of data.recentActivity) {
4100
+ for (const a of briefing.data.recentActivity) {
4007
4101
  const date = a.date ? (0, date_utils_1.formatApiDate)(a.date) : '';
4008
4102
  console.log(` ${chalk_1.default.gray('•')} ${a.summary} ${chalk_1.default.gray(`(${date})`)}`);
4009
4103
  }
4010
4104
  console.log('');
4011
4105
  }
4012
4106
  // Patterns
4013
- if (data.relatedPatterns?.length) {
4107
+ if (briefing.data.relatedPatterns?.length) {
4014
4108
  console.log(chalk_1.default.magenta.bold('Relevant Patterns'));
4015
- for (const p of data.relatedPatterns) {
4109
+ for (const p of briefing.data.relatedPatterns) {
4016
4110
  const conf = p.confidence != null ? chalk_1.default.gray(` (${(p.confidence * 100).toFixed(0)}%)`) : '';
4017
4111
  console.log(` ${chalk_1.default.gray('•')} ${p.pattern}${conf}`);
4018
4112
  }
4019
4113
  console.log('');
4020
4114
  }
4021
4115
  // Meta
4022
- if (data.meta?.contextTerms?.length) {
4023
- console.log(chalk_1.default.gray(`Search terms: ${data.meta.contextTerms.join(', ')}`));
4116
+ if (briefing.data.meta?.contextTerms?.length) {
4117
+ console.log(chalk_1.default.gray(`Search terms: ${briefing.data.meta.contextTerms.join(', ')}`));
4024
4118
  }
4025
4119
  }
4026
4120
  catch (error) {