@cortexkit/opencode-magic-context 0.42.0 → 0.42.2

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.
Files changed (93) hide show
  1. package/dist/config/project-security.d.ts +3 -2
  2. package/dist/config/project-security.d.ts.map +1 -1
  3. package/dist/config/schema/magic-context.d.ts +20 -0
  4. package/dist/config/schema/magic-context.d.ts.map +1 -1
  5. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +8 -4
  6. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  7. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  8. package/dist/features/magic-context/dreamer/expire-memories.d.ts +15 -0
  9. package/dist/features/magic-context/dreamer/expire-memories.d.ts.map +1 -0
  10. package/dist/features/magic-context/dreamer/module-apply.d.ts +4 -2
  11. package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  14. package/dist/features/magic-context/dreamer/task-gates.d.ts +6 -1
  15. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  18. package/dist/features/magic-context/memory/embedding-model-match.d.ts +41 -0
  19. package/dist/features/magic-context/memory/embedding-model-match.d.ts.map +1 -0
  20. package/dist/features/magic-context/memory/embedding-openai.d.ts +7 -20
  21. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  22. package/dist/features/magic-context/memory/embedding-provider.d.ts +2 -2
  23. package/dist/features/magic-context/memory/embedding-provider.d.ts.map +1 -1
  24. package/dist/features/magic-context/memory/embedding-synapse.d.ts +5 -4
  25. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
  26. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  27. package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
  28. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  29. package/dist/features/magic-context/storage-meta-persisted.d.ts +4 -0
  30. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +4 -6
  34. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-runner-validation.d.ts +1 -1
  38. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/embed-session-state.d.ts +1 -1
  40. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/execute-status.d.ts +2 -0
  42. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/format-embed-failure.d.ts +2 -1
  44. package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/lkg-slot.d.ts +2 -0
  47. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/maintenance-authority.d.ts +2 -2
  49. package/dist/hooks/magic-context/maintenance-authority.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +1 -0
  52. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  53. package/dist/{index-c9hamh36.js → index-953x7a79.js} +564 -143
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +579 -217
  56. package/dist/plugin/dream-timer-module-client.d.ts +2 -2
  57. package/dist/plugin/dream-timer-module-client.d.ts.map +1 -1
  58. package/dist/plugin/embedding-bootstrap-helpers.d.ts.map +1 -1
  59. package/dist/plugin/embedding-routing.d.ts.map +1 -1
  60. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  61. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  62. package/dist/{recomp-orchestrator-3atsasjy.js → recomp-orchestrator-5g8f5rhy.js} +1 -1
  63. package/dist/shared/rpc-types.d.ts +10 -0
  64. package/dist/shared/rpc-types.d.ts.map +1 -1
  65. package/dist/shared/status-detail-text.d.ts +3 -1
  66. package/dist/shared/status-detail-text.d.ts.map +1 -1
  67. package/dist/shared/status-summary.d.ts +30 -0
  68. package/dist/shared/status-summary.d.ts.map +1 -0
  69. package/dist/shared/tail-hygiene-status.d.ts +1 -0
  70. package/dist/shared/tail-hygiene-status.d.ts.map +1 -1
  71. package/dist/shared/transcript.d.ts +2 -1
  72. package/dist/shared/transcript.d.ts.map +1 -1
  73. package/dist/shared/user-facing-codes.d.ts +191 -0
  74. package/dist/shared/user-facing-codes.d.ts.map +1 -0
  75. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  76. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  77. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/config/project-security.test.ts +10 -2
  80. package/src/config/project-security.ts +12 -5
  81. package/src/config/schema/magic-context.test.ts +29 -0
  82. package/src/config/schema/magic-context.ts +24 -0
  83. package/src/shared/rpc-types.ts +10 -0
  84. package/src/shared/status-detail-text.test.ts +93 -24
  85. package/src/shared/status-detail-text.ts +9 -2
  86. package/src/shared/status-summary.ts +167 -0
  87. package/src/shared/tag-transcript.ts +14 -7
  88. package/src/shared/tail-hygiene-status.ts +8 -0
  89. package/src/shared/transcript.ts +2 -1
  90. package/src/shared/user-facing-codes.test.ts +140 -0
  91. package/src/shared/user-facing-codes.ts +276 -0
  92. package/src/tui/index.tsx +41 -8
  93. package/src/tui-compiled/index.tsx +660 -647
