@cortexkit/opencode-magic-context 0.33.1 → 0.34.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 (240) hide show
  1. package/README.md +20 -3
  2. package/dist/config/agent-disable.d.ts +22 -0
  3. package/dist/config/agent-disable.d.ts.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/migrate-experimental.d.ts +2 -1
  6. package/dist/config/migrate-experimental.d.ts.map +1 -1
  7. package/dist/config/project-security.d.ts +9 -0
  8. package/dist/config/project-security.d.ts.map +1 -1
  9. package/dist/config/schema/magic-context.d.ts +90 -17
  10. package/dist/config/schema/magic-context.d.ts.map +1 -1
  11. package/dist/config/transform-mode.d.ts +2 -0
  12. package/dist/config/transform-mode.d.ts.map +1 -1
  13. package/dist/{conflict-warning-hook-n48c783v.js → conflict-warning-hook-fvpfvt0w.js} +1 -1
  14. package/dist/{derive-budgets-e9j09sjr.js → derive-budgets-9crp0cvd.js} +1 -1
  15. package/dist/features/builtin-commands/commands.d.ts +1 -1
  16. package/dist/features/builtin-commands/commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/compaction-marker.d.ts +57 -0
  18. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  19. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  20. package/dist/features/magic-context/dreamer/classify.d.ts +1 -0
  21. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  22. package/dist/features/magic-context/dreamer/evaluate-smart-notes.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/map-memories.d.ts +1 -0
  24. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  25. package/dist/features/magic-context/dreamer/refresh-primers.d.ts +1 -0
  26. package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
  27. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +2 -0
  28. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  29. package/dist/features/magic-context/dreamer/task-executor.d.ts +4 -1
  30. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  31. package/dist/features/magic-context/dreamer/task-gates.d.ts +19 -1
  32. package/dist/features/magic-context/dreamer/task-gates.d.ts.map +1 -1
  33. package/dist/features/magic-context/dreamer/task-registry.d.ts +28 -0
  34. package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
  35. package/dist/features/magic-context/dreamer/task-scheduler.d.ts +7 -1
  36. package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
  37. package/dist/features/magic-context/dreamer/verify.d.ts +1 -0
  38. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  39. package/dist/features/magic-context/fail-closed-block.d.ts +10 -0
  40. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  41. package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
  42. package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
  43. package/dist/features/magic-context/memory/embedding-identity.d.ts.map +1 -1
  44. package/dist/features/magic-context/memory/embedding-local.d.ts +7 -1
  45. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  46. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  47. package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
  48. package/dist/features/magic-context/memory/project-identity.d.ts +2 -6
  49. package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
  50. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  51. package/dist/features/magic-context/migrations.d.ts +23 -1
  52. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  53. package/dist/features/magic-context/mural/compress-cues.d.ts +1 -0
  54. package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
  55. package/dist/features/magic-context/mural/render-mural.d.ts.map +1 -1
  56. package/dist/features/magic-context/overflow-detection.d.ts +8 -1
  57. package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
  58. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  59. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  60. package/dist/features/magic-context/schema-fence-probe.d.ts +28 -0
  61. package/dist/features/magic-context/schema-fence-probe.d.ts.map +1 -0
  62. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  63. package/dist/features/magic-context/smart-notes/compiler.d.ts.map +1 -1
  64. package/dist/features/magic-context/storage-db.d.ts +37 -2
  65. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  66. package/dist/features/magic-context/storage-meta-persisted.d.ts +38 -4
  67. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  68. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  69. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  70. package/dist/features/magic-context/storage.d.ts +2 -2
  71. package/dist/features/magic-context/storage.d.ts.map +1 -1
  72. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/boundary-execution.d.ts +1 -1
  74. package/dist/hooks/magic-context/boundary-execution.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/cache-busting-signals.d.ts +1 -0
  76. package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/child-session-spawn.d.ts +26 -0
  78. package/dist/hooks/magic-context/child-session-spawn.d.ts.map +1 -0
  79. package/dist/hooks/magic-context/command-handler.d.ts +3 -0
  80. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/compaction-off-transition.d.ts +96 -0
  82. package/dist/hooks/magic-context/compaction-off-transition.d.ts.map +1 -0
  83. package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -0
  84. package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  86. package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
  87. package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  89. package/dist/hooks/magic-context/compartment-trigger.d.ts +12 -5
  90. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts +47 -1
  92. package/dist/hooks/magic-context/ctx-reduce-availability.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/emergency-drop.d.ts +2 -2
  94. package/dist/hooks/magic-context/event-handler.d.ts +7 -0
  95. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  96. package/dist/hooks/magic-context/event-resolvers.d.ts +13 -21
  97. package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
  98. package/dist/hooks/magic-context/execute-status.d.ts +5 -1
  99. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  100. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  101. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -1
  103. package/dist/hooks/magic-context/hook-handlers.d.ts +1 -0
  104. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  105. package/dist/hooks/magic-context/hook.d.ts +14 -6
  106. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  107. package/dist/hooks/magic-context/inject-compartments.d.ts +10 -1
  108. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  109. package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
  110. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  111. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/rust-mode-transform.d.ts +3 -0
  115. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  116. package/dist/hooks/magic-context/send-session-notification.d.ts +20 -1
  117. package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +3 -0
  119. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  120. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +31 -1
  121. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/transform.d.ts +29 -2
  123. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  124. package/dist/{index-56yjaeb2.js → index-2y0ybv5x.js} +11 -1
  125. package/dist/{index-ey1bc14z.js → index-b563sga5.js} +7 -264
  126. package/dist/{index-y1c9tcm3.js → index-cn960ffd.js} +101 -20
  127. package/dist/{index-xyaekybw.js → index-dtas91s5.js} +6 -4
  128. package/dist/index-fcyvznch.js +279 -0
  129. package/dist/{index-63dq2g4d.js → index-ry8rh8cv.js} +138 -24
  130. package/dist/{index-c2gahrks.js → index-x8spt5wy.js} +1682 -893
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +1677 -723
  133. package/dist/plugin/dream-timer.d.ts +4 -2
  134. package/dist/plugin/dream-timer.d.ts.map +1 -1
  135. package/dist/plugin/embedding-bootstrap.d.ts.map +1 -1
  136. package/dist/plugin/hooks/create-session-hooks.d.ts +12 -1
  137. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  138. package/dist/plugin/messages-transform.d.ts +10 -0
  139. package/dist/plugin/messages-transform.d.ts.map +1 -1
  140. package/dist/plugin/rpc-handlers.d.ts +3 -7
  141. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  142. package/dist/plugin/tool-registry.d.ts +8 -0
  143. package/dist/plugin/tool-registry.d.ts.map +1 -1
  144. package/dist/{recomp-orchestrator-2yb2hfh2.js → recomp-orchestrator-k72ejkkh.js} +6 -4
  145. package/dist/send-session-notification-6njqewzk.js +20 -0
  146. package/dist/shared/conflict-detector.d.ts +38 -1
  147. package/dist/shared/conflict-detector.d.ts.map +1 -1
  148. package/dist/shared/conflict-fixer.d.ts +18 -1
  149. package/dist/shared/conflict-fixer.d.ts.map +1 -1
  150. package/dist/shared/context-limit-provenance.d.ts +3 -0
  151. package/dist/shared/context-limit-provenance.d.ts.map +1 -0
  152. package/dist/shared/escalation-bands.d.ts +9 -0
  153. package/dist/shared/escalation-bands.d.ts.map +1 -0
  154. package/dist/shared/format-threshold.d.ts +1 -1
  155. package/dist/shared/models-dev-cache.d.ts +29 -3
  156. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  157. package/dist/shared/rpc-server.d.ts.map +1 -1
  158. package/dist/shared/rpc-types.d.ts +51 -1
  159. package/dist/shared/rpc-types.d.ts.map +1 -1
  160. package/dist/shared/rpc-utils.d.ts +19 -0
  161. package/dist/shared/rpc-utils.d.ts.map +1 -1
  162. package/dist/shared/storage-permissions.d.ts +5 -0
  163. package/dist/shared/storage-permissions.d.ts.map +1 -0
  164. package/dist/shared/tui-runtime-specifiers.d.ts +20 -0
  165. package/dist/shared/tui-runtime-specifiers.d.ts.map +1 -0
  166. package/dist/{storage-meta-session-td6bwtky.js → storage-meta-session-xt41zbw4.js} +2 -1
  167. package/dist/{task-config-5nx3qfz8.js → task-config-gbp7r23g.js} +1 -1
  168. package/dist/tui/compaction-off.d.ts +13 -0
  169. package/dist/tui/compaction-off.d.ts.map +1 -0
  170. package/dist/tui/data/context-db.d.ts.map +1 -1
  171. package/dist/tui-compiled/compaction-off.d.ts +13 -0
  172. package/dist/tui-compiled/compaction-off.d.ts.map +1 -0
  173. package/dist/tui-compiled/data/context-db.d.ts.map +1 -1
  174. package/package.json +6 -1
  175. package/src/agents/dreamer.ts +68 -0
  176. package/src/agents/hidden-agent-registrations.ts +331 -0
  177. package/src/agents/historian.ts +3 -0
  178. package/src/agents/language-directive.test.ts +138 -0
  179. package/src/agents/language-directive.ts +121 -0
  180. package/src/agents/magic-context-prompt.language.test.ts +35 -0
  181. package/src/agents/magic-context-prompt.test.ts +292 -0
  182. package/src/agents/magic-context-prompt.ts +182 -0
  183. package/src/agents/permissions.test.ts +252 -0
  184. package/src/agents/permissions.ts +186 -0
  185. package/src/agents/sidekick.ts +1 -0
  186. package/src/agents/smart-note-compiler.ts +1 -0
  187. package/src/config/agent-disable.ts +106 -0
  188. package/src/config/compaction-accessor-guard.test.ts +82 -0
  189. package/src/config/compaction-backcompat.test.ts +52 -0
  190. package/src/config/index.test.ts +936 -0
  191. package/src/config/index.ts +601 -0
  192. package/src/config/migrate-config-location.test.ts +332 -0
  193. package/src/config/migrate-config-location.ts +635 -0
  194. package/src/config/migrate-dreamer-v2.test.ts +287 -0
  195. package/src/config/migrate-dreamer-v2.ts +336 -0
  196. package/src/config/migrate-experimental.test.ts +41 -0
  197. package/src/config/migrate-experimental.ts +185 -0
  198. package/src/config/project-security.test.ts +388 -0
  199. package/src/config/project-security.ts +611 -0
  200. package/src/config/prune-config-leaf.test.ts +78 -0
  201. package/src/config/prune-config-leaf.ts +68 -0
  202. package/src/config/removed-config-keys.test.ts +116 -0
  203. package/src/config/schema/agent-overrides.ts +45 -0
  204. package/src/config/schema/magic-context.test.ts +359 -0
  205. package/src/config/schema/magic-context.ts +991 -0
  206. package/src/config/transform-mode.test.ts +62 -0
  207. package/src/config/transform-mode.ts +34 -0
  208. package/src/config/variable.test.ts +323 -0
  209. package/src/config/variable.ts +192 -0
  210. package/src/features/magic-context/defaults.ts +1 -0
  211. package/src/features/magic-context/dreamer/cron.ts +243 -0
  212. package/src/features/magic-context/dreamer/task-registry.ts +151 -0
  213. package/src/shared/conflict-detector.test.ts +121 -0
  214. package/src/shared/conflict-detector.ts +50 -3
  215. package/src/shared/conflict-fixer.test.ts +309 -0
  216. package/src/shared/conflict-fixer.ts +82 -5
  217. package/src/shared/context-limit-provenance.ts +6 -0
  218. package/src/shared/escalation-bands.test.ts +23 -0
  219. package/src/shared/escalation-bands.ts +18 -0
  220. package/src/shared/format-threshold.ts +1 -1
  221. package/src/shared/models-dev-cache.test.ts +171 -0
  222. package/src/shared/models-dev-cache.ts +195 -38
  223. package/src/shared/rpc-server.ts +27 -18
  224. package/src/shared/rpc-types.ts +55 -1
  225. package/src/shared/rpc-utils.test.ts +157 -0
  226. package/src/shared/rpc-utils.ts +137 -0
  227. package/src/shared/storage-permissions.ts +20 -0
  228. package/src/shared/tui-runtime-specifiers.ts +32 -0
  229. package/src/tui/compaction-off.test.ts +56 -0
  230. package/src/tui/compaction-off.ts +29 -0
  231. package/src/tui/data/context-db.ts +6 -0
  232. package/src/tui/index.tsx +113 -65
  233. package/src/tui/slots/sidebar-content.tsx +189 -91
  234. package/src/tui/tui-compiled-runtime-imports.test.ts +151 -0
  235. package/src/tui-compiled/compaction-off.ts +29 -0
  236. package/src/tui-compiled/data/context-db.ts +6 -0
  237. package/src/tui-compiled/index.tsx +558 -455
  238. package/src/tui-compiled/slots/sidebar-content.tsx +334 -169
  239. package/dist/send-session-notification-tghq9gnh.js +0 -11
  240. /package/dist/{index-v7fmc1s2.js → index-ygcng9t8.js} +0 -0
