@cortexkit/opencode-magic-context 0.40.1 → 0.41.1

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 (194) hide show
  1. package/README.md +1 -1
  2. package/dist/{announcement-5eg4t64s.js → announcement-3g9xg433.js} +2 -2
  3. package/dist/config/schema/magic-context.d.ts +15 -0
  4. package/dist/config/schema/magic-context.d.ts.map +1 -1
  5. package/dist/{conflict-warning-hook-f406ggbw.js → conflict-warning-hook-rf17b6et.js} +4 -4
  6. package/dist/{derive-budgets-xk8cy5e6.js → derive-budgets-bb7mrpzs.js} +3 -3
  7. package/dist/features/builtin-commands/commands.d.ts +35 -2
  8. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  9. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +12 -16
  10. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  11. package/dist/features/magic-context/context-authority.d.ts +10 -0
  12. package/dist/features/magic-context/context-authority.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/classify.d.ts +1 -0
  14. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/lease.d.ts +12 -27
  16. package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +1 -1
  18. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -0
  20. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts +9 -0
  22. package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts.map +1 -0
  23. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/task-registry.d.ts +2 -0
  25. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +7 -2
  27. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  28. package/dist/features/magic-context/dreamer/verify.d.ts +13 -10
  29. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/embedding-failure.d.ts +1 -1
  31. package/dist/features/magic-context/memory/embedding-failure.d.ts.map +1 -1
  32. package/dist/features/magic-context/memory/embedding-local.d.ts +12 -2
  33. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  34. package/dist/features/magic-context/memory/embedding-synapse.d.ts +10 -1
  35. package/dist/features/magic-context/memory/embedding-synapse.d.ts.map +1 -1
  36. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  37. package/dist/features/magic-context/memory/project-identity.d.ts +4 -0
  38. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  39. package/dist/features/magic-context/memory/storage-memory-verifications.d.ts +5 -1
  40. package/dist/features/magic-context/memory/storage-memory-verifications.d.ts.map +1 -1
  41. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  42. package/dist/features/magic-context/memory/transformers-web-entry.d.ts +2 -0
  43. package/dist/features/magic-context/memory/transformers-web-entry.d.ts.map +1 -0
  44. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  45. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  46. package/dist/features/magic-context/project-embedding-registry.d.ts +3 -0
  47. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  48. package/dist/features/magic-context/search.d.ts +18 -0
  49. package/dist/features/magic-context/search.d.ts.map +1 -1
  50. package/dist/features/magic-context/storage-db.d.ts +1 -1
  51. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  52. package/dist/features/magic-context/storage-identity-merge.d.ts.map +1 -1
  53. package/dist/features/magic-context/storage-meta-persisted.d.ts +29 -3
  54. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  55. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  56. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  57. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  58. package/dist/features/magic-context/storage-ops.d.ts +2 -0
  59. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  60. package/dist/features/magic-context/storage-tags.d.ts +7 -0
  61. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  62. package/dist/features/magic-context/storage.d.ts +2 -2
  63. package/dist/features/magic-context/storage.d.ts.map +1 -1
  64. package/dist/hooks/magic-context/boundary-execution.d.ts +3 -0
  65. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -4
  67. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  68. package/dist/hooks/magic-context/command-handler.d.ts +7 -0
  69. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  70. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  72. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -0
  74. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +15 -14
  76. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/emergency-drop.d.ts +6 -8
  78. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/event-resolvers.d.ts +2 -2
  80. package/dist/hooks/magic-context/execute-status.d.ts +4 -1
  81. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  82. package/dist/hooks/magic-context/format-embed-failure.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +6 -0
  84. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/hook-handlers.d.ts +21 -8
  86. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/hook.d.ts +13 -0
  88. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/inject-compartments.d.ts +6 -16
  90. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/maintenance-authority.d.ts +3 -0
  92. package/dist/hooks/magic-context/maintenance-authority.d.ts.map +1 -0
  93. package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
  94. package/dist/hooks/magic-context/module-wire.d.ts.map +1 -1
  95. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -0
  97. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/rust-mode-transform.d.ts +9 -0
  99. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/strip-content.d.ts +8 -0
  101. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/stripped-command.d.ts +19 -0
  103. package/dist/hooks/magic-context/stripped-command.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/supersession-reclaim.d.ts +11 -0
  105. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/tag-messages.d.ts +6 -0
  107. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +11 -0
  109. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  111. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +8 -1
  113. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  115. package/dist/{index-mg1yx1ph.js → index-2d4kf2ve.js} +2 -2
  116. package/dist/{index-mmrhhk9a.js → index-37yzyeca.js} +36 -8
  117. package/dist/index-4x691bz5.js +369 -0
  118. package/dist/{index-3dkzzh0t.js → index-9j3tx8hy.js} +1 -1
  119. package/dist/{index-sw8gnq34.js → index-b9j85d29.js} +2 -2
  120. package/dist/{index-ehpzdy0v.js → index-d1mxrd3p.js} +7 -1
  121. package/dist/{index-x64cmpej.js → index-dh0vfghk.js} +817 -438
  122. package/dist/{index-qf20mv7e.js → index-dmvy29z0.js} +1 -1
  123. package/dist/{index-1h9t9xb5.js → index-eq9wr75c.js} +2 -2
  124. package/dist/{index-pck9vadr.js → index-vnz6jvdc.js} +1 -1
  125. package/dist/{index-rw0qrkvq.js → index-z893kjyt.js} +17 -4
  126. package/dist/{index-btj2qhmc.js → index-zwjhba2r.js} +3 -3
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +3230 -2422
  129. package/dist/{model-resolution-r9jbcz86.js → model-resolution-6w6wzw18.js} +3 -1
  130. package/dist/plugin/dream-timer.d.ts.map +1 -1
  131. package/dist/plugin/embedding-routing.d.ts.map +1 -1
  132. package/dist/plugin/hooks/create-session-hooks.d.ts +14 -0
  133. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  134. package/dist/plugin/rpc-handlers.d.ts +10 -0
  135. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  136. package/dist/{recomp-orchestrator-dtgww0f0.js → recomp-orchestrator-w7xg48e3.js} +7 -7
  137. package/dist/{safe-notification-target-tepzc2xa.js → safe-notification-target-2a8py95v.js} +3 -3
  138. package/dist/{send-session-notification-6v5b72b4.js → send-session-notification-bw7eex6w.js} +4 -4
  139. package/dist/shared/data-path.d.ts +12 -3
  140. package/dist/shared/data-path.d.ts.map +1 -1
  141. package/dist/shared/logger.d.ts.map +1 -1
  142. package/dist/shared/model-resolution.d.ts +1 -0
  143. package/dist/shared/model-resolution.d.ts.map +1 -1
  144. package/dist/shared/rpc-types.d.ts +5 -0
  145. package/dist/shared/rpc-types.d.ts.map +1 -1
  146. package/dist/shared/rust-mode-status.d.ts +3 -0
  147. package/dist/shared/rust-mode-status.d.ts.map +1 -0
  148. package/dist/shared/status-detail-text.d.ts.map +1 -1
  149. package/dist/shared/subagent-runner.d.ts +4 -0
  150. package/dist/shared/subagent-runner.d.ts.map +1 -1
  151. package/dist/shared/test-temp-dir.d.ts +40 -0
  152. package/dist/shared/test-temp-dir.d.ts.map +1 -0
  153. package/dist/shared/write-transaction-timing.d.ts +11 -0
  154. package/dist/shared/write-transaction-timing.d.ts.map +1 -0
  155. package/dist/{storage-meta-session-yvtbaw7c.js → storage-meta-session-gr4zn7sq.js} +4 -4
  156. package/dist/{task-config-a1qmj9bx.js → task-config-gv6s04fp.js} +2 -2
  157. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  158. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  159. package/dist/tools/ctx-search/tools.d.ts.map +1 -1
  160. package/dist/transformers-web.js +24570 -0
  161. package/dist/transformers.node-b1wa0hgx.js +28808 -0
  162. package/dist/tui/data/context-db.d.ts +10 -3
  163. package/dist/tui/data/context-db.d.ts.map +1 -1
  164. package/dist/tui-compiled/data/context-db.d.ts +10 -3
  165. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  166. package/package.json +7 -3
  167. package/src/config/index.test.ts +47 -4
  168. package/src/config/schema/magic-context.test.ts +20 -0
  169. package/src/config/schema/magic-context.ts +8 -0
  170. package/src/features/magic-context/dreamer/task-registry.ts +2 -0
  171. package/src/shared/conflict-detector.test.ts +6 -19
  172. package/src/shared/conflict-fixer.test.ts +5 -8
  173. package/src/shared/data-path.test.ts +44 -4
  174. package/src/shared/data-path.ts +54 -11
  175. package/src/shared/logger.test.ts +145 -35
  176. package/src/shared/logger.ts +124 -6
  177. package/src/shared/model-resolution.test.ts +37 -0
  178. package/src/shared/model-resolution.ts +7 -0
  179. package/src/shared/redaction.ts +2 -2
  180. package/src/shared/rpc-types.ts +5 -0
  181. package/src/shared/rust-mode-status.ts +3 -0
  182. package/src/shared/status-detail-text.test.ts +13 -0
  183. package/src/shared/status-detail-text.ts +2 -0
  184. package/src/shared/subagent-runner.ts +4 -0
  185. package/src/shared/test-temp-dir.test.ts +57 -0
  186. package/src/shared/test-temp-dir.ts +165 -0
  187. package/src/shared/write-transaction-timing.ts +25 -0
  188. package/src/tui/data/context-db.test.ts +31 -1
  189. package/src/tui/data/context-db.ts +17 -61
  190. package/src/tui/index.tsx +36 -5
  191. package/src/tui-compiled/data/context-db.ts +17 -61
  192. package/src/tui-compiled/index.tsx +229 -191
  193. package/dist/index-8pqfcghm.js +0 -81
  194. /package/dist/{index-rpgg35ms.js → index-8sseka65.js} +0 -0