@@ -14,6 +14,8 @@ import type { LoggerDiagnostics } from "./logger";
14
14
  export interface TailHygieneStatus {
15
15
  /** Tokens in active, non-protected tail content that the agent can reclaim. */
16
16
  u: number;
17
+ /** Spent tool outputs contributing positive tokens to U in this baseline. */
18
+ reclaimableToolOutputCount?: number;
17
19
  /** Tokens in rendered-tail content eligible for hygiene accounting in the same scan. */
18
20
  t: number;
19
21
  /** Reclaimable-to-eligible token ratio, clamped to 0–1 and shared by both nudge mechanisms. */
@@ -55,6 +57,8 @@ export interface SidebarSnapshot {
55
57
  cacheTtl: string;
56
58
  /** Persistent runtime failure shown directly in the sidebar when non-null. */
57
59
  lastTransformError: string | null;
60
+ /** Durable history-compression failure count used to select the stable summary warning. */
61
+ historianFailureCount?: number;
58
62
  lastDreamerRunAt: number | null;
59
63
  projectIdentity: string | null;
60
64
  compartmentTokens: number;
@@ -224,6 +228,12 @@ export interface StatusDetail extends SidebarSnapshot {
224
228
  * (the plugin owns it), so this surface supplies the live DB value; the module
225
229
  * surface supplies the module-store value instead.
226
230
  */
231
+ /** Search-indexing coverage and current command state for the default status summary. */
232
+ embedding?: {
233
+ state: "off" | "running" | "paused" | "stopped" | "ready" | "waiting";
234
+ indexed: number;
235
+ total: number;
236
+ };
227
237
  storage_versions: {
228
238
  /**
229
239
  * Persisted schema version of context.db (MAX of schema_migrations).
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { formatOpenCodeDbMissingStatusLine, resolveOpenCodeDbPath } from "./opencode-db-path";
3
2
  import type { StatusDetail } from "./rpc-types";
4
- import { formatStatusDetailMarkdown } from "./status-detail-text";
3
+ import { formatStatusDetailMarkdown, formatStatusDiagnosticsMarkdown } from "./status-detail-text";
5
4
 
6
5
  const STATUS_FIXTURE: StatusDetail = {
7
6
  sessionId: "ses_status",
@@ -18,9 +17,11 @@ const STATUS_FIXTURE: StatusDetail = {
18
17
  compartmentInProgress: true,
19
18
  sessionNoteCount: 1,
20
19
  readySmartNoteCount: 2,
21
- cacheTtl: "5m",
22
- cacheTtlSource: "session",
20
+ cacheTtl: "1h",
21
+ cacheTtlSource: "config",
22
+ cacheTtlModelKey: "anthropic/claude-opus-5",
23
23
  lastTransformError: null,
24
+ historianFailureCount: 0,
24
25
  lastDreamerRunAt: null,
25
26
  projectIdentity: "/repo",
26
27
  compartmentTokens: 22_000,
@@ -62,38 +63,106 @@ const STATUS_FIXTURE: StatusDetail = {
62
63
  lastErrorMessage: null,
63
64
  lastErrorTime: null,
64
65
  },
66
+ tailHygiene: {
67
+ u: 14_400,
68
+ t: 48_000,
69
+ severity: 0.3,
70
+ evaluable: true,
71
+ generationInvalidated: false,
72
+ baselineGeneration: 3,
73
+ computedAt: 1_730_000_000_000,
74
+ reclaimableToolOutputCount: 3,
75
+ },
76
+ embedding: {
77
+ state: "running",
78
+ indexed: 9,
79
+ total: 12,
80
+ },
65
81
  storage_versions: {
66
82
  context_db_schema_version: 79,
67
83
  plugin_supported_version: 79,
68
84
  },
69
85
  };
70
86
 
71
- describe("formatStatusDetailMarkdown", () => {
72
- test("renders a missing-store condition before the fixed TUI status payload", () => {
73
- expect(formatStatusDetailMarkdown(STATUS_FIXTURE)).toBe(
74
- `${formatOpenCodeDbMissingStatusLine(resolveOpenCodeDbPath())}\n\n## Magic Context Status
87
+ describe("status detail text", () => {
88
+ test("renders the OpenCode summary golden", () => {
89
+ expect(formatStatusDetailMarkdown(STATUS_FIXTURE)).toBe(`## Magic Context Status
75
90
 
76
- - **Mode:** Magic Context compaction
77
- - **Active profile:** work
78
- - **Usage:** 75.0% (96,000 / 128,000 usable tokens)
79
- - **Cache TTL: 5m (session)**; live (42s remaining); TTL 5m
80
- - **Historian:** running; boundary present; coverage 12
81
- - **Memory:** 8 active; 3 injected
82
- - **Tags:** 4 active, 1 dropped; 2 pending drops
83
- - **Execute threshold:** 65.0%`,
84
- );
91
+ - **Context:** 75.0% of usable context (96,000 / 128,000 tokens)
92
+ - **Cache lifetime:** 1h (config for anthropic/claude-opus-5)
93
+ - **Automatic compression:** at 65.0% of usable context
94
+ - **History compression:** Compressing history · 12 history blocks
95
+ - **Reclaimable:** 3 spent tool outputs (~14k tokens)
96
+ - **Memory:** 8 memories · 3 notes
97
+ - **Search indexing:** Running · 9 / 12 history blocks indexed`);
85
98
  });
86
99
 
87
- test("shows module-routed host paths only in Rust-mode chat fallback", () => {
88
- const rustStatus = formatStatusDetailMarkdown({
100
+ test("renders disabled automatic compression and an empty reclaimable set without a gauge", () => {
101
+ const rendered = formatStatusDetailMarkdown({
102
+ ...STATUS_FIXTURE,
103
+ compaction_enabled: false,
104
+ tailHygiene: {
105
+ ...STATUS_FIXTURE.tailHygiene!,
106
+ u: 0,
107
+ reclaimableToolOutputCount: 0,
108
+ },
109
+ });
110
+ expect(rendered).toContain("- **Automatic compression:** Off");
111
+ expect(rendered).toContain("- **Reclaimable:** none");
112
+ expect(rendered).not.toContain("Reclaimable: 0");
113
+ });
114
+
115
+ test("renders summary and diagnostics from one snapshot with matching status values", () => {
116
+ const summary = formatStatusDetailMarkdown(STATUS_FIXTURE);
117
+ const diagnostics = formatStatusDiagnosticsMarkdown(STATUS_FIXTURE);
118
+ for (const value of ["75.0%", "65.0%", "1h (config for anthropic/claude-opus-5)"]) {
119
+ expect(summary).toContain(value);
120
+ expect(diagnostics).toContain(value);
121
+ }
122
+ });
123
+
124
+ test("keeps the previous OpenCode detail behind diagnostics", () => {
125
+ const diagnostics = formatStatusDiagnosticsMarkdown(STATUS_FIXTURE);
126
+ expect(diagnostics).toContain("- **Active profile:** work");
127
+ expect(diagnostics).toContain("- **Tags:** 4 active, 1 dropped; 2 pending drops");
128
+ expect(diagnostics).toContain("- **Execute threshold:** 65.0%");
129
+ });
130
+
131
+ test("keeps internal vocabulary and identifiers out of the summary", () => {
132
+ const summary = formatStatusDetailMarkdown({
89
133
  ...STATUS_FIXTURE,
134
+ sessionId: "session-secret",
135
+ projectIdentity: "/Users/example/secret-project",
90
136
  hostBackendsModuleSide: true,
137
+ lastTransformError: "facade MODULE drain failed in mc-store /tmp/private",
138
+ historianFailureCount: 2,
91
139
  });
92
- const tsStatus = formatStatusDetailMarkdown(STATUS_FIXTURE);
140
+ for (const forbidden of [
141
+ "session-secret",
142
+ "/Users/",
143
+ "tag counter",
144
+ "protection",
145
+ "formula",
146
+ "MODULE",
147
+ "mc-store",
148
+ "harness",
149
+ "compartment",
150
+ "facade",
151
+ "changefeed",
152
+ "drain",
153
+ ]) {
154
+ expect(summary.toLowerCase()).not.toContain(forbidden.toLowerCase());
155
+ }
156
+ expect(summary).toContain("(MC-S02)");
157
+ expect(summary).toContain("(MC-H01)");
158
+ });
93
159
 
94
- expect(rustStatus).toContain(
95
- "Host backends → MODULE: ctx_memory, ctx_note; historian: module-side",
96
- );
97
- expect(tsStatus).not.toContain("Host backends → MODULE");
160
+ test("does not expose module routing in the summary", () => {
161
+ const rustStatus = formatStatusDetailMarkdown({
162
+ ...STATUS_FIXTURE,
163
+ hostBackendsModuleSide: true,
164
+ });
165
+ expect(rustStatus).not.toContain("MODULE");
166
+ expect(rustStatus).not.toContain("mc-store");
98
167
  });
99
168
  });
@@ -9,6 +9,8 @@ import {
9
9
  } from "./opencode-db-path";
10
10
  import type { StatusDetail } from "./rpc-types";
11
11
  import { RUST_MODE_HOST_PATHS_LINE } from "./rust-mode-status";
12
+ import { renderUserStatusSummary, statusSummaryFromDetail } from "./status-summary";
13
+ import { renderUserFacingFailure } from "./user-facing-codes";
12
14
 
13
15
  function formatCount(value: number): string {
14
16
  return Math.round(value).toLocaleString();
@@ -21,8 +23,13 @@ function formatCacheLane(detail: StatusDetail): string {
21
23
  return `live (${Math.round(detail.cacheRemainingMs / 1000)}s remaining); TTL ${detail.cacheTtl}`;
22
24
  }
23
25
 
24
- /** Render the same StatusDetail payload that powers the TUI dialog for chat-only clients. */
26
+ /** Render the default user summary for chat-only OpenCode clients. */
25
27
  export function formatStatusDetailMarkdown(detail: StatusDetail): string {
28
+ return renderUserStatusSummary(statusSummaryFromDetail(detail), "markdown");
29
+ }
30
+
31
+ /** Render the opt-in operator detail that the status dialog exposes behind Diagnostics. */
32
+ export function formatStatusDiagnosticsMarkdown(detail: StatusDetail): string {
26
33
  const usableLimit =
27
34
  detail.contextLimit > 0
28
35
  ? `${formatCount(detail.contextLimit)} usable tokens`
@@ -71,7 +78,7 @@ export function formatStatusDetailMarkdown(detail: StatusDetail): string {
71
78
  );
72
79
  }
73
80
  if (detail.lastTransformError) {
74
- lines.push(`- **Last transform error:** ${detail.lastTransformError}`);
81
+ lines.push(`- **Warning:** ${renderUserFacingFailure("transform_update_failed")}`);
75
82
  }
76
83
 
77
84
  return lines.join("\n");
@@ -0,0 +1,167 @@
1
+ import { formatCacheTtlDisplay } from "./cache-ttl-display";
2
+ import type { StatusDetail } from "./rpc-types";
3
+ import { renderUserFacingFailure, type UserFacingFailureKey } from "./user-facing-codes";
4
+
5
+ export type StatusCompressionState = "off" | "compressing" | "ready" | "waiting";
6
+ export type StatusEmbeddingState = "off" | "running" | "paused" | "stopped" | "ready" | "waiting";
7
+
8
+ export interface UserStatusSummary {
9
+ inputTokens: number;
10
+ usableContextTokens: number;
11
+ usagePercentage: number;
12
+ cacheLifetime: string;
13
+ automaticCompressionThreshold: number | null;
14
+ compression: {
15
+ state: StatusCompressionState;
16
+ historyBlockCount: number;
17
+ };
18
+ reclaimable: {
19
+ toolOutputCount: number;
20
+ tokens: number;
21
+ };
22
+ memoryCount: number;
23
+ noteCount: number;
24
+ embedding: {
25
+ state: StatusEmbeddingState;
26
+ indexed: number;
27
+ total: number;
28
+ };
29
+ warnings: UserFacingFailureKey[];
30
+ }
31
+
32
+ export function statusSummaryFromDetail(detail: StatusDetail): UserStatusSummary {
33
+ const compressionState: StatusCompressionState =
34
+ detail.compaction_enabled === false
35
+ ? "off"
36
+ : detail.historianRunning ||
37
+ detail.compartmentInProgress ||
38
+ (detail.recompProgress?.phase === "recomp" &&
39
+ detail.recompProgress.kind !== "embed")
40
+ ? "compressing"
41
+ : detail.compartmentCount > 0
42
+ ? "ready"
43
+ : "waiting";
44
+ const warnings: UserFacingFailureKey[] = [];
45
+ if (detail.lastTransformError) warnings.push("transform_update_failed");
46
+ if ((detail.historianFailureCount ?? 0) > 0) warnings.push("historian_unavailable");
47
+ if ((detail.configParseFailures?.length ?? 0) > 0) warnings.push("configuration_warning");
48
+ if (detail.embedding?.state === "stopped") warnings.push("embedding_unavailable");
49
+ if ((detail.loggerDiagnostics?.swallowedWriteCount ?? 0) > 0) {
50
+ warnings.push("status_log_unavailable");
51
+ }
52
+
53
+ return {
54
+ inputTokens: detail.inputTokens,
55
+ usableContextTokens: detail.contextLimit,
56
+ usagePercentage: detail.usagePercentage,
57
+ cacheLifetime: formatCacheTtlDisplay({
58
+ value: detail.cacheTtl,
59
+ source: detail.cacheTtlSource ?? "session",
60
+ modelKey: detail.cacheTtlModelKey,
61
+ }).replace(/^Cache TTL:\s*/, ""),
62
+ automaticCompressionThreshold:
63
+ detail.compaction_enabled === false ? null : detail.executeThreshold,
64
+ compression: {
65
+ state: compressionState,
66
+ historyBlockCount: detail.compartmentCount,
67
+ },
68
+ reclaimable: {
69
+ toolOutputCount: detail.tailHygiene?.reclaimableToolOutputCount ?? 0,
70
+ tokens: detail.tailHygiene?.u ?? 0,
71
+ },
72
+ memoryCount: detail.memoryCount,
73
+ noteCount: (detail.sessionNoteCount ?? 0) + (detail.readySmartNoteCount ?? 0),
74
+ embedding: detail.embedding ?? {
75
+ state: "waiting",
76
+ indexed: 0,
77
+ total: 0,
78
+ },
79
+ warnings: [...new Set(warnings)],
80
+ };
81
+ }
82
+
83
+ function formatCount(value: number): string {
84
+ return Math.round(value).toLocaleString();
85
+ }
86
+
87
+ function compressionText(summary: UserStatusSummary): string {
88
+ const historyBlocks = `${summary.compression.historyBlockCount} history block${
89
+ summary.compression.historyBlockCount === 1 ? "" : "s"
90
+ }`;
91
+ switch (summary.compression.state) {
92
+ case "off":
93
+ return "Off";
94
+ case "compressing":
95
+ return `Compressing history · ${historyBlocks}`;
96
+ case "ready":
97
+ return `Ready · ${historyBlocks}`;
98
+ case "waiting":
99
+ return "Waiting for enough conversation history";
100
+ }
101
+ }
102
+
103
+ function reclaimableText(summary: UserStatusSummary): string {
104
+ const count = Math.max(0, Math.floor(summary.reclaimable.toolOutputCount));
105
+ if (count === 0) return "none";
106
+ const outputs = `${count} spent tool output${count === 1 ? "" : "s"}`;
107
+ return `${outputs} (~${Math.round(Math.max(0, summary.reclaimable.tokens) / 1000)}k tokens)`;
108
+ }
109
+
110
+ function embeddingText(summary: UserStatusSummary): string {
111
+ if (summary.embedding.state === "off") return "Off";
112
+ const coverage = `${formatCount(summary.embedding.indexed)} / ${formatCount(summary.embedding.total)} history blocks indexed`;
113
+ switch (summary.embedding.state) {
114
+ case "running":
115
+ return `Running · ${coverage}`;
116
+ case "paused":
117
+ return `Paused · ${coverage}`;
118
+ case "stopped":
119
+ return `Paused after an issue · ${coverage}`;
120
+ case "ready":
121
+ return `Ready · ${coverage}`;
122
+ case "waiting":
123
+ return `Waiting · ${coverage}`;
124
+ }
125
+ }
126
+
127
+ export function renderUserStatusSummary(
128
+ summary: UserStatusSummary,
129
+ style: "markdown" | "plain",
130
+ ): string {
131
+ const context = `${summary.usagePercentage.toFixed(1)}% of usable context (${formatCount(summary.inputTokens)} / ${
132
+ summary.usableContextTokens > 0 ? formatCount(summary.usableContextTokens) : "?"
133
+ } tokens)`;
134
+ const values = [
135
+ ["Context", context],
136
+ ["Cache lifetime", summary.cacheLifetime],
137
+ [
138
+ "Automatic compression",
139
+ summary.automaticCompressionThreshold === null
140
+ ? "Off"
141
+ : `at ${summary.automaticCompressionThreshold.toFixed(1)}% of usable context`,
142
+ ],
143
+ ["History compression", compressionText(summary)],
144
+ ["Reclaimable", reclaimableText(summary)],
145
+ [
146
+ "Memory",
147
+ `${formatCount(summary.memoryCount)} memories · ${formatCount(summary.noteCount)} notes`,
148
+ ],
149
+ ["Search indexing", embeddingText(summary)],
150
+ ] as const;
151
+ const lines =
152
+ style === "markdown"
153
+ ? [
154
+ "## Magic Context Status",
155
+ "",
156
+ ...values.map(([label, value]) => `- **${label}:** ${value}`),
157
+ ]
158
+ : ["Magic Context Status", ...values.map(([label, value]) => `${label}: ${value}`)];
159
+ for (const warning of summary.warnings) {
160
+ lines.push(
161
+ style === "markdown"
162
+ ? `- **Warning:** ${renderUserFacingFailure(warning)}`
163
+ : `Warning: ${renderUserFacingFailure(warning)}`,
164
+ );
165
+ }
166
+ return lines.join("\n");
167
+ }
@@ -1063,13 +1063,20 @@ function buildAggregateTarget(
1063
1063
  occurrences.some((occ) => occ.kind === "tool_use") &&
1064
1064
  occurrences.some((occ) => occ.kind === "tool_result");
1065
1065
  if (!complete) return "incomplete";
1066
- if (
1067
- !requiresToolArcSkeleton &&
1068
- occurrences.every((occ) => occ.part.remove && occ.part.canRemove?.() !== false)
1069
- ) {
1070
- let removed = false;
1071
- for (const occ of occurrences) removed = occ.part.remove?.() || removed;
1072
- return removed ? "removed" : "absent";
1066
+ if (!requiresToolArcSkeleton) {
1067
+ const authorization = occurrences.map((occ) => occ.part.canRemove?.());
1068
+ // A failed durable marker is not permission to substitute a new
1069
+ // skeleton. Keep both halves unchanged and leave the drop queued.
1070
+ if (authorization.includes("defer")) return "incomplete";
1071
+ if (
1072
+ occurrences.every(
1073
+ (occ, index) => occ.part.remove && authorization[index] !== false,
1074
+ )
1075
+ ) {
1076
+ let removed = false;
1077
+ for (const occ of occurrences) removed = occ.part.remove?.() || removed;
1078
+ return removed ? "removed" : "absent";
1079
+ }
1073
1080
  }
1074
1081
  // Adapters without structural removal retain paired sentinels.
1075
1082
  const sentinel = `[dropped \u00a7${tagId}\u00a7]`;
@@ -9,6 +9,7 @@ export interface WireTailHygieneBaseline {
9
9
  generation_invalidated?: boolean;
10
10
  baseline_generation?: number;
11
11
  computed_at_ms?: number;
12
+ reclaimable_tool_output_count?: number;
12
13
  }
13
14
 
14
15
  function finiteNumber(value: unknown, fallback = 0): number {
@@ -34,6 +35,10 @@ export function resolveTailHygieneStatus(
34
35
  generationInvalidated: rustBaseline.generation_invalidated === true,
35
36
  baselineGeneration: Math.max(0, finiteNumber(rustBaseline.baseline_generation)),
36
37
  computedAt: Math.max(0, finiteNumber(rustBaseline.computed_at_ms)),
38
+ reclaimableToolOutputCount: Math.max(
39
+ 0,
40
+ Math.floor(finiteNumber(rustBaseline.reclaimable_tool_output_count)),
41
+ ),
37
42
  };
38
43
  }
39
44
  if (tsBaseline === undefined) return undefined;
@@ -47,6 +52,9 @@ export function resolveTailHygieneStatus(
47
52
  generationInvalidated: tsBaseline.generationInvalidated,
48
53
  baselineGeneration: Math.max(0, tsBaseline.baselineGeneration),
49
54
  computedAt: Math.max(0, tsBaseline.computedAt),
55
+ reclaimableToolOutputCount: tsBaseline.baselineParts.filter(
56
+ (part) => part.kind === "toolOutput" && part.uTokens > 0,
57
+ ).length,
50
58
  };
51
59
  }
52
60
 
@@ -170,7 +170,8 @@ export interface TranscriptPart {
170
170
  /** Remove one half of a complete tool arc. Callers must remove both halves together. */
171
171
  remove?(): boolean;
172
172
  /** Refuse structural deletion when the adapter cannot match provider-native identity safely. */
173
- canRemove?(): boolean;
173
+ /** A deferred durable decision vetoes the whole drop, including sentinel fallback. */
174
+ canRemove?(): boolean | "defer";
174
175
 
175
176
  /**
176
177
  * Optional: serialized byte size of the part's REAL payload, including
@@ -0,0 +1,140 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import type { DreamRunFailureClass } from "../features/magic-context/dreamer/storage-dream-runs";
5
+ import type { EmbeddingFailureClass } from "../features/magic-context/memory/embedding-failure";
6
+ import {
7
+ type CapabilityRefusal,
8
+ dreamFailureCode,
9
+ embeddingFailureCode,
10
+ renderCapabilityRefusal,
11
+ renderDreamFailure,
12
+ renderEmbeddingFailure,
13
+ renderUserFacingFailure,
14
+ USER_FACING_FAILURES,
15
+ } from "./user-facing-codes";
16
+
17
+ const DREAM_CLASSES: DreamRunFailureClass[] = [
18
+ "provider_timeout",
19
+ "provider_error",
20
+ "empty_completion",
21
+ "no_models",
22
+ "child_aborted",
23
+ "parse_failed",
24
+ "unknown",
25
+ ];
26
+
27
+ const EMBEDDING_CLASSES: EmbeddingFailureClass[] = [
28
+ "substitution_rejected",
29
+ "http_error",
30
+ "transport_error",
31
+ "invalid_envelope",
32
+ "empty_result",
33
+ "certification_refusal",
34
+ "credential_required",
35
+ "local_binding_missing",
36
+ "local_fs_unavailable",
37
+ "local_download_failure",
38
+ "local_runtime_error",
39
+ ];
40
+
41
+ describe("user-facing failure codes", () => {
42
+ test("keeps the closed table unique and renders one calm sentence plus one action", () => {
43
+ const entries = Object.values(USER_FACING_FAILURES);
44
+ expect(new Set(entries.map((entry) => entry.code)).size).toBe(entries.length);
45
+ for (const [key, entry] of Object.entries(USER_FACING_FAILURES)) {
46
+ expect(entry.code).toMatch(/^MC-[CHDERS]\d{2}$/);
47
+ expect(entry.sentence).toEndWith(".");
48
+ expect(entry.action).toEndWith(".");
49
+ expect(renderUserFacingFailure(key as keyof typeof USER_FACING_FAILURES)).toBe(
50
+ `${entry.sentence} ${entry.action} (${entry.code})`,
51
+ );
52
+ }
53
+ });
54
+
55
+ test("uses actionable command and configuration guidance in markdown and plain text", () => {
56
+ expect(renderEmbeddingFailure("local_binding_missing")).toContain(
57
+ "Run `npx @cortexkit/magic-context doctor`, then retry. (MC-E08)",
58
+ );
59
+ expect(renderEmbeddingFailure("local_runtime_error", "plain")).toContain(
60
+ "Run npx @cortexkit/magic-context doctor, then retry. (MC-E11)",
61
+ );
62
+ expect(renderEmbeddingFailure("local_runtime_error", "plain")).not.toContain("`");
63
+ expect(renderEmbeddingFailure("certification_refusal")).toContain(
64
+ "Finish the provider setup, or set a fallback provider in the embedding settings, then run /ctx-embed start again. (MC-E06)",
65
+ );
66
+ expect(renderUserFacingFailure("configuration_warning")).toContain(
67
+ "Fix the configuration warning shown in /ctx-status diagnostics, then restart. (MC-S03)",
68
+ );
69
+ expect(renderUserFacingFailure("session_upgrade_unavailable")).toContain(
70
+ "Run /ctx-recomp instead. (MC-C07)",
71
+ );
72
+ });
73
+
74
+ test("maps every structured dream and embedding failure class", () => {
75
+ for (const failureClass of DREAM_CLASSES) {
76
+ expect(renderDreamFailure(failureClass)).toEndWith(
77
+ `(${dreamFailureCode(failureClass)})`,
78
+ );
79
+ }
80
+ for (const failureClass of EMBEDDING_CLASSES) {
81
+ expect(renderEmbeddingFailure(failureClass)).toEndWith(
82
+ `(${embeddingFailureCode(failureClass)})`,
83
+ );
84
+ }
85
+ });
86
+
87
+ test("renders every capability refusal without internal vocabulary", () => {
88
+ const capabilities: CapabilityRefusal[] = [
89
+ "memory_write",
90
+ "memory_access",
91
+ "note_change",
92
+ "note_access",
93
+ "context_cleanup",
94
+ "partial_history",
95
+ "session_upgrade",
96
+ "smart_note_condition",
97
+ "history_compression",
98
+ "context_service",
99
+ ];
100
+ for (const capability of capabilities) {
101
+ const rendered = renderCapabilityRefusal(capability);
102
+ expect(rendered).toMatch(/\(MC-C\d{2}\)$/);
103
+ for (const forbidden of ["authority", "MODULE", "drain", "facade", "changefeed"]) {
104
+ expect(rendered).not.toContain(forbidden);
105
+ }
106
+ }
107
+ });
108
+
109
+ test("source fence keeps raw exceptions out of user-facing reply builders", () => {
110
+ const builders = [
111
+ "hooks/magic-context/command-handler.ts",
112
+ "hooks/magic-context/compartment-runner-validation.ts",
113
+ "hooks/magic-context/compartment-runner-recomp.ts",
114
+ "hooks/magic-context/compartment-runner-partial-recomp.ts",
115
+ "hooks/magic-context/format-embed-failure.ts",
116
+ "hooks/magic-context/recomp-orchestrator.ts",
117
+ "shared/status-detail-text.ts",
118
+ "../../pi-plugin/src/commands/ctx-dream.ts",
119
+ "../../pi-plugin/src/commands/ctx-status.ts",
120
+ ];
121
+ const allowedLogFragments = ["command notification delivery failed", "/ctx-dream failed"];
122
+ const violations: string[] = [];
123
+ for (const relativePath of builders) {
124
+ const path = resolve(import.meta.dir, "..", relativePath);
125
+ const lines = readFileSync(path, "utf8").split("\n");
126
+ lines.forEach((line, index) => {
127
+ if (
128
+ !/(error\.message|String\((?:err|error)\)|describeError\([^)]*\)\.brief)/.test(
129
+ line,
130
+ )
131
+ ) {
132
+ return;
133
+ }
134
+ if (allowedLogFragments.some((fragment) => line.includes(fragment))) return;
135
+ violations.push(`${relativePath}:${index + 1}: ${line.trim()}`);
136
+ });
137
+ }
138
+ expect(violations).toEqual([]);
139
+ });
140
+ });