ompchamber 1.2.1 → 2.0.0

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 (223) hide show
  1. package/README.md +131 -147
  2. package/dist/client/index.html +4 -2
  3. package/dist/client/static/css/index.5f630421b7.css +2 -0
  4. package/dist/client/static/js/77.0d81c74cf0.js +139 -0
  5. package/dist/client/static/js/async/1k.973e8e21fc.js +2 -0
  6. package/dist/client/static/js/async/{3y.3186ecdd63.js → 3y.3530b824ad.js} +6 -6
  7. package/dist/client/static/js/async/{q.b453193538.js → 4m.928a1dde85.js} +10 -10
  8. package/dist/client/static/js/async/66.5dfa29f957.js +93 -0
  9. package/dist/client/static/js/async/7g.a7814350c3.js +1 -0
  10. package/dist/client/static/js/async/ar.9303e5f2e8.js +2 -0
  11. package/dist/client/static/js/async/{cx.9c8268d542.js → cx.3cb0242a3a.js} +1 -1
  12. package/dist/client/static/js/async/e.93214d622b.js +79 -0
  13. package/dist/client/static/js/async/g.74ff36419f.js +2 -0
  14. package/dist/client/static/js/async/{m.2e1ea3db9e.js.LICENSE.txt → g.74ff36419f.js.LICENSE.txt} +5 -0
  15. package/dist/client/static/js/async/h1.393f066d09.js +1 -0
  16. package/dist/client/static/js/async/jl.874d352278.js +1 -0
  17. package/dist/client/static/js/async/{k.d16cd09ae7.js → k.3210286401.js} +1 -1
  18. package/dist/client/static/js/async/{m.2e1ea3db9e.js → m.eb2e6752e2.js} +3 -3
  19. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +11 -0
  20. package/dist/client/static/js/async/mt.0352e976d8.js +1 -0
  21. package/dist/client/static/js/async/{oc.eb22093f05.js → oc.b73042e8bd.js} +1 -1
  22. package/dist/client/static/js/async/{og.53f068804c.js → og.8eb577893a.js} +1 -1
  23. package/dist/client/static/js/async/px.fc6877924b.js +1 -0
  24. package/dist/client/static/js/async/q.33d6779f98.js +21 -0
  25. package/dist/client/static/js/async/{q5.88b50ffa95.js → q5.97dc1481b6.js} +1 -1
  26. package/dist/client/static/js/async/s.0f395cdda5.js +94 -0
  27. package/dist/client/static/js/async/{tr.2a0d2b7600.js → tr.e878658140.js} +1 -1
  28. package/dist/client/static/js/async/vt.42416b707d.js +1 -0
  29. package/dist/client/static/js/async/x1.da8687159b.js +90 -0
  30. package/dist/client/static/js/async/xe.da8dcc2015.js +2 -0
  31. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +11 -0
  32. package/dist/client/static/js/async/{xn.6360379f71.js → xn.6510c0abe2.js} +2 -2
  33. package/dist/client/static/js/async/yg.35bebd6d0c.js +149 -0
  34. package/dist/client/static/js/async/yv.d99d829136.js +1 -0
  35. package/dist/client/static/js/index.c05535dbde.js +38 -0
  36. package/package.json +3 -3
  37. package/src/client/components/common/ComposerTextarea.tsx +9 -1
  38. package/src/client/components/common/MarkdownRenderer.tsx +50 -12
  39. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +9 -3
  40. package/src/client/components/settings/Modal.tsx +11 -2
  41. package/src/client/components/settings/categories/AppearanceSettings.tsx +7 -30
  42. package/src/client/components/settings/categories/ProviderSettings.tsx +2 -0
  43. package/src/client/components/settings/categories/appearance-settings/ThemeCard.tsx +61 -0
  44. package/src/client/components/settings/categories/appearance-settings/ThemeGrid.tsx +80 -0
  45. package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +1 -1
  46. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +43 -6
  47. package/src/client/components/settings/categories/provider-settings/ModelConfigModal.tsx +54 -54
  48. package/src/client/components/workspace/SearchPanel.tsx +16 -3
  49. package/src/client/components/workspace/btw-panel/AskCard.tsx +11 -6
  50. package/src/client/components/workspace/btw-panel/Messages.tsx +105 -51
  51. package/src/client/components/workspace/btw-panel/index.tsx +124 -45
  52. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +30 -14
  53. package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +10 -1
  54. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +8 -2
  55. package/src/client/components/workspace/chat-timeline/MessageItem.tsx +27 -16
  56. package/src/client/components/workspace/chat-timeline/MessageList.tsx +11 -0
  57. package/src/client/components/workspace/chat-timeline/RunFooter.tsx +8 -0
  58. package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +12 -1
  59. package/src/client/components/workspace/chat-timeline/index.tsx +7 -2
  60. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/QuestionBlock.tsx +42 -15
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.tsx +13 -1
  62. package/src/client/components/workspace/chat-timeline/tool-renderers/extension-dialog/index.tsx +13 -4
  63. package/src/client/components/workspace/editor/index.tsx +11 -2
  64. package/src/client/components/workspace/file-explorer/GitRepoDropdown.tsx +15 -41
  65. package/src/client/components/workspace/file-explorer/index.tsx +86 -16
  66. package/src/client/components/workspace/git-panel/index.tsx +60 -78
  67. package/src/client/components/workspace/terminal-panel/Header.tsx +16 -1
  68. package/src/client/components/workspace/terminal-panel/RealtimeXtermView.tsx +5 -5
  69. package/src/client/components/workspace/terminal-panel/index.tsx +6 -2
  70. package/src/client/components/workspace/terminal-panel/mount.ts +2 -2
  71. package/src/client/data/theme/terminal.ts +50 -52
  72. package/src/client/hooks/chat/btw/index.ts +139 -18
  73. package/src/client/hooks/chat/btw/intercept.ts +8 -8
  74. package/src/client/hooks/chat/btw/mode.ts +111 -16
  75. package/src/client/hooks/chat/composer/index.ts +10 -4
  76. package/src/client/hooks/chat/timeline/access-mode.ts +41 -0
  77. package/src/client/hooks/chat/timeline/ask-frames.test.ts +87 -0
  78. package/src/client/hooks/chat/timeline/ask-frames.ts +20 -1
  79. package/src/client/hooks/chat/timeline/auto-scroll.ts +13 -4
  80. package/src/client/hooks/chat/timeline/index.ts +8 -20
  81. package/src/client/hooks/chat/timeline/queue.ts +66 -9
  82. package/src/client/hooks/chat/timeline/scroll.test.ts +260 -0
  83. package/src/client/hooks/chat/timeline/scroll.ts +93 -20
  84. package/src/client/hooks/chat/timeline/session-load.ts +7 -1
  85. package/src/client/hooks/chat/timeline/session-pagination.test.ts +167 -0
  86. package/src/client/hooks/chat/timeline/session-pagination.ts +37 -11
  87. package/src/client/hooks/settings/providers.ts +34 -3
  88. package/src/client/hooks/ui/open-file.ts +7 -1
  89. package/src/client/hooks/ui/theme.ts +82 -57
  90. package/src/client/hooks/workspace/file-tabs.ts +5 -1
  91. package/src/client/hooks/workspace/repo-scope/index.ts +119 -0
  92. package/src/client/hooks/workspace/repo-scope/store.test.ts +161 -0
  93. package/src/client/hooks/workspace/repo-scope/store.ts +201 -0
  94. package/src/client/hooks/workspace/search-stream.ts +24 -5
  95. package/src/client/main.tsx +6 -0
  96. package/src/client/tailwind.css +19 -56
  97. package/src/server/lib/btw/child.server.ts +41 -0
  98. package/src/server/lib/btw/lifecycle.server.ts +254 -0
  99. package/src/server/lib/btw/prompt.ts +13 -5
  100. package/src/server/lib/btw/registry.server.ts +13 -4
  101. package/src/server/lib/btw/runtime.server.ts +149 -156
  102. package/src/server/lib/btw/schema.server.ts +27 -0
  103. package/src/server/lib/btw/service.server.ts +106 -5
  104. package/src/server/lib/btw/store.server.ts +57 -6
  105. package/src/server/lib/db/schema.ts +4 -0
  106. package/src/server/lib/db/settings-store.test.ts +65 -0
  107. package/src/server/lib/db/settings-store.ts +25 -0
  108. package/src/server/lib/fs/atomic-write.ts +42 -4
  109. package/src/server/lib/fs/config-lock.ts +74 -0
  110. package/src/server/lib/fs/git-ignore.ts +55 -5
  111. package/src/server/lib/fs/ripgrep.ts +119 -0
  112. package/src/server/lib/lifecycle/identity.ts +10 -64
  113. package/src/server/lib/lifecycle/proc/darwin.ts +238 -0
  114. package/src/server/lib/lifecycle/proc/index.ts +38 -0
  115. package/src/server/lib/lifecycle/proc/linux.ts +80 -0
  116. package/src/server/lib/lifecycle/proc/ps.ts +86 -0
  117. package/src/server/lib/lifecycle/proc/types.ts +33 -0
  118. package/src/server/lib/lifecycle/proc/win32.ts +53 -0
  119. package/src/server/lib/models/provider-registry.server.ts +2 -2
  120. package/src/server/lib/models/registry.server.ts +1 -1
  121. package/src/server/lib/omp/config/behavior.ts +17 -17
  122. package/src/server/lib/omp/config/disabled-providers.ts +84 -35
  123. package/src/server/lib/omp/config/document.ts +128 -0
  124. package/src/server/lib/omp/config/extensions.ts +11 -10
  125. package/src/server/lib/omp/config/mcp.ts +4 -57
  126. package/src/server/lib/omp/config/model-overrides.ts +123 -0
  127. package/src/server/lib/omp/config/models-config.ts +92 -0
  128. package/src/server/lib/omp/config/models-validation.ts +159 -0
  129. package/src/server/lib/omp/config/path-scoped.ts +74 -0
  130. package/src/server/lib/omp/config/providers.ts +241 -211
  131. package/src/server/lib/omp/config/roles.ts +6 -14
  132. package/src/server/lib/omp/core/auth-credentials.ts +1 -1
  133. package/src/server/lib/omp/rpc/frame-fold.test.ts +124 -0
  134. package/src/server/lib/omp/rpc/frame-fold.ts +43 -24
  135. package/src/server/lib/omp/rpc/lifecycle.test.ts +41 -0
  136. package/src/server/lib/omp/rpc/manager.ts +21 -21
  137. package/src/server/lib/omp/rpc/pending-ui-dialogs.ts +11 -7
  138. package/src/server/lib/omp/rpc/session-commands.ts +17 -29
  139. package/src/server/lib/omp/rpc/session-registry.ts +5 -66
  140. package/src/server/lib/omp/rpc/web-state.ts +0 -6
  141. package/src/server/lib/omp/session/auto-title.server.test.ts +109 -11
  142. package/src/server/lib/omp/session/auto-title.server.ts +95 -20
  143. package/src/server/lib/omp/session/sidebar-data.server.ts +11 -6
  144. package/src/server/lib/queue/delivery-dispatch.server.test.ts +162 -0
  145. package/src/server/lib/queue/delivery-harness.ts +88 -0
  146. package/src/server/lib/queue/delivery.server.test.ts +78 -149
  147. package/src/server/lib/queue/delivery.server.ts +105 -13
  148. package/src/server/lib/queue/store.server.ts +30 -5
  149. package/src/server/lib/terminal/foreground.test.ts +164 -0
  150. package/src/server/lib/terminal/foreground.ts +34 -6
  151. package/src/server/lib/terminal/history.ts +8 -7
  152. package/src/server/plugins/compress.ts +33 -5
  153. package/src/server/plugins/ssr.ts +14 -7
  154. package/src/server/routes/btw/topics.ts +89 -2
  155. package/src/server/routes/fs/git.ts +5 -6
  156. package/src/server/routes/fs/search.ts +17 -26
  157. package/src/server/routes/sessions/queue.ts +17 -4
  158. package/src/server/routes/settings/index.ts +2 -0
  159. package/src/server/routes/settings/model-override.ts +69 -0
  160. package/src/server/routes/settings/provider-models.ts +35 -12
  161. package/src/server/routes/settings/providers.ts +31 -1
  162. package/src/server/routes/settings/root.ts +8 -3
  163. package/src/shared/lib/chat/btw/client.ts +34 -0
  164. package/src/shared/lib/chat/timeline/tool-phrases.ts +7 -0
  165. package/src/shared/lib/code/shiki-themes.ts +9 -5
  166. package/src/shared/lib/fs/file-tree.ts +17 -12
  167. package/src/shared/lib/markdown/autolink.test.ts +32 -0
  168. package/src/shared/lib/markdown/autolink.ts +50 -0
  169. package/src/shared/lib/markdown/document-urls.test.ts +68 -0
  170. package/src/shared/lib/markdown/document-urls.ts +118 -0
  171. package/src/shared/lib/markdown/marked.ts +27 -15
  172. package/src/shared/lib/markdown/mermaid.ts +2 -2
  173. package/src/shared/lib/models/catalog.ts +1 -1
  174. package/src/shared/lib/omp/session/schema-migrations.ts +30 -0
  175. package/src/shared/lib/omp/session/stream-state.server.test.ts +54 -0
  176. package/src/shared/lib/omp/session/stream-state.server.ts +70 -21
  177. package/src/shared/lib/settings/client.ts +10 -4
  178. package/src/shared/lib/settings/diff.test.ts +31 -0
  179. package/src/shared/lib/settings/diff.ts +37 -0
  180. package/src/shared/lib/theme/catalog.test.ts +168 -0
  181. package/src/shared/lib/theme/catalog.ts +98 -0
  182. package/src/shared/lib/theme/color.ts +88 -0
  183. package/src/shared/lib/theme/css.ts +70 -0
  184. package/src/shared/lib/theme/palettes/chamber.ts +61 -0
  185. package/src/shared/lib/theme/palettes/dark.ts +313 -0
  186. package/src/shared/lib/theme/palettes/light.ts +313 -0
  187. package/src/shared/types/btw.ts +83 -4
  188. package/src/shared/types/index.ts +1 -0
  189. package/src/shared/types/settings/provider.ts +8 -1
  190. package/src/shared/types/settings/state.ts +10 -4
  191. package/src/shared/types/theme.ts +48 -0
  192. package/dist/client/static/css/index.b938f4a693.css +0 -2
  193. package/dist/client/static/js/77.8022a800b5.js +0 -139
  194. package/dist/client/static/js/async/1k.a411fe5b43.js +0 -2
  195. package/dist/client/static/js/async/66.86544ad741.js +0 -93
  196. package/dist/client/static/js/async/7g.db82aca7ba.js +0 -1
  197. package/dist/client/static/js/async/ar.ad5c8822a8.js +0 -2
  198. package/dist/client/static/js/async/e.6443e739a2.js +0 -79
  199. package/dist/client/static/js/async/g.ee41bfed7f.js +0 -2
  200. package/dist/client/static/js/async/h1.d7091cef78.js +0 -1
  201. package/dist/client/static/js/async/jl.4911fa3383.js +0 -1
  202. package/dist/client/static/js/async/mt.853be2b5aa.js +0 -1
  203. package/dist/client/static/js/async/ot.84c71b308f.js +0 -2
  204. package/dist/client/static/js/async/px.783b6f91f2.js +0 -1
  205. package/dist/client/static/js/async/q8.7f1b48e9cd.js +0 -21
  206. package/dist/client/static/js/async/q8.7f1b48e9cd.js.LICENSE.txt +0 -6
  207. package/dist/client/static/js/async/s.aad2ecebc6.js +0 -94
  208. package/dist/client/static/js/async/vt.20406a5604.js +0 -1
  209. package/dist/client/static/js/async/x1.fe8af17016.js +0 -90
  210. package/dist/client/static/js/async/xn.6360379f71.js.LICENSE.txt +0 -6
  211. package/dist/client/static/js/async/yg.5a94e1fc8a.js +0 -149
  212. package/dist/client/static/js/async/yv.2fa22f009a.js +0 -1
  213. package/dist/client/static/js/index.5fe7a2cb64.js +0 -34
  214. package/src/client/components/workspace/btw-panel/Composer.tsx +0 -183
  215. package/src/client/components/workspace/btw-panel/attachments.ts +0 -80
  216. /package/dist/client/static/js/{77.8022a800b5.js.LICENSE.txt → 77.0d81c74cf0.js.LICENSE.txt} +0 -0
  217. /package/dist/client/static/js/async/{g.ee41bfed7f.js.LICENSE.txt → 1k.973e8e21fc.js.LICENSE.txt} +0 -0
  218. /package/dist/client/static/js/async/{3y.3186ecdd63.js.LICENSE.txt → 3y.3530b824ad.js.LICENSE.txt} +0 -0
  219. /package/dist/client/static/js/async/{ot.84c71b308f.js.LICENSE.txt → 4m.928a1dde85.js.LICENSE.txt} +0 -0
  220. /package/dist/client/static/js/async/{q.b453193538.js.LICENSE.txt → ar.9303e5f2e8.js.LICENSE.txt} +0 -0
  221. /package/dist/client/static/js/async/{1k.a411fe5b43.js.LICENSE.txt → q.33d6779f98.js.LICENSE.txt} +0 -0
  222. /package/dist/client/static/js/async/{ar.ad5c8822a8.js.LICENSE.txt → xn.6510c0abe2.js.LICENSE.txt} +0 -0
  223. /package/dist/client/static/js/{index.5fe7a2cb64.js.LICENSE.txt → index.c05535dbde.js.LICENSE.txt} +0 -0