@@ -18,45 +18,121 @@ type LoggerScenarioResult = {
18
18
  };
19
19
  };
20
20
 
21
+ type RotationScenarioResult = {
22
+ firstCurrent: string;
23
+ firstPredecessorFirstByte: number;
24
+ current: string;
25
+ predecessorFirstByte: number;
26
+ predecessorSize: number;
27
+ currentSize: number;
28
+ currentMode: number;
29
+ predecessorMode: number;
30
+ files: string[];
31
+ };
32
+
33
+ type HotPathScenarioResult = {
34
+ statCalls: number;
35
+ };
36
+
21
37
  const loggerScenario = `
22
- import { existsSync, readFileSync } from "node:fs";
38
+ import fs from "node:fs";
23
39
  import * as path from "node:path";
40
+ import { mock } from "bun:test";
24
41
 
25
42
  const root = process.env.LOGGER_SCENARIO_ROOT;
26
43
  const loggerModuleUrl = process.env.LOGGER_MODULE_URL;
27
- if (!root || !loggerModuleUrl) throw new Error("logger scenario environment is incomplete");
44
+ const scenario = process.env.LOGGER_SCENARIO;
45
+ if (!root || !loggerModuleUrl || !scenario) {
46
+ throw new Error("logger scenario environment is incomplete");
47
+ }
28
48
 
29
- const logger = await import(loggerModuleUrl);
30
49
  const logPath = path.join(root, "nested", "magic-context.log");
31
50
  process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
32
- logger.log("first");
33
- logger.flushLogger();
34
- const healthyDiagnostics = logger.getLoggerDiagnostics();
35
-
36
- const logDirectory = path.dirname(logPath);
37
- if (process.env.LOGGER_SCENARIO === "recovery") {
38
- const { rmSync } = await import("node:fs");
39
- rmSync(logDirectory, { recursive: true, force: true });
40
- logger.log("second");
41
- logger.flushLogger();
51
+ let statCalls = 0;
52
+ if (scenario === "hot-path") {
53
+ mock.module("node:fs", () => ({
54
+ ...fs,
55
+ statSync: (...args) => {
56
+ statCalls++;
57
+ return fs.statSync(...args);
58
+ },
59
+ }));
42
60
  }
43
61
 
44
- // A directory used as the file target makes append fail deterministically on every platform.
45
- const failedPath = path.join(root, "unwritable-log-target");
46
- const { mkdirSync } = await import("node:fs");
47
- mkdirSync(failedPath, { recursive: true });
48
- process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
49
- logger.log("failed write");
50
- logger.flushLogger();
51
- const failedDiagnostics = logger.getLoggerDiagnostics();
52
-
53
- const content = existsSync(logPath) ? readFileSync(logPath, "utf8") : "";
54
- console.log(JSON.stringify({
55
- exists: existsSync(logPath),
56
- content,
57
- healthyDiagnostics,
58
- failedDiagnostics,
59
- }));
62
+ const logger = await import(loggerModuleUrl);
63
+
64
+ if (scenario === "rotation") {
65
+ const maxLogBytes = 32 * 1024 * 1024;
66
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
67
+ fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "o"));
68
+ fs.chmodSync(logPath, 0o600);
69
+ fs.writeFileSync(logPath + ".1", "prior predecessor");
70
+
71
+ logger.log("after first rotation");
72
+ logger.flushLogger();
73
+ const firstCurrent = fs.readFileSync(logPath, "utf8");
74
+ const firstPredecessorFirstByte = fs.readFileSync(logPath + ".1")[0];
75
+
76
+ // Switch paths once so the logger rechecks the primary path after this
77
+ // external replacement, as a different plugin process would require.
78
+ fs.writeFileSync(logPath, Buffer.alloc(maxLogBytes, "n"));
79
+ process.env.MAGIC_CONTEXT_LOG_PATH = path.join(root, "alternate.log");
80
+ logger.log("alternate path");
81
+ logger.flushLogger();
82
+ process.env.MAGIC_CONTEXT_LOG_PATH = logPath;
83
+ logger.log("after second rotation");
84
+ logger.flushLogger();
85
+
86
+ const predecessorPath = logPath + ".1";
87
+ console.log(JSON.stringify({
88
+ firstCurrent,
89
+ firstPredecessorFirstByte,
90
+ current: fs.readFileSync(logPath, "utf8"),
91
+ predecessorFirstByte: fs.readFileSync(predecessorPath)[0],
92
+ predecessorSize: fs.statSync(predecessorPath).size,
93
+ currentSize: fs.statSync(logPath).size,
94
+ currentMode: fs.statSync(logPath).mode & 0o777,
95
+ predecessorMode: fs.statSync(predecessorPath).mode & 0o777,
96
+ files: fs.readdirSync(path.dirname(logPath)).filter((name) => name.startsWith("magic-context.log")).sort(),
97
+ }));
98
+ } else if (scenario === "hot-path") {
99
+ for (let index = 0; index < 20; index++) {
100
+ logger.log("hot-path-" + index);
101
+ logger.flushLogger();
102
+ }
103
+ console.log(JSON.stringify({ statCalls }));
104
+ } else {
105
+ logger.log("first");
106
+ logger.log(
107
+ "tokens.input=45000 api_key=sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGH",
108
+ { authorization: "Bearer abcdefghijklmnop", totalInputTokens: 132000 },
109
+ );
110
+ logger.flushLogger();
111
+ const healthyDiagnostics = logger.getLoggerDiagnostics();
112
+
113
+ const logDirectory = path.dirname(logPath);
114
+ if (scenario === "recovery") {
115
+ fs.rmSync(logDirectory, { recursive: true, force: true });
116
+ logger.log("second");
117
+ logger.flushLogger();
118
+ }
119
+
120
+ // A directory used as the file target makes append fail deterministically on every platform.
121
+ const failedPath = path.join(root, "unwritable-log-target");
122
+ fs.mkdirSync(failedPath, { recursive: true });
123
+ process.env.MAGIC_CONTEXT_LOG_PATH = failedPath;
124
+ logger.log("failed write");
125
+ logger.flushLogger();
126
+ const failedDiagnostics = logger.getLoggerDiagnostics();
127
+
128
+ const content = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
129
+ console.log(JSON.stringify({
130
+ exists: fs.existsSync(logPath),
131
+ content,
132
+ healthyDiagnostics,
133
+ failedDiagnostics,
134
+ }));
135
+ }
60
136
  `;
