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,574 @@
1
+ /**
2
+ * dsh-omp-provider — OMP provider plugin for DeepSeek Harness.
3
+ *
4
+ * Replaces the built-in agent loop with an `AgentFactory` that drives an
5
+ * in-process OMP SDK sidecar (`bun run sidecar/main.ts`, spawned by
6
+ * `OmpSdkClient`) and bridges its event stream into the Dash Agent/Session
7
+ * contracts. There is no `omp --mode rpc` fallback: the legacy RPC client was
8
+ * retired with the SDK migration. Mirrors `@deepseek-ai/dsh-agent-loop`'s creation transaction
9
+ * (prepare → setup → publish) so the session + agent publish as one ordered
10
+ * lifecycle.
11
+ *
12
+ * Phase 3 adds resume:
13
+ * - `createAgent` records the Dash→OMP session identity in `OmpSessionIndex`
14
+ * (OMP owns the transcript, Dash owns the live identity).
15
+ * - `resume` re-attaches the OMP app-home session file through the SDK and restores
16
+ * the persisted Dash session log (`sessionPersistence.prepare`), so the
17
+ * Web UI's cold listing/history/follow-up path (`session.list`,
18
+ * `session.history`, `session.prompt` → `agents.resume`) reopens the
19
+ * exact transcript it rendered before the restart. The OMP transcript is
20
+ * replayed (`get_messages`) only when no Dash log was persisted.
21
+ */
22
+ import { realpathSync, statSync } from "node:fs";
23
+ import { Service } from "@deepseek-ai/cordis";
24
+ import { homedir } from "node:os";
25
+ import { join } from "node:path";
26
+ import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
27
+ import { SessionPreparation, SessionLogOffset } from "@deepseek-ai/dsh-session";
28
+ import { LazyOmpRpc } from "./lazy-rpc.js";
29
+ import { OmpAgent } from "./agent.js";
30
+ import { OmpLlmAdapter } from "./adapter.js";
31
+ import { SingleOmpPresetRoster } from "./agent-preset-omp.js";
32
+ import { ompAgentPresetProjection } from "./agent-preset-projection.js";
33
+ import { resolveEntryById } from "./pairing.js";
34
+ import { readOmpDefaultModelFromConfig, sessionHeaderId } from "./omp-store.js";
35
+ import { ompModelRoles, ompSetModelRoles } from "./omp-cli.js";
36
+ import { loadOmpModels, ompProviderIds, refreshOmpModelsCli } from "./models.js";
37
+ import { OMP_NATIVE_HOME, STORAGE_RECONCILE_INTERVAL_MS } from "./knobs.js";
38
+ import { defaultPermissionPreset, envApprovalMode, ompApprovalMode, presetFromEvents } from "./permission.js";
39
+ import { closeBridgeStore, getBridgeStore, initBridgeStore } from "./store/index.js";
40
+ import { prepareIndex, upsertCreated } from "./store/reconcile.js";
41
+ import { installMobileBootScript } from "./mobile-boot.js";
42
+ import { installOmpDiscovery } from "./discovery.js";
43
+ /** Realpath of a recorded cwd, accepted only when it names an existing directory. */
44
+ function validatedCwd(cwd) {
45
+ if (cwd === undefined)
46
+ return undefined;
47
+ try {
48
+ const canonical = realpathSync(cwd);
49
+ return statSync(canonical).isDirectory() ? canonical : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ /** Diagnostic trace (OMP_TRACE=1 on the dsh process enables stderr tracing). */
56
+ const trace = (...parts) => {
57
+ if (process.env.OMP_TRACE === "1")
58
+ process.stderr.write(`[omp-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
59
+ };
60
+ export class OmpProvider extends Service {
61
+ /**
62
+ * `agentDefaultModel` is load-bearing, not decorative: the harness owns the
63
+ * "default for new sessions" selection there (`@deepseek-ai/dsh-agent-default-model`,
64
+ * mounted by dsh-base with a hard-coded `deepseek-official/deepseek-flash`),
65
+ * and this bridge is the only thing that feeds it OMP's configured default.
66
+ * Without the inject the context cannot resolve the service, every
67
+ * `saveSelection` in #syncModelDefaultTick silently no-ops, and a fresh
68
+ * session inherits the unserved native default → `session/model-unavailable`.
69
+ */
70
+ static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
71
+ /** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
72
+ runtime;
73
+ /** Stops the supervisor's follow loop on provider teardown. */
74
+ /** Live RPC-backed agents by dash id, for avoidance hand-off. */
75
+ heldAgents = new Map();
76
+ /**
77
+ * Persist the default selection into the settings section.
78
+ *
79
+ * `ctx.agentDefaultModel.saveSelection()` cannot do it: the harness service
80
+ * installs the section from an `inject(["settings"], …)` callback but then
81
+ * writes through `this.ctx.get("settings")?.replace(…)` on its UNSCOPED
82
+ * context, where `settings` is not injected — so the optional chain swallows
83
+ * the lookup and the call is a silent no-op (verified: it returns undefined
84
+ * and the settings file is untouched). We therefore write the installed
85
+ * section directly through our own injected `settings` service; the harness
86
+ * still reads it back through `installSection`'s fold, so behaviour is
87
+ * unchanged for every other consumer.
88
+ */
89
+ async #saveDefaultSelection(selection) {
90
+ const settings = this.runtime.ctx.get("settings");
91
+ await settings?.replace?.("agent-default-model", {
92
+ provider: selection.provider,
93
+ model: selection.model,
94
+ });
95
+ }
96
+ /** Shadow of OMP's `modelRoles.default` (`provider/model`) seen last tick. */
97
+ #ompDefaultKey = "";
98
+ /** Shadow of DSH's agentDefaultModel selection (`provider/model`) seen last tick. */
99
+ #dshSelectionKey = "";
100
+ constructor(ctx) {
101
+ super(ctx, "ompProvider");
102
+ this.runtime = { ctx };
103
+ ctx.effect(() => ctx.agents.setFactory(this), "ompProvider.setFactory()");
104
+ // Native-home ruling (2026-09-17): the sidecar/SDK runs on the operator's
105
+ // native OMP home (`~/.omp`; $OMP_HOME overrides in tests) — no app-home
106
+ // bootstrap, no config seeding. The world DSH home only holds DSH state.
107
+ this.#registerModelCatalog();
108
+ // Boot the centralized index BEFORE any service reads session state: warm
109
+ // pass + migration, so the first landing sees real titles/models (D5.1).
110
+ initBridgeStore();
111
+ void this.#syncModelDefaultTick();
112
+ // dsh-shape-session-log: the native jsonl backend (re-enabled in the patch)
113
+ // is the sole `sessionPersistence` — the plugin no longer provides a union
114
+ // persistence. Session list/page/replay come from native components; omp-web
115
+ // is only the producer (writes the dsh session log on live turns).
116
+ // The roster must be visible to the API gateway's root-level remote
117
+ // enumeration (dsh-host-apiproxy read it from the root service table);
118
+ // nesting it under a child fiber via ctx.plugin hides its @Remote routes
119
+ // from the gateway and every agentPresets/* call 404s. Register it on
120
+ // this plugin's own (top-level) fiber instead — its Service constructor
121
+ // ties teardown to this fiber via reflect.provide.
122
+ new SingleOmpPresetRoster(ctx);
123
+ // Drive the `agentPreset` session projection ourselves: the upstream
124
+ // registrant lives in the (disabled) dsh-agent-presets package, but the
125
+ // Web UI gates the preset chip and header label on
126
+ // `projectionValues.agentPreset`. Deferred via inject so the registry's
127
+ // service contract is honored (registers only once it exists).
128
+ ctx.inject(["sessionProjections"], (scoped) => {
129
+ scoped.sessionProjections.register(ompAgentPresetProjection);
130
+ });
131
+ ctx.effect(() => () => closeBridgeStore(), "ompProvider.storeClose()");
132
+ this.#reconcileWorkspaces();
133
+ // Mobile page-config boot script (host half): sets `window.__OMP_WEB_MOBILE__`
134
+ // + the iOS zoom-guard section. Fail-open — a missing webServer leaves the
135
+ // feature dormant, and an injection failure is logged, never thrown.
136
+ installMobileBootScript(ctx);
137
+ // OMP skills + slash commands into the host's `skills`/`commands` seams.
138
+ installOmpDiscovery(ctx);
139
+ }
140
+ /**
141
+ * Two-way default-model sync, evaluated once per reconcile tick (and once
142
+ * at boot): OMP's `modelRoles.default` (CLI) ↔ DSH's `agentDefaultModel`
143
+ * selection. TUI semantics on both sides — a default write survives
144
+ * sessions. Direction is decided by shadow keys: whichever side moved
145
+ * since the last tick wins, and an echo (same value round-tripping back)
146
+ * is a no-op.
147
+ */
148
+ async #syncModelDefaultTick() {
149
+ const service = this.runtime.ctx.get("agentDefaultModel");
150
+ // Source of truth is OMP's config.yml ON DISK, never the sidecar: the
151
+ // sidecar is cold at boot and this tick must publish OMP's default to
152
+ // `ctx.agentDefaultModel` immediately (archived default-model contract).
153
+ const ompParsed = readOmpDefaultModelFromConfig();
154
+ const ompKey = ompParsed === undefined ? undefined : `${ompParsed.provider}/${ompParsed.model}`;
155
+ const dsh = service?.currentSelection?.();
156
+ const dshKey = dsh === undefined ? undefined : `${dsh.provider}/${dsh.model}`;
157
+ try {
158
+ if (ompKey !== undefined && ompKey !== this.#ompDefaultKey) {
159
+ // OMP side moved (TUI default switch or config edit): carry it into
160
+ // the store and the DSH default so new Web sessions inherit it.
161
+ this.#ompDefaultKey = ompKey;
162
+ if (ompParsed !== undefined) {
163
+ getBridgeStore()?.setOmpDefaultModel(ompParsed.provider, ompParsed.model);
164
+ await this.#saveDefaultSelection(ompParsed);
165
+ this.#dshSelectionKey = ompKey;
166
+ this.runtime.ctx.logger.info(`omp-provider: default model ← OMP: ${ompKey}`);
167
+ }
168
+ }
169
+ else if (dshKey !== undefined && dshKey !== this.#dshSelectionKey) {
170
+ // DSH side moved (Web UI selector switch): propagate into OMP's
171
+ // modelRoles.default through the CLI so TUI sessions inherit it.
172
+ this.#dshSelectionKey = dshKey;
173
+ if (dsh !== undefined && dshKey !== this.#ompDefaultKey) {
174
+ // Guard: never write a selection OMP cannot serve into the operator's
175
+ // native config (e.g. dash's built-in `deepseek-official`, which no
176
+ // adapter serves) — that would poison `~/.omp/agent/config.yml`.
177
+ const served = loadOmpModels().some((candidate) => candidate.provider === dsh.provider && candidate.id === dsh.model);
178
+ if (!served) {
179
+ this.runtime.ctx.logger.warn(`omp-provider: refusing to write an unserved selection → OMP: ${dshKey}`);
180
+ return;
181
+ }
182
+ const next = { ...((await ompModelRoles()) ?? {}), default: `${dsh.provider}/${dsh.model}` };
183
+ if (await ompSetModelRoles(next)) {
184
+ this.#ompDefaultKey = dshKey;
185
+ this.runtime.ctx.logger.info(`omp-provider: default model → OMP: ${dshKey}`);
186
+ }
187
+ else {
188
+ this.runtime.ctx.logger.warn(`omp-provider: default model → OMP write failed: ${dshKey}`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ catch (error) {
194
+ this.runtime.ctx.logger.warn(`omp-provider: default model sync failed: ${String(error)}`);
195
+ }
196
+ }
197
+ /**
198
+ * Reconcile the Web UI workspace sidebar against the scanned OMP store.
199
+ *
200
+ * The upstream workspace registry (`dsh-workspace`) only auto-groups by cwd
201
+ * on its FIRST boot (`bootstrap`); every session appearing later is grouped
202
+ * solely by `workspace.attachSession`, which the apiproxy calls only during
203
+ * `create`. TUI-born OMP sessions never pass through `create`, so without
204
+ * this pass they land in the "Ungrouped" bucket even though their cwd is
205
+ * durable in the transcript. Attach each scanned session by its DASH-facing
206
+ * id (dev_0.0.3 §11: OMP ids never cross the bridge boundary) to the
207
+ * workspace owning its canonical cwd — idempotent, fail-soft, every boot.
208
+ */
209
+ #reconcileWorkspaces() {
210
+ this.runtime.ctx.inject(["workspaceRegistry"], (wctx) => {
211
+ const registry = wctx.get("workspaceRegistry");
212
+ if (registry === undefined)
213
+ return;
214
+ const run = () => {
215
+ const store = getBridgeStore();
216
+ if (store !== undefined)
217
+ prepareIndex(store);
218
+ void this.#syncModelDefaultTick();
219
+ void refreshOmpModelsCli();
220
+ void this.#attachScannedSessions(registry);
221
+ };
222
+ run();
223
+ // The registry groups by cwd only at ITS boot, so TUI-side sessions
224
+ // created later (the store gains files at any time) would sit in
225
+ // "Ungrouped" forever. Re-attach periodically: idempotent, fail-soft,
226
+ // and the scan is a stat pass over the store.
227
+ const timer = setInterval(run, STORAGE_RECONCILE_INTERVAL_MS);
228
+ timer.unref();
229
+ wctx.effect(() => () => clearInterval(timer), "ompProvider.reconcileTimer()");
230
+ });
231
+ }
232
+ /** One idempotent attach pass: every scanned session joins the workspace owning its canonical cwd. */
233
+ async #attachScannedSessions(registry) {
234
+ try {
235
+ const excluded = new Set();
236
+ for (const home of [
237
+ OMP_NATIVE_HOME,
238
+ process.env.DSH_HOME ?? join(homedir(), ".omp", "dsh"),
239
+ ]) {
240
+ try {
241
+ excluded.add(realpathSync(home));
242
+ }
243
+ catch {
244
+ // Unresolvable home: nothing to exclude.
245
+ }
246
+ }
247
+ const groups = new Map();
248
+ const store = getBridgeStore();
249
+ if (store === undefined)
250
+ return;
251
+ for (const row of store.list()) {
252
+ const cwd = validatedCwd(row.cwd ?? undefined);
253
+ if (cwd === undefined || excluded.has(cwd))
254
+ continue;
255
+ const id = row.dsh_session_id;
256
+ const ids = groups.get(cwd);
257
+ if (ids === undefined)
258
+ groups.set(cwd, [id]);
259
+ else if (!ids.includes(id))
260
+ ids.push(id);
261
+ }
262
+ for (const [path, ids] of groups) {
263
+ const workspace = (await registry.resolveByPath(path)) ?? (await registry.create(path));
264
+ for (const id of ids) {
265
+ if (!workspace.sessionIds.includes(id))
266
+ await workspace.attachSession(id);
267
+ }
268
+ }
269
+ }
270
+ catch (error) {
271
+ this.runtime.ctx.logger.warn(`omp-provider: workspace reconciliation failed: ${String(error)}`);
272
+ }
273
+ }
274
+ /**
275
+ * Register the OMP-backed LLM adapter so the browser model selector (an RPC
276
+ * round-trip through the apiproxy reading `ctx.llm.*`) advertises OMP's real
277
+ * providers and models. The registration is tied to the LLM runtime's own
278
+ * lifecycle, mirroring `llm-deepseek`/`llm-pi-ai` (which the profile disables).
279
+ */
280
+ #registerModelCatalog() {
281
+ const llm = this.runtime.ctx.get("llm");
282
+ if (llm === undefined)
283
+ return;
284
+ const providers = ompProviderIds();
285
+ if (providers.length === 0) {
286
+ this.runtime.ctx.logger.warn("omp-provider: no OMP models discovered; the model selector will be empty");
287
+ return;
288
+ }
289
+ llm.registerAdapter(providers, new OmpLlmAdapter());
290
+ }
291
+ async createAgent(ownerCtx, options) {
292
+ const loopCtx = this.runtime.ctx;
293
+ const id = options.sessionId;
294
+ const meta = options.meta ?? {};
295
+ const cwd = meta.cwd;
296
+ // Fork (the apiproxy's `session.fork`) seeds a copied-turn prefix but OMP
297
+ // has no native fork: the child spawned below would start with an EMPTY
298
+ // context while the UI renders the copied history, and after a restart
299
+ // even that history would vanish (OMP's transcript holds only post-fork
300
+ // traffic). Fail the fork cleanly at the factory boundary instead of
301
+ // splitting the two surfaces. `parentSession` in create metadata is set
302
+ // only by the fork path (subagents go through ctx.subagents instead).
303
+ if (meta.parentSession !== undefined) {
304
+ throw new Error(`cannot fork session "${meta.parentSession}" onto the OMP provider: OMP has no native session fork`);
305
+ }
306
+ // Approval policy is pinned at OMP launch and the Dash session does not
307
+ // exist yet at this point (it is prepared + announced — which is what
308
+ // stamps `permission/preset` via pinInitialPermission — only inside
309
+ // setupAndPublish, AFTER this spawn), so the effective preset cannot
310
+ // come from session events: read the permission service's default and
311
+ // map it onto the launch flag. OMP_APPROVAL_MODE (headless runs) wins.
312
+ const envMode = envApprovalMode();
313
+ const preset = defaultPermissionPreset(loopCtx);
314
+ const approvalMode = envMode ?? ompApprovalMode(preset);
315
+ trace(`create id=${id} preset=${preset ?? "none"} approval-mode=${approvalMode}${envMode === undefined ? "" : " (env override)"}`);
316
+ // Backend takes ZERO part in the UI's browser-local new-session draft:
317
+ // no OMP child, no transcript, no index row, no session events — the
318
+ // session announces blank, exactly like the native agent-loop factory's
319
+ // in-process agent. The child materializes on the first prompt
320
+ // (LazyOmpRpc); adoptSpawnedChild writes the index row and the
321
+ // supervisor's held baseline at that moment, so a draft abandoned before
322
+ // the first prompt costs and leaves NOTHING server-side.
323
+ const rpc = new LazyOmpRpc(["--approval-mode", approvalMode], cwd);
324
+ const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
325
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
326
+ ...(meta === undefined ? {} : { meta }),
327
+ }));
328
+ // The prepared header is the single source of truth for the row the
329
+ // first-spawn adoption writes: v2 folds header identity across
330
+ // live/listed/loaded observations, so created_at/cwd must be copied from
331
+ // it verbatim (an independent Date.now() would SOURCE_CONFLICT the id).
332
+ const header = preparation.session.header;
333
+ // dsh-shape-session-log (producer role): open the native dsh session log
334
+ // write handle now. The jsonl backend routes live `session/event` appends
335
+ // into this handle by session id; we own the handle and close it on teardown
336
+ // (mirrors agent-loop's createStoredSession).
337
+ const persistence = loopCtx.get("sessionPersistence");
338
+ const stored = persistence === undefined
339
+ ? undefined
340
+ : { handle: await persistence.create(header, { inheritedEventCount: preparation.session.inheritedEventCount }) };
341
+ rpc.onSpawned((client) => {
342
+ void adoptSpawnedChild(id, header, client, preset);
343
+ });
344
+ // Nothing can fail from the child here (none exists yet); the publish
345
+ // transaction below unwinds its own half-published state on failure — its
346
+ // catch calls rpc.close(), a no-op until the first prompt spawns.
347
+ const handle = await setupAndPublish(loopCtx, ownerCtx, id, options.agentOptions ?? {}, options.setup, preparation.session, rpc, options.parentAgent, "startup", { enterSession: true, preparation, stored });
348
+ return this.registerHeld(id, "", handle);
349
+ }
350
+ async resume(ownerCtx, options) {
351
+ const loopCtx = this.runtime.ctx;
352
+ const id = options.resumeSessionId;
353
+ // dsh-shape-session-log (dual storage): the dsh session log is the replay
354
+ // of record; the OMP app-home session file is the live runtime's OWN
355
+ // context. Resume opens BOTH — never feed the old dsh transcript back to
356
+ // OMP: OMP re-opens its own file and only receives the NEW user prompt.
357
+ const persistence = loopCtx.get("sessionPersistence");
358
+ if (persistence === undefined) {
359
+ throw new Error(`cannot resume session "${id}": session persistence is not configured`);
360
+ }
361
+ // dsh side: take write ownership of the dsh log and read its full seed.
362
+ const sig = options.signal === undefined ? undefined : { signal: options.signal };
363
+ let stored;
364
+ let coldRead;
365
+ try {
366
+ stored = await persistence.open(id, "write", sig);
367
+ coldRead = await stored.read(0, undefined, sig);
368
+ }
369
+ catch (error) {
370
+ throw new Error(`cannot resume session "${id}": dsh session log is unreadable (${String(error)})`);
371
+ }
372
+ // OMP side: locate the app-home session file via the bridge-store pairing
373
+ // recorded at first spawn (adoptSpawnedChild stores the SDK-reported file).
374
+ // A BLANK draft has no OMP counterpart and no context to resume: the host
375
+ // resumes any persisted id (createOrAdopt), and the UI's new-chat persists
376
+ // an empty session first — so start a FRESH OMP session for it instead of
377
+ // failing. A session with real content has no OMP context we may feed the
378
+ // runtime (never replay the dsh transcript into OMP): fail loudly.
379
+ const record = resolveEntryById(id);
380
+ trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.ompSessionFile}`);
381
+ const hasContent = coldRead.events.some((event) => event.type === "turn/start" || event.type === "user/message" || event.type === "assistant/message");
382
+ if (record === undefined && hasContent) {
383
+ throw new Error(`cannot resume session "${id}": no OMP session is recorded for this Dash session id`);
384
+ }
385
+ const freshOmp = record === undefined;
386
+ const ompFile = record?.ompSessionFile;
387
+ const spawnCwd = validatedCwd(record?.cwd) ?? validatedCwd(stored.header.cwd);
388
+ // Approval mode: persisted permission preset from the dsh log seed
389
+ // (synthesized at create), falling back to the bridge-store row.
390
+ const envMode = envApprovalMode();
391
+ const preset = getBridgeStore()?.byDshId(String(id))?.permission_preset ??
392
+ presetFromEvents([...coldRead.events]);
393
+ const approvalMode = envMode ?? ompApprovalMode(preset);
394
+ trace(`resume id=${id} preset=${preset ?? "none"} approval-mode=${approvalMode}${envMode === undefined ? "" : " (env override)"}${freshOmp ? " fresh-omp" : ""}`);
395
+ // OMP live runtime — ALWAYS LAZY (sidecar-decoupling ruling, both branches).
396
+ // Opening/replaying a persisted session in the WebUI promotes it through
397
+ // history.follow → resume; a read-only visit must NOT materialize the OMP
398
+ // child. `LazyOmpRpc` spawns on the FIRST dispatch (prompt/followUp/steer/
399
+ // ensureStarted in #startTurn); passive queries synthesize without a child.
400
+ // Blank draft: no app-home file exists yet — the child adopts a fresh OMP
401
+ // session on first prompt (adoptSpawnedChild then writes the mapping).
402
+ // With content: the child re-attaches its OWN app-home file via --resume
403
+ // (never the dsh transcript); the mapping already exists in the bridge
404
+ // store, so no adoption hook is needed.
405
+ let rpc;
406
+ if (ompFile === undefined) {
407
+ const lazy = new LazyOmpRpc(["--approval-mode", approvalMode], spawnCwd);
408
+ lazy.onSpawned((client) => { void adoptSpawnedChild(id, stored.header, client, preset); });
409
+ rpc = lazy;
410
+ }
411
+ else {
412
+ rpc = new LazyOmpRpc(["--approval-mode", approvalMode, "--resume", ompFile], spawnCwd);
413
+ }
414
+ try {
415
+ // Fresh attach: seed the dsh session from the dsh log (native replay),
416
+ // header identity folded from the stored header verbatim.
417
+ const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
418
+ seed: [...coldRead.events],
419
+ meta: structuredClone(stored.header),
420
+ inheritedEventCount: stored.inheritedEventCount,
421
+ eventState: coldRead.eventState,
422
+ }));
423
+ trace(`resume id=${id} seeded from dsh session log (${coldRead.events.length} events)`);
424
+ const handle = await setupAndPublish(loopCtx, ownerCtx, id, options.agentOptions ?? {}, options.setup, preparation.session, rpc, options.parentAgent, "resume", { enterSession: true, preparation, stored: { handle: stored, written: coldRead.events.length } });
425
+ return this.registerHeld(id, ompFile ?? "", handle);
426
+ }
427
+ catch (error) {
428
+ trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
429
+ await stored.close().catch(() => { });
430
+ rpc.close();
431
+ throw error;
432
+ }
433
+ }
434
+ /**
435
+ * Track a published RPC-backed agent and hand the session to the supervisor
436
+ * as held. Arrow FIELD (not a `#` method): the provider is exposed through
437
+ * a Cordis tracing proxy, and hard-private methods fail their brand check
438
+ * on the proxy receiver (the same reason setupAndPublish is module-level).
439
+ */
440
+ registerHeld = (id, file, handle) => {
441
+ const wrapped = {
442
+ agent: handle.agent,
443
+ dispose: async () => {
444
+ this.heldAgents.delete(String(id));
445
+ await handle.dispose();
446
+ },
447
+ };
448
+ this.heldAgents.set(String(id), wrapped);
449
+ return wrapped;
450
+ };
451
+ }
452
+ /**
453
+ * Shared creation/resume transaction: build the agent over the prepared
454
+ * session, run unpublished setup, and publish both in order. Kept as a
455
+ * module-level function (not a private method) because Cordis exposes the
456
+ * provider through a tracing proxy, which breaks hard-private (`#`) receivers.
457
+ */
458
+ /**
459
+ * First-spawn adoption for a lazily created session: the OMP child exists only
460
+ * from the first prompt on, so the bridge index row and the supervisor's held
461
+ * baseline are written HERE instead of at create time. The row copies the
462
+ * prepared header's created_at/cwd verbatim — v2 folds header identity across
463
+ * live/listed/loaded observations, and an independent timestamp would make
464
+ * session/query throw SOURCE_CONFLICT for the id. Fail-soft: an adoption
465
+ * failure degrades that session's metadata (the next reconcile pass retries
466
+ * against the transcript), never the prompt itself.
467
+ */
468
+ async function adoptSpawnedChild(id, header, client, preset) {
469
+ try {
470
+ const state = await client.getState();
471
+ const file = state.sessionFile;
472
+ if (file === undefined || file === "")
473
+ return;
474
+ const store = getBridgeStore();
475
+ const ompId = state.sessionId ?? sessionHeaderId(file);
476
+ if (store !== undefined && ompId !== undefined) {
477
+ upsertCreated(store, {
478
+ ompSessionId: ompId,
479
+ sessionFile: file,
480
+ dshSessionId: String(id),
481
+ createdAt: header.createdAt,
482
+ cwd: header.cwd,
483
+ ...(preset === undefined ? {} : { preset }),
484
+ });
485
+ }
486
+ trace(`adoptSpawnedChild id=${id} file=${file}`);
487
+ }
488
+ catch (error) {
489
+ trace(`adoptSpawnedChild id=${id} failed: ${String(error)}`);
490
+ }
491
+ }
492
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, rpc, parentAgent, source, opts) {
493
+ let detachSession;
494
+ let detachAgent;
495
+ let agent;
496
+ // Late-bound teardown the agent triggers itself after its idle window
497
+ // expires (see OmpAgent's idle exit) — defined only after publication.
498
+ let idleExit;
499
+ try {
500
+ // Build the agent shim over the session and the live RPC client. For a
501
+ // promoted shadow the session is already entered+announced; enterSession
502
+ // is false and the projection is reused (seq-continuous promotion).
503
+ agent = new OmpAgent(loopCtx, id, agentOptions, session, rpc, () => idleExit?.());
504
+ // The live system prompt is NOT stamped here anymore: the eager append
505
+ // turned the UI's new-session draft into a non-blank session and broke
506
+ // the composer. It is committed on the first turn by OmpAgent's
507
+ // #bootstrapSessionIdentity, after the lazy child spawns — surface node 0
508
+ // ahead of turn/start, the native loop's step() ordering. A replayed or
509
+ // resumed session seeds its own system/message from the transcript.
510
+ // Composition-only setup on the unpublished agent scope.
511
+ const commit = await setup?.(agent.ctx, agent);
512
+ // Publish: enter both session and agent, announce in order, then signal
513
+ // session-start. The commit runs immediately before publication.
514
+ commit?.commit();
515
+ // dsh-shape-session-log (producer role): flush events appended BEFORE
516
+ // publication that never emit through `session/event` (constructor seeds
517
+ // and the restore path's `session/end-seed`). Without this the jsonl
518
+ // writer would see a gap (expected N, got N+1) and halt the drain.
519
+ // Mirrors agent-loop's `appendUnstoredSuffix`.
520
+ if (opts.stored !== undefined) {
521
+ const written = opts.stored.written ?? 0;
522
+ const suffix = session.snapshotEvents(SessionLogOffset(written));
523
+ if (suffix.length > 0)
524
+ await opts.stored.handle.append(suffix);
525
+ opts.stored.written = written + suffix.length;
526
+ }
527
+ if (opts.enterSession) {
528
+ detachSession = agent.ctx.sessions.enter(session);
529
+ agent.ctx.sessions.announce(session);
530
+ }
531
+ detachAgent = loopCtx.agents.enter(agent, parentAgent);
532
+ loopCtx.agents.announce(agent);
533
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
534
+ let disposed = false;
535
+ let unfollowOwner;
536
+ const dispose = async () => {
537
+ if (disposed)
538
+ return;
539
+ disposed = true;
540
+ trace(`dispose() called for agent ${id} (source=${source}) stack=${new Error().stack?.split("\n").slice(1, 4).join(" <- ")}`);
541
+ unfollowOwner?.();
542
+ await agent?.dispose();
543
+ await opts.stored?.handle.close().catch(() => { });
544
+ detachAgent?.();
545
+ detachSession?.();
546
+ };
547
+ idleExit = () => void dispose();
548
+ // Follow the owner: a caller-fiber unload tears this agent down.
549
+ unfollowOwner = ownerCtx.effect(() => () => {
550
+ void dispose();
551
+ }, `ompProvider.lifecycle(${id})`);
552
+ return { agent, dispose };
553
+ }
554
+ catch (error) {
555
+ // Unwind the half-published transaction. A failure between `enter` and
556
+ // the announcements (e.g. a persistence listener rejecting the session)
557
+ // must not leave a live-but-dead entry the API resolver would serve.
558
+ trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
559
+ detachAgent?.();
560
+ detachSession?.();
561
+ void agent?.dispose().catch(() => { });
562
+ rpc.close();
563
+ throw error;
564
+ }
565
+ finally {
566
+ // Release the preparation's per-id reservation on every path (mirrors
567
+ // the reference loop's unconditional dispose): the session it seeded is
568
+ // already published above, so a same-process re-resume of this id can
569
+ // prepare again instead of colliding with a leaked reservation. Promotion
570
+ // has no preparation (the shadow's projection persists across the call).
571
+ opts.preparation?.[Symbol.dispose]();
572
+ }
573
+ }
574
+ export default OmpProvider;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Env-tunable knobs.
3
+ *
4
+ * dsh-shape-session-log retired the union-era supervisor cadence and replay
5
+ * cache knobs along with the supervisor and the union persistence.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ function parseMs(raw, fallback) {
10
+ if (raw === undefined || raw.trim() === "")
11
+ return fallback;
12
+ const value = Number(raw);
13
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
14
+ }
15
+ /** Bridge-index tick: workspace attach + model-default sync + model refresh. */
16
+ export const STORAGE_RECONCILE_INTERVAL_MS = parseMs(process.env.OMP_STORAGE_RECONCILE_INTERVAL_MS, 30_000);
17
+ /**
18
+ * Slash-command discovery refresh: the disk scan (omp+claude roots) re-mounts
19
+ * the host `commands` registrations on this cadence — boot once, then hourly.
20
+ * Pure disk reads, never a sidecar spawn. `0` disables the refresh.
21
+ */
22
+ export const OMP_DISCOVERY_REFRESH_INTERVAL_MS = parseMs(process.env.OMP_DISCOVERY_REFRESH_INTERVAL_MS, 3_600_000);
23
+ /**
24
+ * The OMP home — the SDK runtime app home (`agentDir`): runtime data
25
+ * (agent.db / models.db / sessions) and config live here. NATIVE by default
26
+ * (user ruling 2026-09-17, pi precedent):
27
+ *
28
+ * OMP_HOME = $OMP_HOME (tests / adapter-level isolation) ?? ~/.omp
29
+ *
30
+ * The installed OMP app keeps its own home; the adapter never redirects it at
31
+ * spawn and never seeds config into it. `<dshHome>/agents/omp` remains the
32
+ * world's DSH home (its sessions/storages/settings) — it is NOT the app home
33
+ * anymore. The old nested derivation (`dshHomePath("agents", "omp", ".omp")`)
34
+ * and the config copy-in (`ensureOmpAppHome`) are retired with the same
35
+ * ruling; `OMP_HOME` stays overridable per the 2026-09-15 adapter-level
36
+ * freedom (optional isolation, never the default).
37
+ */
38
+ export const OMP_HOME = process.env.OMP_HOME ?? resolveOmpNativeHome();
39
+ /**
40
+ * The operator's NATIVE OMP home (`~/.omp`): the TUI/CLI installation owning
41
+ * `agent/config.yml`, `agent/models.yml` and the native session store. Since
42
+ * the 2026-09-17 native-home ruling this is ALSO the default runtime app home
43
+ * ({@link OMP_HOME}); the knob stays as a late env override seam (tests may
44
+ * pin disk-scan roots via `$OMP_NATIVE_HOME` independently of `$OMP_HOME`).
45
+ */
46
+ export function resolveOmpNativeHome() {
47
+ return process.env.OMP_NATIVE_HOME ?? join(homedir(), ".omp");
48
+ }
49
+ /** Boot-time snapshot of {@link resolveOmpNativeHome}. */
50
+ export const OMP_NATIVE_HOME = resolveOmpNativeHome();