@@ -13,53 +13,19 @@
13
13
  * stranger. Identity therefore comes from the OS command line, so a recycled
14
14
  * PID reads back as `mismatched`.
15
15
  *
16
- * Where `ps` is unavailable (minimal containers) the on-disk `/proc` entry
17
- * still answers the identity question on Linux; on Windows, where neither is
18
- * cheap, identity is reported as `unknown` and callers fall back to liveness.
16
+ * How the OS is asked is per platform and lives in `./probe` — macOS through
17
+ * `libproc`, Linux through `/proc`, Windows through signal 0 alone, the BSDs
18
+ * through `ps`. This module only turns those answers into the states callers
19
+ * reason about.
20
+ *
21
+ * Where identity cannot be read, it is reported as `unknown` and callers fall
22
+ * back to liveness — never treated as confirmation that the PID is ours.
19
23
  */
20
24
 
21
- import fs from 'fs';
25
+ import { processProbe } from '@/server/lib/lifecycle/proc';
22
26
 
23
27
  export type ProcessState = 'dead' | 'matched' | 'mismatched' | 'unknown';
24
28
 
25
- interface ProcessRow {
26
- pid: number;
27
- /** `ps` state column — `Z…` means defunct (exited, awaiting reaping). */
28
- state: string;
29
- command: string;
30
- }
31
-
32
- let cachedTable: { at: number; rows: ProcessRow[] } | null = null;
33
-
34
- /**
35
- * One snapshot of the process table, memoized for a startup decision.
36
- *
37
- * Every question in a port check happens within milliseconds, so a briefly
38
- * cached view is strictly better than spawning `ps` per question.
39
- */
40
- function readProcessTable(maxAgeMs = 250): ProcessRow[] {
41
- if (cachedTable && Date.now() - cachedTable.at < maxAgeMs) return cachedTable.rows;
42
- const rows: ProcessRow[] = [];
43
- try {
44
- const snapshot = Bun.spawnSync({
45
- cmd: ['ps', '-ax', '-o', 'pid=,stat=,command='],
46
- stdout: 'pipe',
47
- stderr: 'ignore',
48
- });
49
- if (snapshot.success) {
50
- for (const line of snapshot.stdout.toString().split('\n')) {
51
- const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
52
- if (!match) continue;
53
- rows.push({ pid: Number(match[1]), state: match[2], command: match[3].trim() });
54
- }
55
- }
56
- } catch {
57
- // No `ps` on PATH: identity falls back to /proc, liveness to signal 0.
58
- }
59
- cachedTable = { at: Date.now(), rows };
60
- return rows;
61
- }
62
-
63
29
  /**
64
30
  * Liveness check. Never throws.
65
31
  *
@@ -71,27 +37,7 @@ function readProcessTable(maxAgeMs = 250): ProcessRow[] {
71
37
  */