61
137
 
62
138
  const scenarioRoots: string[] = [];
@@ -67,9 +143,9 @@ afterEach(() => {
67
143
  }
68
144
  });
69
145
 
70
- async function runLoggerScenario(
71
- scenario: "recovery" | "diagnostics",
72
- ): Promise<LoggerScenarioResult> {
146
+ async function runLoggerScenario<T>(
147
+ scenario: "recovery" | "diagnostics" | "rotation" | "hot-path",
148
+ ): Promise<T> {
73
149
  const root = mkdtempSync(path.join(os.tmpdir(), "magic-context-logger-test-"));
74
150
  scenarioRoots.push(root);
75
151
  const child = Bun.spawn({
@@ -91,19 +167,53 @@ async function runLoggerScenario(
91
167
  new Response(child.stderr).text(),
92
168
  ]);
93
169
  expect(exitCode, stderr).toBe(0);
94
- return JSON.parse(stdout.trim()) as LoggerScenarioResult;
170
+ return JSON.parse(stdout.trim()) as T;
95
171
  }
96
172
 
97
173
  describe("logger", () => {
98
174
  test("recreates a log directory removed while the process is running", async () => {
99
- const result = await runLoggerScenario("recovery");
175
+ const result = await runLoggerScenario<LoggerScenarioResult>("recovery");
100
176
 
101
177
  expect(result.exists).toBe(true);
102
178
  expect(result.content).toContain("second");
103
179
  });
104
180
 
181
+ test("redacts secrets while preserving numeric diagnostics", async () => {
182
+ const result = await runLoggerScenario<LoggerScenarioResult>("diagnostics");
183
+
184
+ expect(result.content).toContain("tokens.input=45000");
185
+ expect(result.content).toContain('"totalInputTokens":132000');
186
+ expect(result.content).toContain("<REDACTED:api_key>");
187
+ expect(result.content).toContain("<REDACTED:authorization>");
188
+ expect(result.content).not.toContain("abcdefghijklmnopqrstuvwxyzABCDEFGH");
189
+ expect(result.content).not.toContain("abcdefghijklmnop");
190
+ });
191
+
192
+ test("rotates at the 32 MiB bound with one private predecessor", async () => {
193
+ const result = await runLoggerScenario<RotationScenarioResult>("rotation");
194
+ const maxLogBytes = 32 * 1024 * 1024;
195
+
196
+ expect(result.firstCurrent).toContain("after first rotation");
197
+ expect(result.firstPredecessorFirstByte).toBe("o".charCodeAt(0));
198
+ expect(result.current).toContain("after second rotation");
199
+ expect(result.predecessorFirstByte).toBe("n".charCodeAt(0));
200
+ expect(result.predecessorSize).toBeLessThanOrEqual(maxLogBytes);
201
+ expect(result.currentSize).toBeLessThanOrEqual(maxLogBytes);
202
+ expect(result.files).toEqual(["magic-context.log", "magic-context.log.1"]);
203
+ if (process.platform !== "win32") {
204
+ expect(result.currentMode).toBe(0o600);
205
+ expect(result.predecessorMode).toBe(0o600);
206
+ }
207
+ });
208
+
209
+ test("uses cached size between periodic rotation checks", async () => {
210
+ const result = await runLoggerScenario<HotPathScenarioResult>("hot-path");
211
+
212
+ expect(result.statCalls).toBeLessThanOrEqual(1);
213
+ });
214
+
105
215
  test("reports swallowed writes while healthy writes leave the counter at zero", async () => {
106
- const result = await runLoggerScenario("diagnostics");
216
+ const result = await runLoggerScenario<LoggerScenarioResult>("diagnostics");
107
217
 
108
218
  expect(result.healthyDiagnostics).toEqual({
109
219
  swallowedWriteCount: 0,
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getMagicContextLogPath } from "./data-path";
4
+ import { sanitizeConfigValue, sanitizeDiagnosticText } from "./redaction";
4
5
 
5
6
  const isTestEnv = process.env.NODE_ENV === "test";
6
7
 
@@ -8,6 +9,12 @@ let buffer: string[] = [];
8
9
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
9
10
  const FLUSH_INTERVAL_MS = 500;
10
11
  const BUFFER_SIZE_LIMIT = 50;
12
+ const MAX_LOG_FILE_BYTES = 32 * 1024 * 1024;
13
+ const SIZE_CHECK_INTERVAL_FLUSHES = 64;
14
+
15
+ let activeLogFile: string | null = null;
16
+ let activeLogSize: number | null = null;
17
+ let flushesSinceSizeCheck = 0;
11
18
 
12
19
  export interface LoggerDiagnostics {
13
20
  swallowedWriteCount: number;
@@ -22,7 +29,9 @@ let lastErrorTime: string | null = null;
22
29
  function recordSwallowedWrite(error: unknown): void {
23
30
  try {
24
31
  swallowedWriteCount++;
25
- lastErrorMessage = error instanceof Error ? error.message : String(error);
32
+ lastErrorMessage = sanitizeDiagnosticText(
33
+ error instanceof Error ? error.message : String(error),
34
+ );
26
35
  lastErrorTime = new Date().toISOString();
27
36
  } catch {
28
37
  // Diagnostics must not make the logger throw either.
@@ -33,19 +42,126 @@ function ensureDir(filePath: string): void {
33
42
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
34
43
  }
35
44
 
45
+ function isMissingFile(error: unknown): boolean {
46
+ return (
47
+ typeof error === "object" &&
48
+ error !== null &&
49
+ "code" in error &&
50
+ (error as { code?: unknown }).code === "ENOENT"
51
+ );
52
+ }
53
+
54
+ function getCurrentLogSize(logFile: string): number {
55
+ if (
56
+ activeLogFile === logFile &&
57
+ activeLogSize !== null &&
58
+ flushesSinceSizeCheck < SIZE_CHECK_INTERVAL_FLUSHES
59
+ ) {
60
+ return activeLogSize;
61
+ }
62
+
63
+ try {
64
+ const stat = fs.statSync(logFile);
65
+ if (!stat.isFile()) {
66
+ throw new Error(`Magic Context log path is not a regular file: ${logFile}`);
67
+ }
68
+ // Logs can contain diagnostic context, so tighten files created by older
69
+ // versions before copying or appending any new content.
70
+ fs.chmodSync(logFile, 0o600);
71
+ activeLogFile = logFile;
72
+ activeLogSize = stat.size;
73
+ flushesSinceSizeCheck = 0;
74
+ return stat.size;
75
+ } catch (error) {
76
+ if (!isMissingFile(error)) throw error;
77
+ activeLogFile = logFile;
78
+ activeLogSize = 0;
79
+ flushesSinceSizeCheck = 0;
80
+ return 0;
81
+ }
82
+ }
83
+
84
+ function capLogData(data: string): string {
85
+ if (Buffer.byteLength(data) <= MAX_LOG_FILE_BYTES) return data;
86
+
87
+ // A single diagnostic payload must not defeat the on-disk bound. Cutting a
88
+ // Buffer can split UTF-8, so trim any replacement character expansion too.
89
+ let bounded = Buffer.from(data).subarray(0, MAX_LOG_FILE_BYTES).toString("utf8");
90
+ while (Buffer.byteLength(bounded) > MAX_LOG_FILE_BYTES) {
91
+ bounded = bounded.slice(0, -1);
92
+ }
93
+ return bounded;
94
+ }
95
+
96
+ function writeBoundedPredecessor(logFile: string, predecessorPath: string, size: number): void {
97
+ const predecessorFd = fs.openSync(predecessorPath, "w", 0o600);
98
+ try {
99
+ // `openSync(..., "w")` preserves an existing file's mode, so set it
100
+ // before any copied diagnostic text reaches the predecessor.
101
+ fs.fchmodSync(predecessorFd, 0o600);
102
+ const bytesToCopy = Math.min(size, MAX_LOG_FILE_BYTES);
103
+ const sourceFd = fs.openSync(logFile, "r");
104
+ try {
105
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, bytesToCopy));
106
+ let remaining = bytesToCopy;
107
+ let position = Math.max(0, size - bytesToCopy);
108
+ while (remaining > 0) {
109
+ const bytesRead = fs.readSync(
110
+ sourceFd,
111
+ chunk,
112
+ 0,
113
+ Math.min(chunk.length, remaining),
114
+ position,
115
+ );
116
+ if (bytesRead === 0) break;
117
+ fs.writeSync(predecessorFd, chunk, 0, bytesRead);
118
+ remaining -= bytesRead;
119
+ position += bytesRead;
120
+ }
121
+ } finally {
122
+ fs.closeSync(sourceFd);
123
+ }
124
+ } finally {
125
+ fs.closeSync(predecessorFd);
126
+ }
127
+ }
128
+
129
+ function rotateLogFile(logFile: string, size: number): void {
130
+ const predecessorPath = `${logFile}.1`;
131
+ writeBoundedPredecessor(logFile, predecessorPath, size);
132
+ // Keep the current inode in place so diagnostic readers can observe a
133
+ // shorter log during rotation instead of seeing the path disappear.
134
+ fs.truncateSync(logFile, 0);
135
+ activeLogSize = 0;
136
+ flushesSinceSizeCheck = 0;
137
+ }
138
+
36
139
  function flush(): void {
37
140
  if (flushTimer) {
38
141
  clearTimeout(flushTimer);
39
142
  flushTimer = null;
40
143
  }
41
144
  if (buffer.length === 0) return;
42
- const data = buffer.join("");
145
+ const bufferedData = buffer.join("");
43
146
  buffer = [];
44
147
  try {
148
+ const data = capLogData(bufferedData);
45
149
  const logFile = getMagicContextLogPath();
46
150
  ensureDir(logFile);
47
- fs.appendFileSync(logFile, data);
151
+ let currentSize = getCurrentLogSize(logFile);
152
+ const dataSize = Buffer.byteLength(data);
153
+ if (currentSize > 0 && currentSize + dataSize > MAX_LOG_FILE_BYTES) {
154
+ rotateLogFile(logFile, currentSize);
155
+ currentSize = 0;
156
+ }
157
+ fs.appendFileSync(logFile, data, { encoding: "utf8", mode: 0o600 });
158
+ activeLogFile = logFile;
159
+ activeLogSize = currentSize + dataSize;
160
+ flushesSinceSizeCheck++;
48
161
  } catch (error) {
162
+ activeLogFile = null;
163
+ activeLogSize = null;
164
+ flushesSinceSizeCheck = 0;
49
165
  recordSwallowedWrite(error);
50
166
  }
51
167
  }
@@ -66,9 +182,11 @@ export function log(message: string, data?: unknown): void {
66
182
  data === undefined
67
183
  ? ""
68
184
  : data instanceof Error
69
- ? ` ${data.message}${data.stack ? `\n${data.stack}` : ""}`
70
- : ` ${JSON.stringify(data)}`;
71
- buffer.push(`[${timestamp}] ${message}${serialized}\n`);
185
+ ? ` ${sanitizeDiagnosticText(
186
+ `${data.message}${data.stack ? `\n${data.stack}` : ""}`,
187
+ )}`
188
+ : ` ${JSON.stringify(sanitizeConfigValue(data))}`;
189
+ buffer.push(`[${timestamp}] ${sanitizeDiagnosticText(message)}${serialized}\n`);
72
190
  if (buffer.length >= BUFFER_SIZE_LIMIT) {
73
191
  flush();
74
192
  } else {
@@ -4,10 +4,29 @@ import {
4
4
  normalizeModelEntry,
5
5
  resolveDreamerTaskModel,
6
6
  resolveFallbackEntries,
7
+ resolveHistorianAgentOverrides,
7
8
  resolveHistorianModel,
8
9
  } from "./model-resolution";
9
10
 
10
11
  describe("model-resolution", () => {
12
+ test("keeps historian temperature opt-in while preserving explicit overrides", () => {
13
+ expect(resolveHistorianAgentOverrides(undefined)).toEqual({
14
+ maxTokens: 32_000,
15
+ });
16
+ expect(
17
+ resolveHistorianAgentOverrides({
18
+ temperature: 0.2,
19
+ maxTokens: 16_000,
20
+ opencode: { model: "google/flash", variant: "low" },
21
+ }),
22
+ ).toEqual({
23
+ temperature: 0.2,
24
+ maxTokens: 16_000,
25
+ model: "google/flash",
26
+ variant: "low",
27
+ });
28
+ });
29
+
11
30
  test("normalizes string and object entries to the same model identity", () => {
12
31
  expect(normalizeModelEntry("anthropic/sonnet", "opencode")).toEqual({
13
32
  model: "anthropic/sonnet",
@@ -36,6 +55,24 @@ describe("model-resolution", () => {
36
55
  ]);
37
56
  });
38
57
 
58
+ test("keeps the flash calibration available only as an explicit override", () => {
59
+ expect(
60
+ resolveHistorianAgentOverrides({
61
+ opencode: { model: { model: "google/flash", variant: "fast" } },
62
+ pi: { model: "pi/ignored" },
63
+ two_pass: true,
64
+ }),
65
+ ).toEqual({
66
+ maxTokens: 32_000,
67
+ two_pass: true,
68
+ model: "google/flash",
69
+ variant: "fast",
70
+ });
71
+ expect(
72
+ resolveHistorianAgentOverrides({ temperature: 0.1, maxTokens: 12_000 }),
73
+ ).toMatchObject({ temperature: 0.1, maxTokens: 12_000 });
74
+ });
75
+
39
76
  test("does not inherit a block qualifier into unqualified fallbacks", () => {
40
77
  const config = {
41
78
  historian: {
@@ -225,3 +225,10 @@ export function resolveOpenCodeAgentOverrides(agent: unknown): Record<string, un
225
225
  ...(primary?.qualifier ? { variant: primary.qualifier } : {}),
226
226
  };
227
227
  }
228
+
229
+ export function resolveHistorianAgentOverrides(agent: unknown): Record<string, unknown> {
230
+ return {
231
+ maxTokens: 32_000,
232
+ ...resolveOpenCodeAgentOverrides(agent),
233
+ };
234
+ }
@@ -108,7 +108,7 @@ export function isSecretKey(key: string): boolean {
108
108
  }
109
109
 
110
110
  export function sanitizePathString(value: string): string {
111
- const home = homedir();
111
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
112
112
  const username = userInfo().username;
113
113
  let sanitized = value;
114
114
  if (home) {
@@ -132,7 +132,7 @@ const SECRET_TEXT_PATTERNS: Array<{
132
132
  replacement: "<ANTHROPIC_API_KEY_REDACTED>",
133
133
  },
134
134
  {
135
- pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}/g,
135
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{12,}/g,
136
136
  replacement: "<OPENAI_API_KEY_REDACTED>",
137
137
  },
138
138
  {
@@ -141,12 +141,17 @@ export interface SidebarSnapshot {
141
141
  }
142
142
 
143
143
  export interface StatusDetail extends SidebarSnapshot {
144
+ /** True when Rust authority has rerouted host tool and historian paths to the module. */
145
+ hostBackendsModuleSide?: boolean;
144
146
  /** User-owned model profile selected for this project, or null for the base config. */
145
147
  activeProfile: string | null;
146
148
  tagCounter: number;
147
149
  activeTags: number;
148
150
  droppedTags: number;
149
151
  totalTags: number;
152
+ /** False when Rust authority supplies only the exact total; active/dropped host-mirror
153
+ * counts are not presented as module truth. Omitted by older RPC servers means true. */
154
+ tagCountsAuthoritative?: boolean;
150
155
  activeBytes: number;
151
156
  lastResponseTime: number;
152
157
  lastNudgeTokens: number;
@@ -0,0 +1,3 @@
1
+ /** User-visible paths that move out of the host when Rust authority is active. */
2
+ export const RUST_MODE_HOST_PATHS_LINE =
3
+ "Host backends → MODULE: ctx_memory, ctx_note; historian: module-side";
@@ -79,4 +79,17 @@ describe("formatStatusDetailMarkdown", () => {
79
79
  - **Tags:** 4 active, 1 dropped; 2 pending drops
80
80
  - **Execute threshold:** 65.0%`);
81
81
  });
82
+
83
+ test("shows module-routed host paths only in Rust-mode chat fallback", () => {
84
+ const rustStatus = formatStatusDetailMarkdown({
85
+ ...STATUS_FIXTURE,
86
+ hostBackendsModuleSide: true,
87
+ });
88
+ const tsStatus = formatStatusDetailMarkdown(STATUS_FIXTURE);
89
+
90
+ expect(rustStatus).toContain(
91
+ "Host backends → MODULE: ctx_memory, ctx_note; historian: module-side",
92
+ );
93
+ expect(tsStatus).not.toContain("Host backends → MODULE");
94
+ });
82
95
  });
@@ -1,4 +1,5 @@
1
1
  import type { StatusDetail } from "./rpc-types";
2
+ import { RUST_MODE_HOST_PATHS_LINE } from "./rust-mode-status";
2
3
 
3
4
  function formatCount(value: number): string {
4
5
  return Math.round(value).toLocaleString();
@@ -39,6 +40,7 @@ export function formatStatusDetailMarkdown(detail: StatusDetail): string {
39
40
  `- **Usage:** ${detail.usagePercentage.toFixed(1)}% (${formatCount(detail.inputTokens)} / ${usableLimit})`,
40
41
  `- **Cache lane:** ${formatCacheLane(detail)}`,
41
42
  `- **Historian:** ${[historianState, ...historianDetails].join("; ")}`,
43
+ ...(detail.hostBackendsModuleSide ? [`- ${RUST_MODE_HOST_PATHS_LINE}`] : []),
42
44
  `- **Memory:** ${formatCount(detail.memoryCount)} active; ${formatCount(detail.memoryBlockCount)} injected`,
43
45
  `- **Tags:** ${formatCount(detail.activeTags)} active, ${formatCount(detail.droppedTags)} dropped; ${formatCount(detail.pendingOpsCount)} pending drops`,
44
46
  `- **Execute threshold:** ${detail.executeThreshold.toFixed(1)}%${detail.executeThresholdClamped ? " (clamped)" : ""}`,
@@ -67,6 +67,10 @@ export interface SubagentRunOptions {
67
67
  model?: string | undefined;
68
68
  fallbackModels?: readonly ModelInput[];
69
69
  timeoutMs?: number | undefined;
70
+ /** Sampling temperature for the provider request when the harness can enforce it. */
71
+ temperature?: number | undefined;
72
+ /** Requested output-token budget for the provider request. */
73
+ maxOutputTokens?: number | undefined;
70
74
  cwd?: string | undefined;
71
75
  signal?: AbortSignal | undefined;
72
76
  /**
@@ -0,0 +1,57 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, utimesSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ cleanupTestTempDir,
6
+ createTestTempDir,
7
+ sweepStaleTestTempDirs,
8
+ withTestTempDir,
9
+ } from "./test-temp-dir";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ afterEach(() => {
14
+ for (const directory of tempDirs.splice(0)) cleanupTestTempDir(directory);
15
+ });
16
+
17
+ function createFixtureRoot(): string {
18
+ const { dir } = createTestTempDir("mc-test-temp-dir-helper-");
19
+ tempDirs.push(dir);
20
+ return dir;
21
+ }
22
+
23
+ describe("test temp directories", () => {
24
+ it("removes a root when its fixture callback throws", () => {
25
+ let directory = "";
26
+
27
+ expect(() =>
28
+ withTestTempDir("mc-test-temp-dir-helper-", (tempDir) => {
29
+ directory = tempDir;
30
+ throw new Error("fixture setup failed");
31
+ }),
32
+ ).toThrow("fixture setup failed");
33
+
34
+ expect(existsSync(directory)).toBe(false);
35
+ });
36
+
37
+ it("sweeps only aged directories with recognized prefixes", () => {
38
+ const root = createFixtureRoot();
39
+ const staleRecognized = join(root, "mc-config-secret-stale");
40
+ const freshRecognized = join(root, "mc-config-secret-fresh");
41
+ const staleBystander = join(root, "unrelated-tool-stale");
42
+ const nowMs = Date.now();
43
+
44
+ for (const directory of [staleRecognized, freshRecognized, staleBystander]) {
45
+ mkdirSync(directory);
46
+ }
47
+ utimesSync(staleRecognized, new Date(nowMs - 7_200_000), new Date(nowMs - 7_200_000));
48
+ utimesSync(staleBystander, new Date(nowMs - 7_200_000), new Date(nowMs - 7_200_000));
49
+
50
+ const removed = sweepStaleTestTempDirs({ tempDir: root, nowMs });
51
+
52
+ expect(removed).toEqual([staleRecognized]);
53
+ expect(existsSync(staleRecognized)).toBe(false);
54
+ expect(existsSync(freshRecognized)).toBe(true);
55
+ expect(existsSync(staleBystander)).toBe(true);
56
+ });
57
+ });