@@ -3,13 +3,31 @@
3
3
  * Both sides import these — no SQLite dependency.
4
4
  */
5
5
 
6
+ import type {
7
+ DreamTaskBacklogMap,
8
+ DreamTaskProgress,
9
+ } from "../features/magic-context/dreamer/task-registry";
10
+
6
11
  export interface SidebarSnapshot {
7
12
  sessionId: string;
8
13
  usagePercentage: number;
9
14
  inputTokens: number;
10
15
  contextLimit: number;
16
+ /**
17
+ * Raw wire-input pressure against the resolved model window. This is separate
18
+ * from Magic Context's execute threshold so native-compaction UI never shows
19
+ * a threshold-relative fill percentage.
20
+ */
21
+ native_context_usage_percentage?: number;
22
+ /**
23
+ * Magic Context compaction mode is resolved at startup and sent in the
24
+ * status-detail wire payload; snake_case matches that payload's field naming.
25
+ */
26
+ compaction_enabled?: boolean;
11
27
  systemPromptTokens: number;
12
28
  compartmentCount: number;
29
+ /** Historical compartment rows retained while native compaction owns the window. */
30
+ archivedCompartmentCount?: number;
13
31
  memoryCount: number;
14
32
  memoryBlockCount: number;
15
33
  pendingOpsCount: number;
@@ -18,6 +36,8 @@ export interface SidebarSnapshot {
18
36
  sessionNoteCount: number;
19
37
  readySmartNoteCount: number;
20
38
  cacheTtl: string;
39
+ /** Persistent runtime failure shown directly in the sidebar when non-null. */
40
+ lastTransformError: string | null;
21
41
  lastDreamerRunAt: number | null;
22
42
  projectIdentity: string | null;
23
43
  compartmentTokens: number;
@@ -69,7 +89,7 @@ export interface SidebarSnapshot {
69
89
  executeThreshold: number;
70
90
  /**
71
91
  * True when `executeThreshold` was clamped down from a higher configured value
72
- * (tokens config above 80% × contextLimit, or a percentage above the 80% cap).
92
+ * (tokens config above 90% × contextLimit, or a percentage above the 90% cap).
73
93
  * The sidebar/status dialog append a small marker so the user knows their
74
94
  * configured value was reduced rather than applied verbatim (issue #241).
75
95
  * Absent when no clamp occurred.
@@ -86,6 +106,10 @@ export interface SidebarSnapshot {
86
106
  * sidebar "Recomp"/"Upgrade" progress bar and the /ctx-status dialog. Mirrors
87
107
  * the runtime `RecompProgress` shape from compartment-runner-types.ts.
88
108
  */
109
+ /** Read-only per-task candidate counts; populated by the server RPC. */
110
+ dreamerBacklog?: DreamTaskBacklogMap;
111
+ /** Process-local task progress; absent when no Dreamer task is running. */
112
+ dreamerProgress?: DreamTaskProgress | null;
89
113
  recompProgress?: {
90
114
  /** "recomp" → "Recomp" labels; "upgrade" → "Upgrade" labels. */
91
115
  kind?: "recomp" | "upgrade" | "embed" | "wrapup";
@@ -111,9 +135,20 @@ export interface StatusDetail extends SidebarSnapshot {
111
135
  isSubagent: boolean;
112
136
  pendingOps: Array<{ tagId: number; operation: string }>;
113
137
  contextLimit: number;
138
+ /**
139
+ * Parsed cache TTL in ms. -1 = never expires (cacheTtl "never"; Infinity
140
+ * cannot ride JSON-RPC and 0 would be indistinguishable from unset). The
141
+ * values discriminate without the cacheNeverExpires flag: -1 never /
142
+ * N live. Falsy-value contract: see storage_versions for the precedent.
143
+ */
114
144
  cacheTtlMs: number;
145
+ /** Remaining ms in the idle-TTL window. -1 = never expires; 0 = expired
146
+ * (only meaningful when lastResponseTime > 0); N = live countdown. */
115
147
  cacheRemainingMs: number;
116
148
  cacheExpired: boolean;
149
+ /** True when cacheTtl is "never" — the idle-TTL heuristic is disabled on
150
+ * this lane. Redundant with cacheTtlMs === -1; kept as the readable form. */
151
+ cacheNeverExpires?: boolean;
117
152
  executeThreshold: number;
118
153
  /**
119
154
  * Which config source produced `executeThreshold`. "tokens" means
@@ -135,6 +170,25 @@ export interface StatusDetail extends SidebarSnapshot {
135
170
  toastDurationMs: number;
136
171
  /** One-line status data for the experimental memory mural. */
137
172
  mural?: { present: boolean; ageMs: number | null };
173
+ /**
174
+ * Stable storage-version probe: "which schema is the DB at, which fence does
175
+ * this binary carry". Field names are deliberately snake_case, mirroring the
176
+ * `storage_versions` block of the mc-module status envelope, so fleet probes
177
+ * parse one shape across both surfaces. The module cannot read context.db
178
+ * (the plugin owns it), so this surface supplies the live DB value; the module
179
+ * surface supplies the module-store value instead.
180
+ */
181
+ storage_versions: {
182
+ /**
183
+ * Persisted schema version of context.db (MAX of schema_migrations).
184
+ * null = the version probe FAILED (read threw — broken/unreachable store);
185
+ * 0 = probe succeeded on a fresh DB without a migrations table. Distinct so
186
+ * readers never conflate broken-with-empty (fleet status-surface contract).
187
+ */
188
+ context_db_schema_version: number | null;
189
+ /** Highest context.db schema version this plugin build supports. */
190
+ plugin_supported_version: number;
191
+ };
138
192
  }
139
193
 
140
194
  /** Embedding coverage for `/ctx-embed` status (mirrors getEmbeddingCoverageStatus). */
@@ -0,0 +1,157 @@
1
+ /// <reference types="bun-types" />
2
+
3
+ import { afterEach, describe, expect, test } from "bun:test";
4
+ import type { execFileSync } from "node:child_process";
5
+ import type { readFileSync } from "node:fs";
6
+ import {
7
+ __resetRpcIdentityTestHooks,
8
+ __setRpcIdentityTestHooks,
9
+ isPidIdentityPlausible,
10
+ type RpcPortFileRecord,
11
+ } from "./rpc-utils";
12
+
13
+ const PID = 1234;
14
+ const NOW_MS = 2_000_000;
15
+ const UPTIME_SECONDS = 1_000;
16
+
17
+ function record(startedAt: number): RpcPortFileRecord {
18
+ return { port: 43123, pid: PID, started_at: startedAt };
19
+ }
20
+
21
+ function procStat(startTimeTicks: number): string {
22
+ // After the closing command-name parenthesis, field 3 is `state` and field
23
+ // 22 is the twentieth value in the suffix.
24
+ return `${PID} (opencode) S ${Array.from({ length: 18 }, () => "0").join(" ")} ${startTimeTicks}`;
25
+ }
26
+
27
+ function linuxFiles(files: Record<string, string | Error>): typeof readFileSync {
28
+ return ((path: string | URL) => {
29
+ const value = files[String(path)];
30
+ if (value instanceof Error) throw value;
31
+ if (value === undefined) throw new Error(`unexpected read: ${String(path)}`);
32
+ return value;
33
+ }) as typeof readFileSync;
34
+ }
35
+
36
+ function psOutput(output: string | Error): typeof execFileSync {
37
+ return (() => {
38
+ if (output instanceof Error) throw output;
39
+ return output;
40
+ }) as typeof execFileSync;
41
+ }
42
+
43
+ afterEach(() => {
44
+ __resetRpcIdentityTestHooks();
45
+ });
46
+
47
+ describe("isPidIdentityPlausible", () => {
48
+ test("rejects a reused Linux PID when proc start time is substantially newer", () => {
49
+ const readPaths: string[] = [];
50
+ __setRpcIdentityTestHooks({
51
+ platform: "linux",
52
+ nowMs: () => NOW_MS,
53
+ readFileSync: ((path: string | URL) => {
54
+ readPaths.push(String(path));
55
+ const files = {
56
+ [`/proc/${PID}/stat`]: procStat(10_000),
57
+ "/proc/uptime": `${UPTIME_SECONDS}.0 0.0`,
58
+ };
59
+ return files[String(path) as keyof typeof files];
60
+ }) as typeof readFileSync,
61
+ execFileSync: (() => {
62
+ throw new Error("ps must not run on Linux");
63
+ }) as typeof execFileSync,
64
+ });
65
+
66
+ expect(isPidIdentityPlausible(record(500_000))).toBe(false);
67
+ expect(readPaths).toEqual([`/proc/${PID}/stat`, "/proc/uptime"]);
68
+ });
69
+
70
+ test("accepts a genuine Linux record when the process started no later than the record", () => {
71
+ __setRpcIdentityTestHooks({
72
+ platform: "linux",
73
+ nowMs: () => NOW_MS,
74
+ readFileSync: linuxFiles({
75
+ [`/proc/${PID}/stat`]: procStat(10_000),
76
+ "/proc/uptime": `${UPTIME_SECONDS}.0 0.0`,
77
+ }),
78
+ });
79
+
80
+ // The mocked process start is 1,100,000ms. The 120s tolerance is part of
81
+ // the contract because port-file creation follows process startup.
82
+ expect(isPidIdentityPlausible(record(1_000_000))).toBe(true);
83
+ });
84
+
85
+ test("treats an unreadable Linux start-time probe as live", () => {
86
+ __setRpcIdentityTestHooks({
87
+ platform: "linux",
88
+ readFileSync: linuxFiles({
89
+ [`/proc/${PID}/stat`]: new Error("procfs unavailable"),
90
+ }),
91
+ });
92
+
93
+ expect(isPidIdentityPlausible(record(500_000))).toBe(true);
94
+ });
95
+
96
+ test("uses the legacy Linux command fallback and fails closed on probe errors", () => {
97
+ __setRpcIdentityTestHooks({
98
+ platform: "linux",
99
+ readFileSync: linuxFiles({
100
+ [`/proc/${PID}/cmdline`]: "/usr/sbin/opendkim --config /etc/opendkim.conf",
101
+ }),
102
+ });
103
+ expect(isPidIdentityPlausible(record(0))).toBe(false);
104
+
105
+ __setRpcIdentityTestHooks({
106
+ platform: "linux",
107
+ readFileSync: linuxFiles({
108
+ [`/proc/${PID}/cmdline`]: "/usr/local/bin/opencode serve",
109
+ }),
110
+ });
111
+ expect(isPidIdentityPlausible(record(0))).toBe(true);
112
+
113
+ __setRpcIdentityTestHooks({
114
+ platform: "linux",
115
+ readFileSync: linuxFiles({
116
+ [`/proc/${PID}/cmdline`]: new Error("procfs unavailable"),
117
+ }),
118
+ });
119
+ expect(isPidIdentityPlausible(record(0))).toBe(true);
120
+ });
121
+
122
+ test("uses ps start time and command probes on non-Linux platforms", () => {
123
+ __setRpcIdentityTestHooks({
124
+ platform: "darwin",
125
+ execFileSync: psOutput("Mon Aug 7 00:00:00 1970"),
126
+ });
127
+ expect(
128
+ isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 121_000)),
129
+ ).toBe(false);
130
+
131
+ __setRpcIdentityTestHooks({
132
+ platform: "darwin",
133
+ execFileSync: psOutput("Mon Aug 7 00:00:00 1970"),
134
+ });
135
+ expect(
136
+ isPidIdentityPlausible(record(Date.parse("Mon Aug 7 00:00:00 1970") - 120_000)),
137
+ ).toBe(true);
138
+
139
+ __setRpcIdentityTestHooks({
140
+ platform: "darwin",
141
+ execFileSync: psOutput("/usr/sbin/opendkim -f"),
142
+ });
143
+ expect(isPidIdentityPlausible(record(0))).toBe(false);
144
+
145
+ __setRpcIdentityTestHooks({
146
+ platform: "darwin",
147
+ execFileSync: psOutput("/Applications/OpenCode.app/Contents/MacOS/opencode"),
148
+ });
149
+ expect(isPidIdentityPlausible(record(0))).toBe(true);
150
+
151
+ __setRpcIdentityTestHooks({
152
+ platform: "darwin",
153
+ execFileSync: psOutput(new Error("ps unavailable")),
154
+ });
155
+ expect(isPidIdentityPlausible(record(0))).toBe(true);
156
+ });
157
+ });
@@ -1,4 +1,6 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
 
4
6
  export interface RpcPortFileRecord {
@@ -58,6 +60,141 @@ export function isPidAlive(pid: number): boolean {
58
60
  }
59
61
  }
60
62
 
63
+ const RPC_IDENTITY_SKEW_TOLERANCE_MS = 120_000;
64
+ const LINUX_CLOCK_TICKS_PER_SECOND = 100;
65
+ const PS_PROBE_TIMEOUT_MS = 1_000;
66
+ const OPEN_CODE_COMMAND_MARKERS = ["opencode", "node", "bun", "electron"];
67
+
68
+ let rpcIdentityReadFileSync: typeof readFileSync = readFileSync;
69
+ let rpcIdentityExecFileSync: typeof execFileSync = execFileSync;
70
+ let rpcIdentityPlatform: NodeJS.Platform = process.platform;
71
+ let rpcIdentityNowMs: () => number = () => Date.now();
72
+
73
+ function parseLinuxProcessStartTime(statContent: string, uptimeContent: string): number | null {
74
+ const closingCommandName = statContent.lastIndexOf(")");
75
+ if (closingCommandName < 0) return null;
76
+
77
+ // The fields after the command name begin at field 3 (`state`), so field 22
78
+ // (`starttime`) is index 19 in this suffix. The command name can contain ')',
79
+ // hence the last closing parenthesis rather than the first one is significant.
80
+ const statFields = statContent
81
+ .slice(closingCommandName + 1)
82
+ .trim()
83
+ .split(/\s+/);
84
+ const startTimeTicks = Number(statFields[19]);
85
+ const uptimeSeconds = Number(uptimeContent.trim().split(/\s+/)[0]);
86
+ if (
87
+ !Number.isFinite(startTimeTicks) ||
88
+ startTimeTicks < 0 ||
89
+ !Number.isFinite(uptimeSeconds) ||
90
+ uptimeSeconds < 0
91
+ ) {
92
+ return null;
93
+ }
94
+
95
+ const processStartTime =
96
+ rpcIdentityNowMs() -
97
+ uptimeSeconds * 1_000 +
98
+ (startTimeTicks / LINUX_CLOCK_TICKS_PER_SECOND) * 1_000;
99
+ return Number.isFinite(processStartTime) ? processStartTime : null;
100
+ }
101
+
102
+ function readLinuxProcessStartTime(pid: number): number | null {
103
+ try {
104
+ const statContent = String(rpcIdentityReadFileSync(`/proc/${pid}/stat`, "utf8"));
105
+ const uptimeContent = String(rpcIdentityReadFileSync("/proc/uptime", "utf8"));
106
+ return parseLinuxProcessStartTime(statContent, uptimeContent);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function readPsProcessStartTime(pid: number): number | null {
113
+ try {
114
+ const output = rpcIdentityExecFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
115
+ encoding: "utf8",
116
+ timeout: PS_PROBE_TIMEOUT_MS,
117
+ });
118
+ const processStartTime = Date.parse(String(output).trim());
119
+ return Number.isFinite(processStartTime) ? processStartTime : null;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ function readLinuxProcessCommand(pid: number): string | null {
126
+ try {
127
+ return String(rpcIdentityReadFileSync(`/proc/${pid}/cmdline`, "utf8"));
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function readPsProcessCommand(pid: number): string | null {
134
+ try {
135
+ const output = rpcIdentityExecFileSync("ps", ["-p", String(pid), "-o", "command="], {
136
+ encoding: "utf8",
137
+ timeout: PS_PROBE_TIMEOUT_MS,
138
+ });
139
+ return String(output);
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function commandLooksLikeOpenCode(command: string): boolean {
146
+ const normalized = command.toLowerCase();
147
+ return OPEN_CODE_COMMAND_MARKERS.some((marker) => normalized.includes(marker));
148
+ }
149
+
150
+ /**
151
+ * Verify that a live PID still belongs to the process that wrote a port record.
152
+ *
153
+ * A PID can be reused after its original process exits. On Linux, procfs gives
154
+ * us a process start time without spawning a helper; other platforms use `ps`
155
+ * only on this cold database-open guard path. Legacy records without a start
156
+ * time use a weaker command-name check, and every probe failure stays live so
157
+ * this identity check cannot weaken the migration guard on uncertainty.
158
+ */
159
+ export function isPidIdentityPlausible(record: RpcPortFileRecord): boolean {
160
+ if (!Number.isInteger(record.pid) || record.pid <= 0) return false;
161
+
162
+ if (Number.isFinite(record.started_at) && record.started_at > 0) {
163
+ const processStartTime =
164
+ rpcIdentityPlatform === "linux"
165
+ ? readLinuxProcessStartTime(record.pid)
166
+ : readPsProcessStartTime(record.pid);
167
+ if (processStartTime === null) return true;
168
+ return processStartTime <= record.started_at + RPC_IDENTITY_SKEW_TOLERANCE_MS;
169
+ }
170
+
171
+ const command =
172
+ rpcIdentityPlatform === "linux"
173
+ ? readLinuxProcessCommand(record.pid)
174
+ : readPsProcessCommand(record.pid);
175
+ if (command === null) return true;
176
+ return commandLooksLikeOpenCode(command);
177
+ }
178
+
179
+ export function __setRpcIdentityTestHooks(hooks: {
180
+ readFileSync?: typeof readFileSync;
181
+ execFileSync?: typeof execFileSync;
182
+ platform?: NodeJS.Platform;
183
+ nowMs?: () => number;
184
+ }): void {
185
+ rpcIdentityReadFileSync = hooks.readFileSync ?? readFileSync;
186
+ rpcIdentityExecFileSync = hooks.execFileSync ?? execFileSync;
187
+ rpcIdentityPlatform = hooks.platform ?? process.platform;
188
+ rpcIdentityNowMs = hooks.nowMs ?? (() => Date.now());
189
+ }
190
+
191
+ export function __resetRpcIdentityTestHooks(): void {
192
+ rpcIdentityReadFileSync = readFileSync;
193
+ rpcIdentityExecFileSync = execFileSync;
194
+ rpcIdentityPlatform = process.platform;
195
+ rpcIdentityNowMs = () => Date.now();
196
+ }
197
+
61
198
  export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileRecord | null {
62
199
  const trimmed = content.trim();
63
200
  if (!trimmed) return null;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Process-wide storage permission policy resolved from trusted user config.
3
+ *
4
+ * Storage is shared by OpenCode and Pi, so all writers consult one setting before
5
+ * applying POSIX modes. The default preserves the historical owner-only policy.
6
+ */
7
+ let enforcePrivateStoragePermissions = true;
8
+
9
+ export function setStoragePrivatePermissionEnforcement(enforce: boolean): void {
10
+ enforcePrivateStoragePermissions = enforce;
11
+ }
12
+
13
+ export function shouldEnforcePrivateStoragePermissions(): boolean {
14
+ return enforcePrivateStoragePermissions;
15
+ }
16
+
17
+ /** Test-only reset for suites that exercise both permission policies in one process. */
18
+ export function __resetStoragePrivatePermissionEnforcementForTests(): void {
19
+ enforcePrivateStoragePermissions = true;
20
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The module specifiers the compiled TUI is allowed to resolve through OpenCode's
3
+ * process-wide OpenTUI runtime registry (`opentui:runtime-module:<encoded>`).
4
+ *
5
+ * Single source of truth, shared by `scripts/build-tui.ts` (which rewrites these
6
+ * specifiers during the Solid transform) and
7
+ * `src/tui/tui-compiled-runtime-imports.test.ts` (which verifies every emitted
8
+ * specifier names a real export). Keeping one list means the guard test cannot
9
+ * drift into skipping a specifier the build actually rewrites — a skipped
10
+ * specifier is exactly the silent hole the test exists to close.
11
+ *
12
+ * Lives in `src/shared/` rather than `src/tui/` on purpose: `build-tui.ts` copies
13
+ * every file under `src/tui/` into the shipped `src/tui-compiled/` bundle, and
14
+ * this list is build/test tooling that the runtime bundle must not carry.
15
+ */
16
+ export const TUI_RUNTIME_SPECIFIERS = [
17
+ "@opentui/core",
18
+ "@opentui/core/testing",
19
+ "@opentui/solid",
20
+ "@opentui/solid/components",
21
+ "@opentui/solid/jsx-runtime",
22
+ "@opentui/solid/jsx-dev-runtime",
23
+ "solid-js",
24
+ "solid-js/store",
25
+ ] as const;
26
+
27
+ export type TuiRuntimeSpecifier = (typeof TUI_RUNTIME_SPECIFIERS)[number];
28
+
29
+ /** Virtual module id OpenCode registers for a runtime specifier. */
30
+ export function runtimeModuleId(specifier: string): string {
31
+ return `opentui:runtime-module:${encodeURIComponent(specifier)}`;
32
+ }
@@ -0,0 +1,56 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { SidebarSnapshot } from "../shared/rpc-types";
3
+ import { compactionOffSidebarRows, nativeCompactionContextLabel } from "./compaction-off";
4
+
5
+ function snapshot(overrides: Partial<SidebarSnapshot> = {}): SidebarSnapshot {
6
+ return {
7
+ sessionId: "ses-native-ui",
8
+ usagePercentage: 63.1,
9
+ inputTokens: 41_000,
10
+ contextLimit: 100_000,
11
+ native_context_usage_percentage: 41,
12
+ compaction_enabled: false,
13
+ systemPromptTokens: 0,
14
+ compartmentCount: 12,
15
+ archivedCompartmentCount: 3,
16
+ memoryCount: 5,
17
+ memoryBlockCount: 2,
18
+ pendingOpsCount: 4,
19
+ historianRunning: true,
20
+ compartmentInProgress: true,
21
+ sessionNoteCount: 2,
22
+ readySmartNoteCount: 1,
23
+ cacheTtl: "5m",
24
+ lastDreamerRunAt: null,
25
+ projectIdentity: null,
26
+ compartmentTokens: 0,
27
+ factTokens: 0,
28
+ memoryTokens: 0,
29
+ docsTokens: 0,
30
+ profileTokens: 0,
31
+ conversationTokens: 0,
32
+ toolCallTokens: 0,
33
+ toolDefinitionTokens: 0,
34
+ executeThreshold: 65,
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ test("renders raw input-versus-model usage rather than threshold-relative fill", () => {
40
+ const value = nativeCompactionContextLabel(snapshot());
41
+
42
+ expect(value).toBe("Context: 41.0% · native compaction");
43
+ expect(value).not.toBe("Context: 63.1% · native compaction");
44
+ });
45
+
46
+ test("keeps historical compartments as a static archived row", () => {
47
+ const initialRows = compactionOffSidebarRows(snapshot());
48
+ const activeCountChangedRows = compactionOffSidebarRows(snapshot({ compartmentCount: 99 }));
49
+
50
+ expect(initialRows).toEqual([
51
+ { label: "Memories", value: "5" },
52
+ { label: "Notes", value: "2" },
53
+ { label: "Archived compartments", value: "3" },
54
+ ]);
55
+ expect(activeCountChangedRows).toEqual(initialRows);
56
+ });
@@ -0,0 +1,29 @@
1
+ import type { SidebarSnapshot } from "../shared/rpc-types";
2
+
3
+ export interface CompactionOffSidebarRow {
4
+ label: "Memories" | "Notes" | "Archived compartments";
5
+ value: string;
6
+ }
7
+
8
+ /**
9
+ * Formats context pressure from the current wire input and the model context
10
+ * limit. It deliberately does not use the stored Magic Context threshold
11
+ * percentage, because native compaction acts on the model window instead.
12
+ */
13
+ export function nativeCompactionContextLabel(snapshot: SidebarSnapshot): string {
14
+ if (snapshot.contextLimit <= 0) return "Context: unknown · native compaction";
15
+ const percentage = (snapshot.inputTokens / snapshot.contextLimit) * 100;
16
+ return `Context: ${percentage.toFixed(1)}% · native compaction`;
17
+ }
18
+
19
+ export function compactionOffSidebarRows(snapshot: SidebarSnapshot): CompactionOffSidebarRow[] {
20
+ const rows: CompactionOffSidebarRow[] = [
21
+ { label: "Memories", value: String(snapshot.memoryCount) },
22
+ { label: "Notes", value: String(snapshot.sessionNoteCount) },
23
+ ];
24
+ const archivedCount = snapshot.archivedCompartmentCount ?? 0;
25
+ if (archivedCount > 0) {
26
+ rows.push({ label: "Archived compartments", value: String(archivedCount) });
27
+ }
28
+ return rows;
29
+ }
@@ -65,6 +65,7 @@ const EMPTY_SNAPSHOT: SidebarSnapshot = {
65
65
  sessionNoteCount: 0,
66
66
  readySmartNoteCount: 0,
67
67
  cacheTtl: "5m",
68
+ lastTransformError: null,
68
69
  lastDreamerRunAt: null,
69
70
  projectIdentity: null,
70
71
  compartmentTokens: 0,
@@ -194,6 +195,7 @@ export async function loadStatusDetail(
194
195
  cacheTtlMs: 0,
195
196
  cacheRemainingMs: 0,
196
197
  cacheExpired: false,
198
+ cacheNeverExpires: false,
197
199
  executeThreshold: 65,
198
200
  executeThresholdMode: "percentage",
199
201
  protectedTagCount: 20,
@@ -202,6 +204,10 @@ export async function loadStatusDetail(
202
204
  compressionBudget: null,
203
205
  compressionUsage: null,
204
206
  toastDurationMs: 5000,
207
+ storage_versions: {
208
+ context_db_schema_version: null,
209
+ plugin_supported_version: 0,
210
+ },
205
211
  };
206
212
 
207
213
  if (!rpcClient) return emptyDetail;