72
38
  export function isProcessAlive(pid: number): boolean {
73
39
  if (!Number.isFinite(pid) || pid <= 0) return false;
74
- try {
75
- process.kill(pid, 0);
76
- } catch {
77
- return false;
78
- }
79
- const row = readProcessTable().find((entry) => entry.pid === pid);
80
- return row ? !row.state.startsWith('Z') : true;
81
- }
82
-
83
- /** Command line for a live PID, or null when this platform cannot report one. */
84
- function readProcessCommandLine(pid: number): string | null {
85
- const row = readProcessTable().find((entry) => entry.pid === pid);
86
- if (row) return row.command.length > 0 ? row.command : null;
87
- if (process.platform === 'linux') {
88
- try {
89
- return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim() || null;
90
- } catch {
91
- return null;
92
- }
93
- }
94
- return null;
40
+ return processProbe.isAlive(pid) && !processProbe.isZombie(pid);
95
41
  }
96
42
 
97
43
  /**
@@ -102,7 +48,7 @@ function readProcessCommandLine(pid: number): string | null {
102
48
  */
103
49
  export function getProcessState(pid: number): ProcessState {
104
50
  if (!isProcessAlive(pid)) return 'dead';
105
- const command = readProcessCommandLine(pid);
51
+ const command = processProbe.commandLine(pid);
106
52
  if (command === null) return 'unknown';
107
53
  return command.toLowerCase().includes('ompchamber') ? 'matched' : 'mismatched';
108
54
  }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * macOS process probe: `libproc` + `sysctl`, no subprocess.
