super-dsh 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Read-only scanner over OMP's native session store
3
+ * (`$OMP_HOME/agent/sessions/<cwd-dashed>/<timestamp>_<uuid>.jsonl`).
4
+ *
5
+ * OMP owns the transcript format; this module extracts only what the Dash
6
+ * surfaces need per session — identity, cwd, creation time, and a display
7
+ * title — by reading a bounded HEAD of each JSONL file (never the full
8
+ * transcript). Parsed entries are memoized per file keyed on (size, mtime),
9
+ * so repeated `list()` calls re-stat but re-parse only changed files.
10
+ */
11
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, statSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { basename, dirname, join } from "node:path";
14
+ import { OMP_NATIVE_HOME, resolveOmpNativeHome } from "./knobs.js";
15
+ /** Root of OMP's native session store. */
16
+ export const OMP_SESSIONS_ROOT = process.env.OMP_SESSIONS_ROOT ?? join(OMP_NATIVE_HOME, "agent", "sessions");
17
+ /** How many head bytes of one session file the scanner will read. */
18
+ const HEAD_BUDGET_BYTES = 256 * 1024;
19
+ /** Title fallback budget (characters of the first user message). */
20
+ const FALLBACK_TITLE_CHARS = 80;
21
+ /** Memoized parse results, keyed by session file path. */
22
+ const entryCache = new Map();
23
+ /** Epoch ms from a `<YYYY>-<MM>-<DD>T<HH>-<MM>-<SS>-<mmm>Z` filename prefix. */
24
+ function createdAtFromFileName(name) {
25
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z_/.exec(name);
26
+ if (match === null)
27
+ return undefined;
28
+ const [year, month, day, hour, minute, second, ms] = match.slice(1).map(Number);
29
+ const epoch = Date.UTC(year, month - 1, day, hour, minute, second, ms);
30
+ return Number.isSafeInteger(epoch) ? epoch : undefined;
31
+ }
32
+ /** One normalized display-title fallback from a first user message. */
33
+ function fallbackTitle(text) {
34
+ const firstLine = text.split("\n", 1)[0] ?? "";
35
+ const collapsed = firstLine.replaceAll(/\s+/gu, " ").trim();
36
+ return collapsed.length === 0 ? undefined : collapsed.slice(0, FALLBACK_TITLE_CHARS);
37
+ }
38
+ /**
39
+ * Read one OMP session transcript in a single pass, extracting both the
40
+ * `message` records and the `model_change` records.
41
+ */
42
+ export function readOmpTranscript(path) {
43
+ const messages = [];
44
+ const modelChanges = [];
45
+ let buffer;
46
+ try {
47
+ buffer = readFileSync(path, "utf8");
48
+ }
49
+ catch {
50
+ return { messages, modelChanges };
51
+ }
52
+ for (const line of buffer.split("\n")) {
53
+ if (line === "")
54
+ continue;
55
+ let record;
56
+ try {
57
+ record = JSON.parse(line);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ if (record === null || typeof record !== "object")
63
+ continue;
64
+ const rec = record;
65
+ if (rec["type"] === "message") {
66
+ const message = rec["message"];
67
+ if (message === null || typeof message !== "object")
68
+ continue;
69
+ const msg = message;
70
+ const role = msg["role"];
71
+ if (role !== "user" && role !== "assistant" && role !== "toolResult")
72
+ continue;
73
+ messages.push({ ...msg, role });
74
+ }
75
+ else if (rec["type"] === "model_change") {
76
+ const model = rec["model"];
77
+ if (typeof model !== "string" || model === "")
78
+ continue;
79
+ modelChanges.push({ model, role: typeof rec["role"] === "string" ? rec["role"] : undefined });
80
+ }
81
+ }
82
+ return { messages, modelChanges };
83
+ }
84
+ /**
85
+ * Legacy fallback: the last assistant message that names a provider/model
86
+ * pair. Used only for transcripts without any `model_change` record; newer
87
+ * sessions derive their resume model from {@link lastRestorableModel}.
88
+ */
89
+ export function lastModelCall(messages) {
90
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
91
+ const message = messages[index];
92
+ if (message.role !== "assistant")
93
+ continue;
94
+ const provider = typeof message.provider === "string" ? message.provider : "";
95
+ const model = typeof message.model === "string" ? message.model : "";
96
+ if (provider !== "" && model !== "")
97
+ return { provider, model };
98
+ }
99
+ return undefined;
100
+ }
101
+ /** Split a `provider/model[:variant]` selector into provider/model, dropping the variant suffix. */
102
+ export function parseSelector(selector) {
103
+ if (selector === undefined)
104
+ return undefined;
105
+ const slash = selector.indexOf("/");
106
+ if (slash <= 0)
107
+ return undefined;
108
+ let model = selector.slice(slash + 1);
109
+ const colon = model.indexOf(":");
110
+ if (colon > 0)
111
+ model = model.slice(0, colon);
112
+ return { provider: selector.slice(0, slash), model };
113
+ }
114
+ /**
115
+ * The model OMP restores on `--resume`: the last non-fallback `model_change`.
116
+ * Mirrors OMP's `getRestorableSessionModels` (`session-context.ts`): a
117
+ * fallback switch is recorded under `role:"fallback"` (EPHEMERAL) and never
118
+ * restores — the last such switch falls back to the role's primary.
119
+ */
120
+ export function lastRestorableModel(modelChanges) {
121
+ const models = {};
122
+ let lastRole;
123
+ for (const change of modelChanges) {
124
+ const role = change.role ?? "default";
125
+ models[role] = change.model;
126
+ lastRole = change.role;
127
+ }
128
+ const selector = lastRole === undefined || lastRole === "default" || lastRole === "fallback"
129
+ ? models["default"]
130
+ : (models[lastRole] ?? models["default"]);
131
+ return parseSelector(selector);
132
+ }
133
+ /**
134
+ * Path to OMP's per-user agent config (`<OMP_NATIVE_HOME>/agent/config.yml`).
135
+ *
136
+ * This is the OMP layer's own config, distinct from dsh's settings layer:
137
+ * upstream dsh keeps a home-level singleton settings document
138
+ * (`$DSH_HOME/settings.yaml`); the profile only composes bundles and has no
139
+ * per-profile settings isolation of its own. This bridge never reads that
140
+ * settings.yaml — OMP data lives under OMP_NATIVE_HOME, and the bridge's own
141
+ * index under `$DSH_HOME/bridge-store.sqlite` (see store/index.ts).
142
+ */
143
+ function ompConfigPath() {
144
+ // Per-call resolution (not the frozen knob const) so tests can inject the
145
+ // native root via OMP_NATIVE_HOME at any time.
146
+ return join(resolveOmpNativeHome(), "agent", "config.yml");
147
+ }
148
+ /**
149
+ * Read the whole `modelRoles` map (role → "provider/model[:effort]") from
150
+ * OMP's config.yml. Returns undefined when the block is absent/empty or the
151
+ * file is unreadable. A deliberately minimal YAML scan — the bridge has no
152
+ * YAML dependency; one indent level of `key: value` lines inside the block.
153
+ */
154
+ export function readOmpModelRolesFromConfig() {
155
+ let text;
156
+ try {
157
+ text = readFileSync(ompConfigPath(), "utf8");
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ const roles = {};
163
+ let inModelRoles = false;
164
+ for (const line of text.split("\n")) {
165
+ if (/^modelRoles:\s*$/.test(line)) {
166
+ inModelRoles = true;
167
+ continue;
168
+ }
169
+ if (!inModelRoles)
170
+ continue;
171
+ if (line !== "" && !/^\s/.test(line))
172
+ break; // left the modelRoles block
173
+ const match = /^\s*([A-Za-z0-9_-]+):\s*(\S+)\s*$/.exec(line);
174
+ if (match !== null)
175
+ roles[match[1]] = match[2];
176
+ }
177
+ return Object.keys(roles).length > 0 ? roles : undefined;
178
+ }
179
+ /**
180
+ * Read the configured default role model (`modelRoles.default`) from OMP's
181
+ * config.yml. Returns the provider/model pair (variant suffix dropped), or
182
+ * undefined when unset or unreadable. Delegates to the whole-map reader.
183
+ */
184
+ export function readOmpDefaultModelFromConfig() {
185
+ const def = readOmpModelRolesFromConfig()?.default;
186
+ return def === undefined ? undefined : parseSelector(def);
187
+ }
188
+ /**
189
+ * Derive the working directory a session ran in from its location in OMP's
190
+ * store: the parent directory name is the cwd with every "/" flattened to
191
+ * "-" (leading "/" → leading "-"). Literal dashes in path segments are
192
+ * ambiguous, so segments are re-joined greedily longest-first against the
193
+ * filesystem and the candidate is accepted only as an existing directory
194
+ * (realpath'd). This is the trusted resume cwd — the omp-sessions.json
195
+ * mapping's verbatim `cwd` is user-writable and never consulted.
196
+ */
197
+ export function cwdFromSessionFile(path) {
198
+ const dashed = basename(dirname(path));
199
+ if (!dashed.startsWith("-"))
200
+ return undefined;
201
+ const tokens = dashed.slice(1).split("-").filter((token) => token !== "");
202
+ if (tokens.length === 0)
203
+ return undefined;
204
+ // OMP flattens the cwd's "/" separators to "-"; home-relative cwds lose
205
+ // their "~/" prefix entirely ("-workspaces-x" ≡ ~/workspaces/x), absolute
206
+ // ones keep the leading slash ("-tmp-x" ≡ /tmp/x). Try the absolute base
207
+ // first, then home; literal dashes in segment names are ambiguous, so each
208
+ // walk consumes the LONGEST run of remaining tokens that names an existing
209
+ // directory (dash-joined), which resolves the common cases exactly.
210
+ for (const base of ["/", homedir()]) {
211
+ let current = base === "/" ? "" : base;
212
+ let consumed = 0;
213
+ let complete = true;
214
+ while (consumed < tokens.length) {
215
+ let stepped = false;
216
+ for (let take = tokens.length - consumed; take >= 1; take--) {
217
+ const candidate = `${current}/${tokens.slice(consumed, consumed + take).join("-")}`;
218
+ try {
219
+ if (!statSync(candidate).isDirectory())
220
+ continue;
221
+ }
222
+ catch {
223
+ continue;
224
+ }
225
+ current = candidate;
226
+ consumed += take;
227
+ stepped = true;
228
+ break;
229
+ }
230
+ if (!stepped) {
231
+ complete = false;
232
+ break;
233
+ }
234
+ }
235
+ if (complete) {
236
+ try {
237
+ return realpathSync(current);
238
+ }
239
+ catch {
240
+ // try the next base
241
+ }
242
+ }
243
+ }
244
+ return undefined;
245
+ }
246
+ /** Parse the interesting head records of one OMP session file. */
247
+ function scanHead(path) {
248
+ const out = {};
249
+ let buffer = "";
250
+ let remaining = HEAD_BUDGET_BYTES;
251
+ const chunks = [];
252
+ const fd = openSync(path, "r");
253
+ try {
254
+ while (remaining > 0) {
255
+ const chunk = Buffer.allocUnsafe(Math.min(remaining, 64 * 1024));
256
+ const read = readSync(fd, chunk, 0, chunk.length, null);
257
+ if (read === 0)
258
+ break;
259
+ chunks.push(chunk.subarray(0, read));
260
+ remaining -= read;
261
+ }
262
+ }
263
+ finally {
264
+ closeSync(fd);
265
+ }
266
+ buffer = Buffer.concat(chunks).toString("utf8");
267
+ for (const line of buffer.split("\n")) {
268
+ let record;
269
+ try {
270
+ record = JSON.parse(line);
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ if (record === null || typeof record !== "object")
276
+ continue;
277
+ const rec = record;
278
+ if (rec["type"] === "title" || rec["type"] === "title_change") {
279
+ // Latest wins: OMP renames titles over a session's life (replans), and
280
+ // the scan must surface the name the TUI shows today, not the first.
281
+ const title = rec["title"];
282
+ if (typeof title === "string" && title.trim().length > 0)
283
+ out.title = title.trim();
284
+ }
285
+ else if (rec["type"] === "session" && out.ompSessionId === undefined) {
286
+ const id = rec["id"];
287
+ const timestamp = rec["timestamp"];
288
+ const cwd = rec["cwd"];
289
+ if (typeof id === "string" && id.length > 0)
290
+ out.ompSessionId = id;
291
+ if (typeof timestamp === "string") {
292
+ const epoch = Date.parse(timestamp);
293
+ if (Number.isSafeInteger(epoch))
294
+ out.createdAt = epoch;
295
+ }
296
+ if (typeof cwd === "string" && cwd.length > 0)
297
+ out.cwd = cwd;
298
+ }
299
+ else if (rec["type"] === "message" && out.firstUserText === undefined) {
300
+ const message = rec["message"];
301
+ if (message === null || typeof message !== "object")
302
+ continue;
303
+ const msg = message;
304
+ if (msg["role"] !== "user" || (msg["attribution"] !== undefined && msg["attribution"] !== "user"))
305
+ continue;
306
+ const content = msg["content"];
307
+ if (!Array.isArray(content))
308
+ continue;
309
+ for (const block of content) {
310
+ if (block === null || typeof block !== "object")
311
+ continue;
312
+ const b = block;
313
+ if (b["type"] === "text" && typeof b["text"] === "string" && b["text"].trim().length > 0) {
314
+ out.firstUserText = b["text"];
315
+ break;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ return out;
321
+ }
322
+ /** Read one session file's header id (the `session` record's `id`), no full scan. */
323
+ export function sessionHeaderId(path) {
324
+ return scanHead(path).ompSessionId;
325
+ }
326
+ /**
327
+
328
+ /**
329
+ * Scan OMP's native store (PURE DISK — sidecar-free). Returns sessions keyed
330
+ * by OMP session id; files without a parsable `session` header record are
331
+ * skipped (corrupt/foreign).
332
+ *
333
+ * The FILE LIST always comes from a fresh directory walk; per-session metadata
334
+ * (id/cwd/title/createdAt) comes from the self-written {@link scanHead},
335
+ * memoized by size+mtime in `entryCache`. The old SDK `sessions.listAll` fast
336
+ * path is GONE (sidecar-decoupling): the only consumer left is pairing.ts's
337
+ * store-absent fallback (unit tests).
338
+ */
339
+ export function scanOmpSessions() {
340
+ const sessions = new Map();
341
+ let workspaceDirs;
342
+ try {
343
+ workspaceDirs = readdirSync(OMP_SESSIONS_ROOT, { withFileTypes: true })
344
+ .filter((entry) => entry.isDirectory())
345
+ .map((entry) => entry.name);
346
+ }
347
+ catch {
348
+ return sessions;
349
+ }
350
+ for (const dirName of workspaceDirs) {
351
+ let files;
352
+ try {
353
+ files = readdirSync(join(OMP_SESSIONS_ROOT, dirName));
354
+ }
355
+ catch {
356
+ continue;
357
+ }
358
+ for (const fileName of files) {
359
+ if (!fileName.endsWith(".jsonl"))
360
+ continue;
361
+ const path = join(OMP_SESSIONS_ROOT, dirName, fileName);
362
+ let size;
363
+ let mtimeMs;
364
+ try {
365
+ const stats = statSync(path);
366
+ size = stats.size;
367
+ mtimeMs = stats.mtimeMs;
368
+ }
369
+ catch {
370
+ entryCache.delete(path);
371
+ continue;
372
+ }
373
+ let cached = entryCache.get(path);
374
+ if (cached === undefined || cached.size !== size || cached.mtimeMs !== mtimeMs) {
375
+ const head = scanHead(path);
376
+ const parsed = head.ompSessionId === undefined
377
+ ? undefined
378
+ : {
379
+ ompSessionId: head.ompSessionId,
380
+ ompSessionFile: path,
381
+ size,
382
+ mtimeMs,
383
+ ...(head.cwd === undefined ? {} : { cwd: head.cwd }),
384
+ createdAt: head.createdAt ?? createdAtFromFileName(fileName) ?? 0,
385
+ ...(head.title !== undefined
386
+ ? { title: head.title }
387
+ : head.firstUserText !== undefined
388
+ ? { title: fallbackTitle(head.firstUserText) }
389
+ : {}),
390
+ revision: `omp:${size}:${mtimeMs}`,
391
+ };
392
+ cached = { size, mtimeMs, entry: parsed };
393
+ entryCache.set(path, cached);
394
+ }
395
+ const entry = cached.entry;
396
+ if (entry !== undefined && !sessions.has(entry.ompSessionId)) {
397
+ sessions.set(entry.ompSessionId, entry);
398
+ }
399
+ }
400
+ }
401
+ return sessions;
402
+ }
403
+ /** Parent pid of a process, from `/proc/<pid>/stat` (undefined when unreadable). */
404
+ function parentPid(pid) {
405
+ try {
406
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
407
+ // The comm field may contain spaces and parens; fields resume after the LAST ')'.
408
+ const tail = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
409
+ const ppid = Number.parseInt(tail[1] ?? "", 10);
410
+ return Number.isNaN(ppid) ? undefined : ppid;
411
+ }
412
+ catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ /** Whether `pid` sits anywhere below `ancestor` in the process tree. */
417
+ function isDescendantOf(pid, ancestor) {
418
+ let current = pid;
419
+ for (let hop = 0; hop < 32; hop += 1) {
420
+ if (current === ancestor)
421
+ return true;
422
+ const parent = parentPid(current);
423
+ if (parent === undefined || parent <= 1)
424
+ return false;
425
+ current = parent;
426
+ }
427
+ return false;
428
+ }
429
+ /**
430
+ * One /proc pass over every foreign process's write-mode file descriptors,
431
+ * returning `canonical path → first foreign writer pid`. Shared by every
432
+ * session in one tick so the daemon never re-scans /proc per file.
433
+ *
434
+ * OMP has no session-level exclusivity: neither the TUI nor `--mode rpc`
435
+ * locks its transcript, so two live writers interleave appends silently
436
+ * (verified: a TUI `omp` and a bridge `omp --mode rpc --resume` coexisted on
437
+ * one file with plain `w` descriptors and no advisory locks). The bridge
438
+ * therefore refuses to resume a transcript another process already writes —
439
+ * the TUI case — closing the split-brain at the only layer that can see it.
440
+ * Own descendants (a just-torn-down bridge child whose fds linger) are
441
+ * exempt to keep teardown races from wedging re-resume.
442
+ */
443
+ export function scanForeignWriters() {
444
+ const holders = new Map();
445
+ let procEntries;
446
+ try {
447
+ procEntries = readdirSync("/proc");
448
+ }
449
+ catch {
450
+ return holders;
451
+ }
452
+ const self = process.pid;
453
+ for (const entry of procEntries) {
454
+ if (!/^[0-9]+$/.test(entry))
455
+ continue;
456
+ const pid = Number.parseInt(entry, 10);
457
+ if (pid === self || isDescendantOf(pid, self))
458
+ continue;
459
+ let fds;
460
+ try {
461
+ fds = readdirSync(`/proc/${entry}/fd`);
462
+ }
463
+ catch {
464
+ continue;
465
+ }
466
+ for (const fd of fds) {
467
+ let target;
468
+ try {
469
+ target = readlinkSync(`/proc/${entry}/fd/${fd}`);
470
+ }
471
+ catch {
472
+ continue;
473
+ }
474
+ // Non-path targets (socket:[…], pipe:[…], anon_inode:[…]) can never be a session file.
475
+ if (!target.startsWith("/"))
476
+ continue;
477
+ let flags;
478
+ try {
479
+ const info = readFileSync(`/proc/${entry}/fdinfo/${fd}`, "utf8");
480
+ const flagLine = info.split("\n").find((line) => line.startsWith("flags:"));
481
+ const raw = flagLine?.split(":")[1]?.trim();
482
+ flags = raw === undefined ? undefined : Number.parseInt(raw, 8);
483
+ }
484
+ catch {
485
+ continue;
486
+ }
487
+ if (flags === undefined || Number.isNaN(flags))
488
+ continue;
489
+ if ((flags & 0b11) === 0)
490
+ continue; // neither O_WRONLY nor O_RDWR
491
+ let canonical;
492
+ try {
493
+ canonical = realpathSync(target);
494
+ }
495
+ catch {
496
+ continue;
497
+ }
498
+ if (!holders.has(canonical))
499
+ holders.set(canonical, pid);
500
+ }
501
+ }
502
+ return holders;
503
+ }
504
+ /**
505
+ * Pid of a FOREIGN process holding `file` open for writing, if any.
506
+ * Convenience wrapper over {@link scanForeignWriters} for the single-file
507
+ * resume gate; the supervisor uses the shared collector directly per tick.
508
+ */
509
+ export function foreignWriterPid(file) {
510
+ let canonical;
511
+ try {
512
+ canonical = realpathSync(file);
513
+ }
514
+ catch {
515
+ return undefined;
516
+ }
517
+ return scanForeignWriters().get(canonical);
518
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Dash↔OMP session id pairing, backed by the centralized index (DB).
3
+ *
4
+ * The index is the sole pairing authority (D3): bridge-created sessions carry
5
+ * their real Dash id (migrated from webui.json or written at createAgent);
6
+ * every other OMP session gets a deterministic derived id (`session-` + the
7
+ * OMP uuidv7, whose version nibble 7 ≠ 4 keeps it collision-free against
8
+ * apiproxy-minted ids). All id translation is now an O(1) indexed lookup —
9
+ * no webui.json read/write, no full-store scan.
10
+ */
11
+ import { getBridgeStore } from "./store/index.js";
12
+ import { scanOmpSessions } from "./omp-store.js";
13
+ /** The Dash-format prefix apiproxy mints; derived ids reuse it verbatim. */
14
+ const DASH_PREFIX = "session-";
15
+ /** Reconstruct a scanned-entry-shaped view from an index row. */
16
+ function rowToEntry(row) {
17
+ return {
18
+ ompSessionId: row.omp_session_id,
19
+ ompSessionFile: row.session_file,
20
+ ...(row.cwd === null ? {} : { cwd: row.cwd }),
21
+ createdAt: row.created_at,
22
+ ...(row.title === null ? {} : { title: row.title }),
23
+ size: row.transcript_size,
24
+ mtimeMs: row.last_modified_at,
25
+ revision: `omp:${row.transcript_size}:${row.last_modified_at}`,
26
+ };
27
+ }
28
+ /**
29
+ * Resolve an id from the Dash side back to its session entry. Indexed lookup
30
+ * (byDshId / byOmpId) — never a full scan. Falls back to the scanner only
31
+ * when the store is absent (unit tests running the supervisor in isolation).
32
+ */
33
+ export function resolveEntryById(id) {
34
+ const store = getBridgeStore();
35
+ if (store !== undefined) {
36
+ const row = id.startsWith(DASH_PREFIX) ? store.byDshId(id) : store.byOmpId(id);
37
+ return row === undefined ? undefined : rowToEntry(row);
38
+ }
39
+ if (id.startsWith(DASH_PREFIX)) {
40
+ const stripped = id.slice(DASH_PREFIX.length);
41
+ const direct = scanOmpSessions().get(stripped);
42
+ return direct;
43
+ }
44
+ return scanOmpSessions().get(id);
45
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Dash permission preset ↔ OMP approval mode.
3
+ *
4
+ * OMP has no runtime approval control: `--approval-mode` is a launch flag, no
5
+ * RPC changes it mid-session. Dash's 3-way permission preset is therefore
6
+ * resolved once, when the OMP child is spawned, and mapped explicitly:
7
+ *
8
+ * danger-full-access → `--approval-mode yolo` (OMP's own native default)
9
+ * workspace-write → `--approval-mode write`
10
+ * read-only → `--approval-mode always-ask`
11
+ * no explicit preset → yolo (native default)
12
+ *
13
+ * `OMP_APPROVAL_MODE` (a valid mode) overrides the mapping — headless test
14
+ * runs pin the flag without composing a preset default.
15
+ *
16
+ * The preset is a Dash-side session fact OMP never records. It now lives in
17
+ * the centralized index (`sessions.permission_preset`), seeded once from the
18
+ * legacy per-session webui.json by the migration; webui.json stays on disk
19
+ * but is never read or written again (D3).
20
+ */
21
+ import { dirname, join } from "node:path";
22
+ /** The approval modes OMP actually implements (unknown modes degrade to yolo). */
23
+ const APPROVAL_MODES = ["write", "always-ask", "yolo"];
24
+ /** The Dash preset table (mirrors the base bundle's permission plugin config). */
25
+ const PRESETS = {
26
+ "danger-full-access": { sandbox: "danger-full-access", approval: "never", ompMode: "yolo" },
27
+ "workspace-write": { sandbox: "workspace-write", approval: "ask", ompMode: "write" },
28
+ "read-only": { sandbox: "read-only", approval: "ask", ompMode: "always-ask" },
29
+ };
30
+ /** Narrow an untrusted value to a known preset name. */
31
+ export function isPresetName(raw) {
32
+ return typeof raw === "string" && Object.hasOwn(PRESETS, raw);
33
+ }
34
+ /**
35
+ * `OMP_APPROVAL_MODE` when it names a mode OMP implements, else undefined.
36
+ * An invalid value is IGNORED (the preset mapping applies) — the old
37
+ * fall-back-to-`write` behavior silently downgraded from OMP's native yolo.
38
+ */
39
+ export function envApprovalMode() {
40
+ const raw = process.env.OMP_APPROVAL_MODE;
41
+ return raw !== undefined && APPROVAL_MODES.includes(raw) ? raw : undefined;
42
+ }
43
+ /** Map a preset onto OMP's `--approval-mode` flag value; no preset → yolo. */
44
+ export function ompApprovalMode(preset) {
45
+ return isPresetName(preset) ? PRESETS[preset].ompMode : "yolo";
46
+ }
47
+ /**
48
+ * The default preset for FUTURE sessions, as the permission service
49
+ * resolves it (settings-backed; the profile patch pins the base default).
50
+ * Undefined when the service is absent or its getter fails — callers fall
51
+ * back to OMP's native default.
52
+ */
53
+ export function defaultPermissionPreset(ctx) {
54
+ const service = ctx.get("permissionPresets");
55
+ if (service === undefined)
56
+ return undefined;
57
+ try {
58
+ const preset = service.defaultPreset;
59
+ return isPresetName(preset) ? preset : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** The session's effective preset: the last `permission/preset` event. */
66
+ export function presetFromEvents(events) {
67
+ for (let index = events.length - 1; index >= 0; index -= 1) {
68
+ const event = events[index];
69
+ if (event.type === "permission/preset" && isPresetName(event.data?.preset))
70
+ return event.data.preset;
71
+ }
72
+ return undefined;
73
+ }
74
+ /**
75
+ * The three Dash events `pinInitialPermission` stamps for a fresh session,
76
+ * in its append order (preset, sandbox, approval). Cold replay prepends
77
+ * them so the UI shows the session's permission after wrapper restarts.
78
+ */
79
+ export function permissionEventsFor(preset, time) {
80
+ const spec = PRESETS[preset];
81
+ return [
82
+ { type: "permission/preset", time, data: { preset } },
83
+ { type: "sandbox/mode", time, data: { mode: spec.sandbox } },
84
+ { type: "approval/policy", time, data: { policy: spec.approval } },
85
+ ];
86
+ }
87
+ /**
88
+ * The bridge's legacy per-session artifact path (the migration reads this to
89
+ * import the old `dashSessionId` / `permissionPreset` once). Kept read-only;
90
+ * nothing writes it after the migration.
91
+ */
92
+ export function webuiArtifactPath(sessionFile) {
93
+ const base = sessionFile.split("/").pop() ?? sessionFile;
94
+ const stem = base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
95
+ return join(dirname(sessionFile), stem, "webui.json");
96
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * omp-web sidecar protocol (v0).
3
+ *
4
+ * Transport: JSON lines over stdio of the bun sidecar process.
5
+ * - Node → sidecar: request frame { id, method, params }
6
+ * - sidecar → Node: response frame { id, ok: true, result } | { id, ok: false, error }
7
+ * - sidecar → Node: event frame { event: "<channel>", sessionId?, payload }
8
+ * - Node → sidecar: notification { method, params } (no id, no response — e.g. session.prompt cancel is out of band via abort)
9
+ *
10
+ * Handshake: on start the sidecar pushes { event: "ready", payload: { protocol: 0, sdk: <version> } }.
11
+ * Every request gets exactly one response, in any order (correlate by id).
12
+ */
13
+ export const PROTOCOL_VERSION = 0;
@@ -0,0 +1 @@
1
+ export {};