8
+ *
9
+ * `ps -ax` answers the same questions, but it costs a process spawn and a full
10
+ * process-table scan per snapshot — measured at ~32 ms here, against ~0.001 ms
11
+ * for the direct calls — and the CLI's `waitForProcessExit` polls liveness every
12
+ * 100 ms. These are the same APIs `ps` itself uses.
13
+ *
14
+ * `sysctl(KERN_PROC_PID)` is the liveness source rather than
15
+ * `proc_pidinfo(PROC_PIDTBSDINFO)`: it answers for a ZOMBIE (which `proc_pidinfo`
16
+ * refuses, returning 0 — verified) and for root-owned PIDs, so one call covers
17
+ * liveness and the zombie distinction for every PID.
18
+ *
19
+ * Every entry point tolerates failure: a PID that vanished between two calls
20
+ * makes the syscall report zero bytes, which is "not alive", and a sandbox that
21
+ * denies the call degrades to signal 0 rather than throwing.
22
+ *
23
+ * The libraries are opened on the FIRST CALL rather than at module load, so
24
+ * merely importing this module — which happens on every platform, whatever
25
+ * `index.ts` dispatches to — cannot fail. See `bindLibs`.
26
+ */
27
+
28
+ import { dlopen, FFIType, ptr } from 'bun:ffi';
29
+ import type { FFIFunction, Library } from 'bun:ffi';
30
+
31
+ import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
32
+
33
+ /** `sysctl` MIB pieces. */
34
+ const CTL_KERN = 1;
35
+ const KERN_PROC = 14;
36
+ const KERN_PROC_PID = 1;
37
+ const KERN_PROCARGS2 = 49;
38
+
39
+ /**
40
+ * `struct kinfo_proc` up to its `p_stat` byte: `extern_proc`'s union (16),
41
+ * `p_vmspace` (8), `p_sigacts` (8), `p_flag` (4). Verified against this
42
+ * platform: a running process reads 2, a zombie reads 5, `launchd` reads 2.
43
+ */
44
+ const KINFO_PROC_BYTES = 648;
45
+ const P_STAT_OFFSET = 36;
46
+ /** Darwin `p_stat` values. */
47
+ const SZOMB = 5;
48
+ const SZOMB_LIKE = new Set([SZOMB, 6 /* SDEAD */]);
49
+
50
+ /** Upper bound on a command line block; the kernel truncates instead. */
51
+ const ARGV_BUFFER_BYTES = 262_144;
52
+ const PATH_BUFFER_BYTES = 4096;
53
+
54
+ /**
55
+ * The two Darwin libraries and their symbols, opened on the first probe call.
56
+ *
57
+ * `dlopen` at module scope runs on EVERY platform that imports this module —
58
+ * `index.ts` picks a probe at load, but the import itself is what executes the
59
+ * call, so the dispatch cannot prevent it. On Linux the Darwin dylibs do not
60
+ * exist, so the import threw (`Failed to open library "libc.dylib"`) and took
61
+ * down every module transitively importing the probe, tests included. Opening
62
+ * them on first use keeps this module importable everywhere, and a host that
63
+ * cannot open them gets the `null` degradation every method here promises.
64
+ */
65
+ const LIBC_SYMBOLS = {
66
+ sysctl: { args: [FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
67
+ } satisfies Record<string, FFIFunction>;
68
+
69
+ const LIBPROC_SYMBOLS = {
70
+ proc_pidpath: { args: [FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
71
+ proc_pidinfo: { args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
72
+ } satisfies Record<string, FFIFunction>;
73
+
74
+ /** The opened Darwin libraries, as used by the probe below. */
75
+ interface DarwinLibs {
76
+ libc: Library<typeof LIBC_SYMBOLS>;
77
+ libproc: Library<typeof LIBPROC_SYMBOLS>;
78
+ }
79
+
80
+ let libs: DarwinLibs | null = null;
81
+ let libsAttempted = false;
82
+
83
+ /** Open the libraries once, and remember a host that cannot as unavailable. */
84
+ function bindLibs(): DarwinLibs | null {
85
+ if (!libsAttempted) {
86
+ libsAttempted = true;
87
+ try {
88
+ libs = {
89
+ libc: dlopen('libc.dylib', LIBC_SYMBOLS),
90
+ libproc: dlopen('/usr/lib/libproc.dylib', LIBPROC_SYMBOLS),
91
+ };
92
+ } catch {
93
+ libs = null;
94
+ }
95
+ }
96
+ return libs;
97
+ }
98
+
99
+ /** `PROC_PIDTBSDINFO` — the flavor whose reply is a `struct proc_bsdinfo`. */
100
+ const PROC_PIDTBSDINFO = 3;
101
+ /** `sizeof(struct proc_bsdinfo)`, verified against this platform. */
102
+ const BSDINFO_BYTES = 136;
103
+ /** `pbi_pid` — the identity check that guards every offset below. */
104
+ const BSDINFO_PID_OFFSET = 12;
105
+ /** `e_tpgid` — the field `ps -o tpgid=` prints. */
106
+ const BSDINFO_TPGID_OFFSET = 112;
107
+
108
+ /**
109
+ * `p_stat` for a live PID, or null when the PID does not exist.
110
+ *
111
+ * A zero-length result is the kernel saying "no such process"; the call itself
112
+ * failing (rc !== 0) is reported as null too, and callers then fall back.
113
+ */
114
+ function procStatus(pid: number): number | null {
115
+ const bound = bindLibs();
116
+ if (bound === null) return null;
117
+ const mib = new Int32Array([CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]);
118
+ const size = new BigUint64Array([BigInt(KINFO_PROC_BYTES)]);
119
+ const buffer = new Uint8Array(KINFO_PROC_BYTES);
120
+ try {
121
+ if (bound.libc.symbols.sysctl(ptr(mib), 4, ptr(buffer), ptr(size), null, 0) !== 0) return null;
122
+ } catch {
123
+ return null;
124
+ }
125
+ if (Number(size[0]) === 0) return null;
126
+ return buffer[P_STAT_OFFSET];
127
+ }
128
+
129
+ /**
130
+ * Scratch for `foregroundGroup` below. One module-level buffer, safe because
131
+ * the whole call is synchronous — no await can interleave another caller.
132
+ */
133
+ const bsdInfo = new Uint8Array(BSDINFO_BYTES);
134
+ const bsdInfoView = new DataView(bsdInfo.buffer);
135
+
136
+ /**
137
+ * Executable path, or null. Also answers for root-owned PIDs (verified: pid 1 →
138
+ * `/sbin/launchd`), which is what makes it the identity fallback when argv is
139
+ * denied.
140
+ */
141
+ function executablePath(pid: number): string | null {
142
+ const bound = bindLibs();
143
+ if (bound === null) return null;
144
+ const buffer = new Uint8Array(PATH_BUFFER_BYTES);
145
+ try {
146
+ const written = bound.libproc.symbols.proc_pidpath(pid, ptr(buffer), buffer.length);
147
+ return written > 0 ? new TextDecoder().decode(buffer.subarray(0, written)) : null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * argv of a live PID via `KERN_PROCARGS2`, or null.
155
+ *
156
+ * The block is laid out as: `argc` (i32), the executable path (NUL-terminated),
157
+ * padding NULs, then `argc` NUL-terminated arguments. Denied for root-owned
158
+ * PIDs, which is why `commandLine` also consults the executable path.
159
+ */
160
+ function argvOf(pid: number): string[] | null {
161
+ const bound = bindLibs();
162
+ if (bound === null) return null;
163
+ const mib = new Int32Array([CTL_KERN, KERN_PROCARGS2, pid]);
164
+ const size = new BigUint64Array([BigInt(ARGV_BUFFER_BYTES)]);
165
+ const buffer = new Uint8Array(ARGV_BUFFER_BYTES);
166
+ try {
167
+ if (bound.libc.symbols.sysctl(ptr(mib), 3, ptr(buffer), ptr(size), null, 0) !== 0) return null;
168
+ } catch {
169
+ return null;
170
+ }
171
+
172
+ const length = Number(size[0]);
173
+ if (length <= 4) return null;
174
+ const argc = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getInt32(0, true);
175
+ let cursor = 4;
176
+ while (cursor < length && buffer[cursor] !== 0) cursor += 1;
177
+ while (cursor < length && buffer[cursor] === 0) cursor += 1;
178
+
179
+ const decoder = new TextDecoder();
180
+ const args: string[] = [];
181
+ for (let index = 0; index < argc && cursor < length; index += 1) {
182
+ let end = cursor;
183
+ while (end < length && buffer[end] !== 0) end += 1;
184
+ args.push(decoder.decode(buffer.subarray(cursor, end)));
185
+ cursor = end + 1;
186
+ }
187
+ return args;
188
+ }
189
+
190
+ function signalAlive(pid: number): boolean {
191
+ try {
192
+ process.kill(pid, 0);
193
+ return true;
194
+ } catch (error) {
195
+ // EPERM: the PID exists, we may not signal it (root-owned, another user).
196
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
197
+ }
198
+ }
199
+
200
+ export const darwinProbe: ProcessProbe = {
201
+ commandLine(pid) {
202
+ const args = argvOf(pid);
203
+ if (args && args.length > 0) return args.join(' ');
204
+ return executablePath(pid);
205
+ },
206
+ isAlive(pid) {
207
+ const status = procStatus(pid);
208
+ if (status !== null) return !SZOMB_LIKE.has(status);
209
+ // The syscall itself failed (sandbox) — liveness alone is all that is left.
210
+ return executablePath(pid) !== null || signalAlive(pid);
211
+ },
212
+ isZombie(pid) {
213
+ const status = procStatus(pid);
214
+ return status !== null && SZOMB_LIKE.has(status);
215
+ },
216
+ /**
217
+ * `e_tpgid` — the field `ps -o tpgid=` prints. `proc_pidinfo` is the cheap
218
+ * route to it (measured ~0.9 µs against ~10 ms for the subprocess).
219
+ *
220
+ * The reply's own `pbi_pid` is checked against the requested pid before
221
+ * `e_tpgid` is trusted: a struct-layout drift on a future macOS would
222
+ * otherwise hand back a garbage group, and a wrong "busy" answer makes the
223
+ * terminal cap refuse an attach. Returning null keeps the caller on `ps`.
224
+ */
225
+ foregroundGroup(pid) {
226
+ const bound = bindLibs();
227
+ if (bound === null) return null;
228
+ let written = 0;
229
+ try {
230
+ written = bound.libproc.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, ptr(bsdInfo), BSDINFO_BYTES);
231
+ } catch {
232
+ return null;
233
+ }
234
+ if (written < BSDINFO_BYTES) return null;
235
+ if (bsdInfoView.getUint32(BSDINFO_PID_OFFSET, true) !== pid) return null;
236
+ return bsdInfoView.getUint32(BSDINFO_TPGID_OFFSET, true);
237
+ },
238
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Which process probe answers on this platform.
8
+ *
9
+ * The choice is by `process.platform`, at module load: macOS gets `libproc`,
10
+ * Linux gets `/proc`, Windows gets the liveness-only probe, and anything else
11
+ * (the BSDs) falls back to `ps`. No caller knows which one it got.
12
+ *
13
+ * macOS and Linux are the native paths because they are the platforms the
14
+ * chamber actually runs on and where a subprocess-free answer exists; the `ps`
15
+ * fallback stays for the platforms that have no other option.
16
+ */
17
+
18
+ import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
19
+ import { darwinProbe } from '@/server/lib/lifecycle/proc/darwin';
20
+ import { linuxProbe } from '@/server/lib/lifecycle/proc/linux';
21
+ import { psProbe } from '@/server/lib/lifecycle/proc/ps';
22
+ import { win32Probe } from '@/server/lib/lifecycle/proc/win32';
23
+
24
+ function selectProbe(): ProcessProbe {
25
+ switch (process.platform) {
26
+ case 'darwin':
27
+ return darwinProbe;
28
+ case 'linux':
29
+ return linuxProbe;
30
+ case 'win32':
31
+ return win32Probe;
32
+ default:
33
+ return psProbe;
34
+ }
35
+ }
36
+
37
+ /** The probe for the running platform. */
38
+ export const processProbe: ProcessProbe = selectProbe();
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Linux process probe: `/proc`, no subprocess and no FFI.
8
+ *
9
+ * `procfs` already holds everything `ps` reads, so this is both faster and
10
+ * immune to a missing binary in minimal containers (where the previous `ps`
11
+ * snapshot simply came back empty and identity degraded to `unknown`).
12
+ *
13
+ * `stat` is parsed from the LAST `)` because field 2 is the executable name in
14
+ * parentheses and may itself contain spaces and parentheses.
15
+ */
16
+
17
+ import fs from 'fs';
18
+
19
+ import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
20
+
21
+ /** Field 3 of `/proc/<pid>/stat`, 1-indexed, once the comm field is stripped. */
22
+ const STATE_FIELD = 0;
23
+ const ZOMBIE_STATES = new Set(['Z', 'X']);
24
+
25
+ function readFile(path: string): string | null {
26
+ try {
27
+ return fs.readFileSync(path, 'utf8');
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /** `/proc/<pid>/stat` fields after the `(comm)` column, or null. */
34
+ function statTail(pid: number): string[] | null {
35
+ const raw = readFile(`/proc/${pid}/stat`);
36
+ if (raw === null) return null;
37
+ const close = raw.lastIndexOf(')');
38
+ if (close === -1) return null;
39
+ return raw.slice(close + 1).trim().split(/\s+/);
40
+ }
41
+
42
+ /**
43
+ * Index of tpgid in `statTail`'s output.
44
+ *
45
+ * `/proc/<pid>/stat` numbers it 8 (1-indexed), and `statTail` has already
46
+ * dropped fields 1 (`pid`) and 2 (`(comm)`) — so the index is 8 - 3 = 5. At 6
47
+ * this read field 9, `flags`, which answered every live process with 0x400000
48
+ * instead of a process group.
49
+ */
50
+ const TPGID_FIELD = 5;
51
+
52
+ export const linuxProbe: ProcessProbe = {
53
+ commandLine(pid) {
54
+ const raw = readFile(`/proc/${pid}/cmdline`);
55
+ if (raw === null) return null;
56
+ // NUL-separated argv, with a trailing NUL; empty for a kernel thread.
57
+ const args = raw.split('\0').filter((part) => part.length > 0);
58
+ if (args.length > 0) return args.join(' ');
59
+ // Kernel threads have no argv — `comm` is all the identity there is.
60
+ return readFile(`/proc/${pid}/comm`)?.trim() || null;
61
+ },
62
+ isAlive(pid) {
63
+ // A `/proc` entry exists for a zombie too, so liveness is "the directory is
64
+ // there"; `isZombie` is what separates a corpse from a running process.
65
+ return fs.existsSync(`/proc/${pid}`);
66
+ },
67
+ isZombie(pid) {
68
+ const fields = statTail(pid);
69
+ if (fields === null) return false;
70
+ return ZOMBIE_STATES.has(fields[STATE_FIELD]);
71
+ },
72
+ foregroundGroup(pid) {
73
+ // Already read for `isZombie` on the same question, so this costs no extra
74
+ // syscall beyond the file read itself.
75
+ const fields = statTail(pid);
76
+ if (fields === null) return null;
77
+ const tpgid = Number(fields[TPGID_FIELD]);
78
+ return Number.isFinite(tpgid) ? tpgid : null;
79
+ },
80
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * `ps` fallback for POSIX platforms without a native probe (the BSDs).
8
+ *
9
+ * Kept because it is the only mechanism left that works everywhere a `ps` is
10
+ * installed, and because dropping it would make identity worse on those
11
+ * platforms than it was before the native probes existed. macOS and Linux no
12
+ * longer reach this path.
13
+ *
14
+ * The whole table is read once and cached briefly: every question in a port
15
+ * check happens within milliseconds, so one snapshot answers all of them.
16
+ */
17
+
18
+ import { spawnSync } from 'bun';
19
+
20
+ import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
21
+
22
+ interface ProcessRow {
23
+ pid: number;
24
+ /** `ps` state column — `Z…` means defunct (exited, awaiting reaping). */
25
+ state: string;
26
+ command: string;
27
+ }
28
+
29
+ const TABLE_MAX_AGE_MS = 250;
30
+ let cached: { at: number; rows: ProcessRow[] } | null = null;
31
+
32
+ function processTable(): ProcessRow[] {
33
+ if (cached && Date.now() - cached.at < TABLE_MAX_AGE_MS) return cached.rows;
34
+ const rows: ProcessRow[] = [];
35
+ try {
36
+ const snapshot = spawnSync({
37
+ cmd: ['ps', '-ax', '-o', 'pid=,stat=,command='],
38
+ stdout: 'pipe',
39
+ stderr: 'ignore',
40
+ });
41
+ if (snapshot.success) {
42
+ for (const line of snapshot.stdout.toString().split('\n')) {
43
+ const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
44
+ if (!match) continue;
45
+ rows.push({ pid: Number(match[1]), state: match[2], command: match[3].trim() });
46
+ }
47
+ }
48
+ } catch {
49
+ // No `ps` on PATH: every question degrades to "unknown".
50
+ }
51
+ cached = { at: Date.now(), rows };
52
+ return rows;
53
+ }
54
+
55
+ function signalAlive(pid: number): boolean {
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ } catch (error) {
60
+ // EPERM: the PID exists, we may not signal it.
61
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
62
+ }
63
+ }
64
+
65
+ export const psProbe: ProcessProbe = {
66
+ commandLine(pid) {
67
+ const row = processTable().find((entry) => entry.pid === pid);
68
+ return row && row.command.length > 0 ? row.command : null;
69
+ },
70
+ isAlive(pid) {
71
+ if (!signalAlive(pid)) return false;
72
+ const row = processTable().find((entry) => entry.pid === pid);
73
+ // Absent from the table means no `ps` (or a race) — liveness stands.
74
+ return row ? !row.state.startsWith('Z') : true;
75
+ },
76
+ isZombie(pid) {
77
+ const row = processTable().find((entry) => entry.pid === pid);
78
+ return row ? row.state.startsWith('Z') : false;
79
+ },
80
+ foregroundGroup() {
81
+ // The cached table has no `tpgid` column, and adding one would make every
82
+ // question pay for a field only this one asks. Null keeps the caller on its
83
+ // own `ps -o pid=,tpgid=` path, which is where the BSDs were anyway.
84
+ return null;
85
+ },
86
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * What a platform must be able to answer about a foreign PID, in its own
8
+ * syscall vocabulary. One implementation per OS, selected in `identity.ts`.
9
+ *
10
+ * Every method must degrade rather than throw: a diagnostic question about a
11
+ * PID is never worth failing a request over, and a platform that cannot answer
12
+ * says so (`null` / `false`) so the caller can fall back.
13
+ */
14
+ export interface ProcessProbe {
15
+ /** Command line of a live PID, or null when unreadable. */
16
+ commandLine(pid: number): string | null;
17
+ /** True when the PID exists. */
18
+ isAlive(pid: number): boolean;
19
+ /** True when the PID is defunct — exited but not yet reaped by its parent.
20
+ * Such a PID still answers signal 0 and holds no port. */
21
+ isZombie(pid: number): boolean;
22
+ /**
23
+ * Foreground process group of the PID's controlling terminal — the value
24
+ * `ps -o tpgid=` prints, including its `0` for "no controlling terminal".
25
+ * Null when this platform (or this call) cannot answer, which is the
26
+ * caller's signal to fall back to `ps`.
27
+ *
28
+ * A PTY's `tpgid` is what tells a shell sitting at its prompt (the group is
29
+ * the shell itself) from one running a command (the group is that command),
30
+ * so it is the exact answer to "is this terminal busy".
31
+ */
32
+ foregroundGroup(pid: number): number | null;
33
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Windows process probe.
8
+ *
9
+ * Deliberately the thinnest of the three, because Windows is where the other
10
+ * two mechanisms have no equivalent:
11
+ *
12
+ * - There is no `ps` and no `/proc`.
13
+ * - There is no zombie: a process object lives until its last handle closes,
14
+ * so "exited but not reaped" is not a state that can be observed. `isZombie`
15
+ * is therefore always false rather than a guess.
16
+ * - A foreign process's command line is not reachable without walking its PEB
17
+ * through `ntdll` while it is suspended — untestable here and far more
18
+ * dangerous than the problem it solves. `commandLine` returns null, which
19
+ * `getProcessState` already reports as `unknown`, and callers treat
20
+ * `unknown` as "alive but unverified" — never as confirmation.
21
+ *
22
+ * So identity on Windows is liveness, exactly as before this module existed;
23
+ * the value here is that the behaviour is stated instead of emerging from a
24
+ * `ps` snapshot that silently comes back empty.
25
+ */
26
+
27
+ import type { ProcessProbe } from '@/server/lib/lifecycle/proc/types';
28
+
29
+ function signalAlive(pid: number): boolean {
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ } catch (error) {
34
+ // EPERM: the PID exists, we may not signal it.
35
+ return (error as NodeJS.ErrnoException).code === 'EPERM';
36
+ }
37
+ }
38
+
39
+ export const win32Probe: ProcessProbe = {
40
+ commandLine() {
41
+ return null;
42
+ },
43
+ isAlive: signalAlive,
44
+ isZombie() {
45
+ return false;
46
+ },
47
+ foregroundGroup() {
48
+ // Windows has no process group tied to a console the way a POSIX PTY has
49
+ // `tpgid`, and no `ps` either — the caller's own platform check keeps this
50
+ // from being asked, and null says "unknown" rather than guessing.
51
+ return null;
52
+ },
53
+ };
@@ -11,8 +11,8 @@
11
11
  * files and drives the shared utility RPC process.
12
12
  */
13
13
 
14
- import { readDisabledProviders } from '@/server/lib/omp/config/roles';
15
- import { getModelsConfigPath, readNativeProviders } from '@/server/lib/omp/config/providers';
14
+ import { readDisabledProviders } from '@/server/lib/omp/config/disabled-providers';
15
+ import { getModelsConfigPath, readNativeProviders } from '@/server/lib/omp/config/models-config';
16
16
  import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
17
17
  import { isKenariProvider, removeLegacyKenariModels } from '@/shared/lib/models/provider-cleanup';
18
18
  import { formatContextWindow } from '@/shared/lib/code/format';
@@ -15,7 +15,7 @@
15
15
  import { getDb } from '@/server/db.server';
16
16
  import { PROVIDERS_SETTINGS_KEY } from '@/server/lib/models/provider-registry.server';
17
17
  import { readSettingsJson } from '@/server/lib/db/settings-store';
18
- import { readDisabledProviders } from '@/server/lib/omp/config/roles';
18
+ import { readDisabledProviders } from '@/server/lib/omp/config/disabled-providers';
19
19
  import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
20
20
  import { invalidateModelsCaches } from '@/shared/lib/models/server-cache';
21
21
  import type { ModelsData, ProviderItem } from '@/shared/types';