super-dsh 0.1.9 → 0.1.10-e

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/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-claude/dist/adapter.js +106 -31
  41. package/agent-claude/dist/agent.js +154 -17
  42. package/agent-claude/dist/claude-client.js +84 -43
  43. package/agent-claude/dist/claude-launch.js +101 -0
  44. package/agent-claude/dist/claude-native-config.js +169 -0
  45. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  46. package/agent-claude/dist/claude-providers.js +391 -0
  47. package/agent-claude/dist/claude-route.js +152 -0
  48. package/agent-claude/dist/index.js +214 -37
  49. package/agent-claude/dist/models.js +14 -0
  50. package/agent-claude/dist/sdk-runtime.js +320 -0
  51. package/agent-claude/package.json +6 -4
  52. package/agent-codex/README.md +141 -36
  53. package/agent-codex/cordis.patch.yml +14 -10
  54. package/agent-codex/dist/adapter.js +64 -28
  55. package/agent-codex/dist/agent.js +255 -81
  56. package/agent-codex/dist/app-server-approvals.js +165 -0
  57. package/agent-codex/dist/app-server-client.js +723 -0
  58. package/agent-codex/dist/app-server-events.js +432 -0
  59. package/agent-codex/dist/app-server-protocol.js +95 -0
  60. package/agent-codex/dist/app-server-wire.js +299 -0
  61. package/agent-codex/dist/codex-launch.js +78 -0
  62. package/agent-codex/dist/codex-native-config.js +392 -0
  63. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex/dist/codex-providers.js +376 -0
  65. package/agent-codex/dist/codex-route.js +86 -0
  66. package/agent-codex/dist/index.js +256 -115
  67. package/agent-codex/dist/permission.js +75 -1
  68. package/agent-codex/package.json +1 -2
  69. package/agent-codex-sdk/README.md +79 -0
  70. package/agent-codex-sdk/cordis.patch.yml +81 -0
  71. package/agent-codex-sdk/dist/adapter.js +91 -0
  72. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  73. package/agent-codex-sdk/dist/agent.js +1085 -0
  74. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  75. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  76. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  77. package/agent-codex-sdk/dist/codex-route.js +86 -0
  78. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  79. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  80. package/agent-codex-sdk/dist/codex-store.js +180 -0
  81. package/agent-codex-sdk/dist/inbox.js +53 -0
  82. package/agent-codex-sdk/dist/index.js +739 -0
  83. package/agent-codex-sdk/dist/permission.js +71 -0
  84. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  85. package/agent-codex-sdk/dist/session-map.js +167 -0
  86. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  87. package/agent-codex-sdk/package.json +79 -0
  88. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  89. package/agent-omp/README.md +72 -55
  90. package/agent-omp/dist/agent.js +36 -11
  91. package/agent-omp/dist/index.js +92 -52
  92. package/agent-omp/dist/lazy-rpc.js +11 -4
  93. package/agent-omp/dist/omp-cli.js +114 -97
  94. package/agent-omp/dist/omp-store.js +12 -4
  95. package/agent-omp/dist/permission.js +70 -5
  96. package/agent-omp/dist/rpc-client.js +409 -0
  97. package/agent-omp/dist/world-plugin.js +36 -3
  98. package/agent-omp/package.json +2 -6
  99. package/agent-omp-sdk/README.md +103 -0
  100. package/agent-omp-sdk/cordis.patch.yml +108 -0
  101. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  102. package/agent-omp-sdk/dist/adapter.js +36 -0
  103. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  104. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  105. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  106. package/agent-omp-sdk/dist/agent.js +1178 -0
  107. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  108. package/agent-omp-sdk/dist/discovery.js +116 -0
  109. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  110. package/agent-omp-sdk/dist/inbox.js +53 -0
  111. package/agent-omp-sdk/dist/index.d.ts +60 -0
  112. package/agent-omp-sdk/dist/index.js +632 -0
  113. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  114. package/agent-omp-sdk/dist/knobs.js +50 -0
  115. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  116. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  117. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  118. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  119. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  120. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  121. package/agent-omp-sdk/dist/models.d.ts +39 -0
  122. package/agent-omp-sdk/dist/models.js +299 -0
  123. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  124. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  125. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  126. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  127. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  128. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  129. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  130. package/agent-omp-sdk/dist/omp-store.js +526 -0
  131. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  132. package/agent-omp-sdk/dist/pairing.js +45 -0
  133. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  134. package/agent-omp-sdk/dist/permission.js +161 -0
  135. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  136. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  137. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  138. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  139. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  140. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  141. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  142. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  143. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  144. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  145. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  146. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  147. package/agent-omp-sdk/dist/store/db.js +210 -0
  148. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  149. package/agent-omp-sdk/dist/store/index.js +59 -0
  150. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  151. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  152. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  153. package/agent-omp-sdk/dist/store/schema.js +40 -0
  154. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  155. package/agent-omp-sdk/dist/transport.js +35 -0
  156. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  157. package/agent-omp-sdk/dist/world-home.js +23 -0
  158. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  159. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  160. package/agent-omp-sdk/package.json +87 -0
  161. package/client/index.js +1204 -93
  162. package/cordis.patch.yml +64 -0
  163. package/package.json +17 -8
  164. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  165. package/superd-hub/dist/client/agent-settings.js +300 -0
  166. package/superd-hub/dist/client/roster.js +6 -0
  167. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  168. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  169. package/superd-hub/dist/client/switch.d.ts +165 -0
  170. package/superd-hub/dist/client/switch.js +295 -0
  171. package/superd-hub/lib/client/index.js +1204 -93
  172. package/agent-codex/dist/models.js +0 -152
  173. package/agent-omp/dist/sidecar-client.js +0 -195
  174. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  175. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  176. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  177. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  178. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * dsh-omp-provider — OMP provider plugin for DeepSeek Harness.
3
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
4
+ * Replaces the built-in agent loop with an `AgentFactory` that spawns
5
+ * `omp --mode rpc` per session (RPC transport: `LazyOmpRpc` → `OmpRpcClient`)
6
+ * and bridges its event stream into the Dash Agent/Session contracts. The OMP
7
+ * runtime is the operator's own `omp` binary — the adapter bundles no OMP copy.
8
+ * Mirrors `@deepseek-ai/dsh-agent-loop`'s creation transaction
9
9
  * (prepare → setup → publish) so the session + agent publish as one ordered
10
10
  * lifecycle.
11
11
  *
12
12
  * Phase 3 adds resume:
13
13
  * - `createAgent` records the Dash→OMP session identity in `OmpSessionIndex`
14
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
15
+ * - `resume` re-attaches the OMP session file through the RPC child (`--resume`) and restores
16
16
  * the persisted Dash session log (`sessionPersistence.prepare`), so the
17
17
  * Web UI's cold listing/history/follow-up path (`session.list`,
18
18
  * `session.history`, `session.prompt` → `agents.resume`) reopens the
@@ -33,7 +33,7 @@ import { readOmpDefaultModelFromConfig, sessionHeaderId } from "./omp-store.js";
33
33
  import { ompModelRoles, ompSetModelRoles } from "./omp-cli.js";
34
34
  import { loadOmpModels, ompProviderIds, refreshOmpModelsCli } from "./models.js";
35
35
  import { OMP_NATIVE_HOME, STORAGE_RECONCILE_INTERVAL_MS } from "./knobs.js";
36
- import { defaultPermissionPreset, envApprovalMode, ompApprovalMode, presetFromEvents } from "./permission.js";
36
+ import { defaultPermissionPreset, envApprovalMode, resolveSpawnPermission } from "./permission.js";
37
37
  import { closeBridgeStore, getBridgeStore, initBridgeStore } from "./store/index.js";
38
38
  import { prepareIndex, upsertCreated } from "./store/reconcile.js";
39
39
  import { installMobileBootScript } from "./mobile-boot.js";
@@ -55,6 +55,28 @@ const trace = (...parts) => {
55
55
  if (process.env.OMP_TRACE === "1")
56
56
  process.stderr.write(`[omp-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
57
57
  };
58
+ /**
59
+ * Whether any live OMP session currently holds an `omp --mode rpc` child.
60
+ *
61
+ * The reconcile tick used to read the catalog through the shared sidecar's
62
+ * live gate; with the RPC transport the equivalent fact is "a session's
63
+ * transport child exists". `refreshOmpModelsCli()` is a real
64
+ * `omp models --json` subprocess, so this gate is load-bearing (AGENTS.md
65
+ * §五·五: the periodic tick never materializes an `omp`). Sessions count only
66
+ * once they actually spawned — a draft session holds a `LazyOmpRpc` with no
67
+ * child yet, and an idle daemon's tick must pull ZERO children.
68
+ *
69
+ * Exported beside the plugin (the cordis surface itself is unchanged) so the
70
+ * no-sidecar guard can pin this gate's truth table behaviourally — a shape
71
+ * regex alone cannot see an empty input set.
72
+ */
73
+ export function liveOmpSessionExists(sessions) {
74
+ for (const rpc of sessions) {
75
+ if (rpc.spawned)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
58
80
  export class OmpProvider extends Service {
59
81
  /**
60
82
  * `agentDefaultModel` is load-bearing, not decorative: the harness owns the
@@ -71,6 +93,12 @@ export class OmpProvider extends Service {
71
93
  /** Stops the supervisor's follow loop on provider teardown. */
72
94
  /** Live RPC-backed agents by dash id, for avoidance hand-off. */
73
95
  heldAgents = new Map();
96
+ /**
97
+ * Live RPC wrappers by dash id — the reconcile tick's liveness gate (AGENTS.md
98
+ * §五·五): the catalog refresh runs only while one of these holds an
99
+ * `omp --mode rpc` child (`spawned`). Entries are dropped with the handle.
100
+ */
101
+ liveSessions = new Set();
74
102
  /**
75
103
  * Persist the default selection into the settings section.
76
104
  *
@@ -99,9 +127,10 @@ export class OmpProvider extends Service {
99
127
  super(ctx, "ompProvider");
100
128
  this.runtime = { ctx };
101
129
  ctx.effect(() => ctx.agents.setFactory(this), "ompProvider.setFactory()");
102
- // Native-home ruling (2026-09-17): the sidecar/SDK runs on the operator's
103
- // native OMP home (`~/.omp`; $OMP_HOME overrides in tests) — no app-home
104
- // bootstrap, no config seeding. The world DSH home only holds DSH state.
130
+ // Native-home ruling (2026-09-17): the spawned `omp` child runs on the
131
+ // operator's native OMP home (`~/.omp`; $OMP_HOME overrides in tests) — no
132
+ // app-home bootstrap, no config seeding. The world DSH home only holds DSH
133
+ // state.
105
134
  this.#registerModelCatalog();
106
135
  // Boot the centralized index BEFORE any service reads session state: warm
107
136
  // pass + migration, so the first landing sees real titles/models (D5.1).
@@ -136,9 +165,9 @@ export class OmpProvider extends Service {
136
165
  */
137
166
  async #syncModelDefaultTick() {
138
167
  const service = this.runtime.ctx.get("agentDefaultModel");
139
- // Source of truth is OMP's config.yml ON DISK, never the sidecar: the
140
- // sidecar is cold at boot and this tick must publish OMP's default to
141
- // `ctx.agentDefaultModel` immediately (archived default-model contract).
168
+ // Source of truth is OMP's config.yml ON DISK, never the live transport:
169
+ // the `omp` child is cold at boot and this tick must publish OMP's default
170
+ // to `ctx.agentDefaultModel` immediately (archived default-model contract).
142
171
  const ompParsed = readOmpDefaultModelFromConfig();
143
172
  const ompKey = ompParsed === undefined ? undefined : `${ompParsed.provider}/${ompParsed.model}`;
144
173
  const dsh = service?.currentSelection?.();
@@ -205,7 +234,12 @@ export class OmpProvider extends Service {
205
234
  if (store !== undefined)
206
235
  prepareIndex(store);
207
236
  void this.#syncModelDefaultTick();
208
- void refreshOmpModelsCli();
237
+ // AGENTS.md §五·五 "tick 永不 spawn": the catalog refresh runs a real
238
+ // `omp models --json` subprocess, so it may only fire while an OMP
239
+ // session is live. An idle daemon's tick pulls ZERO children; the
240
+ // on-disk model cache (models.ts) keeps serving the boot catalog.
241
+ if (liveOmpSessionExists(this.liveSessions))
242
+ void refreshOmpModelsCli();
209
243
  void this.#attachScannedSessions(registry);
210
244
  };
211
245
  run();
@@ -292,16 +326,14 @@ export class OmpProvider extends Service {
292
326
  if (meta.parentSession !== undefined) {
293
327
  throw new Error(`cannot fork session "${meta.parentSession}" onto the OMP provider: OMP has no native session fork`);
294
328
  }
295
- // Approval policy is pinned at OMP launch and the Dash session does not
296
- // exist yet at this point (it is prepared + announced — which is what
297
- // stamps `permission/preset` via pinInitialPermission — only inside
298
- // setupAndPublish, AFTER this spawn), so the effective preset cannot
299
- // come from session events: read the permission service's default and
300
- // map it onto the launch flag. OMP_APPROVAL_MODE (headless runs) wins.
301
- const envMode = envApprovalMode();
302
- const preset = defaultPermissionPreset(loopCtx);
303
- const approvalMode = envMode ?? ompApprovalMode(preset);
304
- trace(`create id=${id} preset=${preset ?? "none"} approval-mode=${approvalMode}${envMode === undefined ? "" : " (env override)"}`);
329
+ // Approval policy is pinned at OMP launch — but it is NOT frozen here.
330
+ // The Dash session is prepared (and `pinInitialPermission`-stamped) only
331
+ // inside setupAndPublish, AFTER this wrapper exists, and the new-chat
332
+ // `/permission` pick arrives as a later `permission/preset`; the child
333
+ // materializes on the first prompt. The launch flag is therefore resolved
334
+ // at spawn time from the session's own events
335
+ // (`resolveSpawnPermission`), with the composition default as the fallback
336
+ // and OMP_APPROVAL_MODE (headless runs) as the override.
305
337
  // Backend takes ZERO part in the UI's browser-local new-session draft:
306
338
  // no OMP child, no transcript, no index row, no session events — the
307
339
  // session announces blank, exactly like the native agent-loop factory's
@@ -309,11 +341,13 @@ export class OmpProvider extends Service {
309
341
  // (LazyOmpRpc); adoptSpawnedChild writes the index row and the
310
342
  // supervisor's held baseline at that moment, so a draft abandoned before
311
343
  // the first prompt costs and leaves NOTHING server-side.
312
- const rpc = new LazyOmpRpc(["--approval-mode", approvalMode], cwd);
313
344
  const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
314
345
  ...(options.seed === undefined ? {} : { seed: options.seed }),
315
346
  ...(meta === undefined ? {} : { meta }),
316
347
  }));
348
+ const session = preparation.session;
349
+ const spawnPermission = () => resolveSpawnPermission(session.snapshotEvents(), defaultPermissionPreset(loopCtx), envApprovalMode());
350
+ const rpc = new LazyOmpRpc(() => ["--approval-mode", spawnPermission().approvalMode], cwd);
317
351
  // The prepared header is the single source of truth for the row the
318
352
  // first-spawn adoption writes: v2 folds header identity across
319
353
  // live/listed/loaded observations, so created_at/cwd must be copied from
@@ -328,13 +362,13 @@ export class OmpProvider extends Service {
328
362
  ? undefined
329
363
  : { handle: await persistence.create(header, { inheritedEventCount: preparation.session.inheritedEventCount }) };
330
364
  rpc.onSpawned((client) => {
331
- void adoptSpawnedChild(id, header, client, preset);
365
+ void adoptSpawnedChild(id, header, client, spawnPermission().preset);
332
366
  });
333
367
  // Nothing can fail from the child here (none exists yet); the publish
334
368
  // transaction below unwinds its own half-published state on failure — its
335
369
  // catch calls rpc.close(), a no-op until the first prompt spawns.
336
- const handle = await setupAndPublish(loopCtx, ownerCtx, id, options.agentOptions ?? {}, options.setup, preparation.session, rpc, options.parentAgent, "startup", { enterSession: true, preparation, stored });
337
- return this.registerHeld(id, "", handle);
370
+ const handle = await setupAndPublish(loopCtx, ownerCtx, id, options.agentOptions ?? {}, options.setup, session, rpc, options.parentAgent, "startup", { enterSession: true, preparation, stored });
371
+ return this.registerHeld(id, "", handle, rpc);
338
372
  }
339
373
  async resume(ownerCtx, options) {
340
374
  const loopCtx = this.runtime.ctx;
@@ -374,14 +408,12 @@ export class OmpProvider extends Service {
374
408
  const freshOmp = record === undefined;
375
409
  const ompFile = record?.ompSessionFile;
376
410
  const spawnCwd = validatedCwd(record?.cwd) ?? validatedCwd(stored.header.cwd);
377
- // Approval mode: persisted permission preset from the dsh log seed
378
- // (synthesized at create), falling back to the bridge-store row.
379
- const envMode = envApprovalMode();
380
- const preset = getBridgeStore()?.byDshId(String(id))?.permission_preset ??
381
- presetFromEvents([...coldRead.events]);
382
- const approvalMode = envMode ?? ompApprovalMode(preset);
383
- trace(`resume id=${id} preset=${preset ?? "none"} approval-mode=${approvalMode}${envMode === undefined ? "" : " (env override)"}${freshOmp ? " fresh-omp" : ""}`);
384
- // OMP live runtime — ALWAYS LAZY (sidecar-decoupling ruling, both branches).
411
+ // Approval mode: the session's own last `permission/preset` (= what the
412
+ // `/permission` pick wrote, or the event synthesized at create), with the
413
+ // bridge-index row as the fallback for logs that predate the event. Resolved
414
+ // AT SPAWN so a pick made after this resume but before the first prompt
415
+ // still reaches the child; OMP_APPROVAL_MODE (headless) wins.
416
+ // OMP live runtime — ALWAYS LAZY (lazy-transport ruling, both branches).
385
417
  // Opening/replaying a persisted session in the WebUI promotes it through
386
418
  // history.follow → resume; a read-only visit must NOT materialize the OMP
387
419
  // child. `LazyOmpRpc` spawns on the FIRST dispatch (prompt/followUp/steer/
@@ -391,15 +423,8 @@ export class OmpProvider extends Service {
391
423
  // With content: the child re-attaches its OWN app-home file via --resume
392
424
  // (never the dsh transcript); the mapping already exists in the bridge
393
425
  // store, so no adoption hook is needed.
426
+ // Both branches are LAZY and both resolve their launch flag at spawn.
394
427
  let rpc;
395
- if (ompFile === undefined) {
396
- const lazy = new LazyOmpRpc(["--approval-mode", approvalMode], spawnCwd);
397
- lazy.onSpawned((client) => { void adoptSpawnedChild(id, stored.header, client, preset); });
398
- rpc = lazy;
399
- }
400
- else {
401
- rpc = new LazyOmpRpc(["--approval-mode", approvalMode, "--resume", ompFile], spawnCwd);
402
- }
403
428
  try {
404
429
  // Fresh attach: seed the dsh session from the dsh log (native replay),
405
430
  // header identity folded from the stored header verbatim.
@@ -409,32 +434,47 @@ export class OmpProvider extends Service {
409
434
  inheritedEventCount: stored.inheritedEventCount,
410
435
  eventState: coldRead.eventState,
411
436
  }));
412
- trace(`resume id=${id} seeded from dsh session log (${coldRead.events.length} events)`);
437
+ const rowPreset = () => getBridgeStore()?.byDshId(String(id))?.permission_preset ?? undefined;
438
+ trace(`resume id=${id} seeded from dsh session log (${coldRead.events.length} events), approval=resolve-at-spawn (row fallback ${rowPreset() ?? "none"})${freshOmp ? " fresh-omp" : ""}`);
439
+ const spawnPermission = () => resolveSpawnPermission(preparation.session.snapshotEvents(), rowPreset(), envApprovalMode());
440
+ if (ompFile === undefined) {
441
+ const lazy = new LazyOmpRpc(() => ["--approval-mode", spawnPermission().approvalMode], spawnCwd);
442
+ lazy.onSpawned((client) => { void adoptSpawnedChild(id, stored.header, client, spawnPermission().preset); });
443
+ rpc = lazy;
444
+ }
445
+ else {
446
+ rpc = new LazyOmpRpc(() => ["--approval-mode", spawnPermission().approvalMode, "--resume", ompFile], spawnCwd);
447
+ }
413
448
  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 } });
414
- return this.registerHeld(id, ompFile ?? "", handle);
449
+ return this.registerHeld(id, ompFile ?? "", handle, rpc);
415
450
  }
416
451
  catch (error) {
417
452
  trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
418
453
  await stored.close().catch(() => { });
419
- rpc.close();
454
+ rpc?.close();
420
455
  throw error;
421
456
  }
422
457
  }
423
458
  /**
424
- * Track a published RPC-backed agent and hand the session to the supervisor
425
- * as held. Arrow FIELD (not a `#` method): the provider is exposed through
426
- * a Cordis tracing proxy, and hard-private methods fail their brand check
427
- * on the proxy receiver (the same reason setupAndPublish is module-level).
459
+ * Track a published RPC-backed agent, register its transport wrapper with the
460
+ * tick's liveness gate, and hand the session to the supervisor as held. Arrow
461
+ * FIELD (not a `#` method): the provider is exposed through a Cordis tracing
462
+ * proxy, and hard-private methods fail their brand check on the proxy receiver
463
+ * (the same reason setupAndPublish is module-level).
428
464
  */
429
- registerHeld = (id, file, handle) => {
465
+ registerHeld = (id, file, handle, rpc) => {
430
466
  const wrapped = {
431
467
  agent: handle.agent,
432
468
  dispose: async () => {
433
469
  this.heldAgents.delete(String(id));
470
+ this.liveSessions.delete(rpc);
434
471
  await handle.dispose();
435
472
  },
436
473
  };
437
474
  this.heldAgents.set(String(id), wrapped);
475
+ // Populate the tick's liveness gate: without this the gate is a constant
476
+ // false and the catalog refresh never runs (dead-input regression).
477
+ this.liveSessions.add(rpc);
438
478
  return wrapped;
439
479
  };
440
480
  }
@@ -1,4 +1,4 @@
1
- import { OmpSdkClient } from "./sdk-client.js";
1
+ import { OmpRpcClient } from "./rpc-client.js";
2
2
  /** Synthetic state for a not-yet-spawned child: quiescent by definition. */
3
3
  const IDLE_STATE = { isStreaming: false };
4
4
  export class LazyOmpRpc {
@@ -12,6 +12,12 @@ export class LazyOmpRpc {
12
12
  #failureListeners = new Set();
13
13
  #pendingModel;
14
14
  #onSpawned;
15
+ /**
16
+ * @param spawnArgs - the child's argv after `--mode rpc`, or a thunk called
17
+ * AT SPAWN. Use the thunk whenever a launch fact can still change between
18
+ * construction and the first prompt — the new-chat permission pick does.
19
+ * @param cwd - the spawn cwd.
20
+ */
15
21
  constructor(spawnArgs, cwd) {
16
22
  this.#spawnArgs = spawnArgs;
17
23
  this.#cwd = cwd;
@@ -35,7 +41,8 @@ export class LazyOmpRpc {
35
41
  if (this.#client !== undefined)
36
42
  return Promise.resolve(this.#client);
37
43
  if (this.#spawn === undefined) {
38
- this.#spawn = OmpSdkClient.spawn(this.#spawnArgs, this.#cwd).then((client) => {
44
+ const args = typeof this.#spawnArgs === "function" ? this.#spawnArgs() : this.#spawnArgs;
45
+ this.#spawn = OmpRpcClient.spawn(args, this.#cwd).then((client) => {
39
46
  // A close() that raced the in-flight spawn wins: dispose immediately.
40
47
  if (this.#closed) {
41
48
  client.close();
@@ -89,8 +96,8 @@ export class LazyOmpRpc {
89
96
  }
90
97
  async prompt(message) {
91
98
  const client = await this.#ensure();
92
- // Apply a pre-spawn model selection now, ahead of the prompt (the sidecar
93
- // records it as the session's model_change, like a pre-turn set_model).
99
+ // Apply a pre-spawn model selection now, ahead of the prompt (the child
100
+ // emits a config_update, like a pre-turn set_model).
94
101
  const pending = this.#pendingModel;
95
102
  if (pending !== undefined) {
96
103
  this.#pendingModel = undefined;
@@ -1,147 +1,164 @@
1
1
  /**
2
- * OMP model catalog + modelRoles data plane, served by the shared bun sidecar
3
- * (which embeds the OMP core via @oh-my-pi/pi-coding-agent) instead of
4
- * spawning `omp` CLI subprocesses.
2
+ * OMP CLI subprocess data plane — the model catalog and the `modelRoles` map
3
+ * are read from (and written through) the operator's own `omp` binary, the
4
+ * first-party entries the bridge uses instead of parsing OMP's files or
5
+ * hitting frame-limited RPC:
5
6
  *
6
- * Two data planes are bridged:
7
- * - `models.list` → the credential-resolved AVAILABLE model
8
- * catalog (the same set `omp models --json` returned: ~50 models / 5
9
- * providers), keyed the way `models.ts` normalizes them (id/provider/name/
10
- * api/baseUrl/reasoning/input/contextWindow/maxTokens/thinking).
11
- * - `settings.modelRoles.get/set` → the role→"provider/model[:effort]" map,
12
- * including the persisted `default` selector (TUI semantics: the default
13
- * model IS config state, surviving sessions). `set` is a whole-object
14
- * roundtrip write, matching `omp config set modelRoles` (dotted keys are
15
- * rejected; the entire modelRoles object is the unit).
7
+ * - `omp models --json` → the credential-resolved AVAILABLE model catalog
8
+ * (documented selectability rule: provider not in `disabledProviders` and
9
+ * keyless or with a resolvable credential — 7-level precedence, including
10
+ * OAuth/login keys stored in agent.db that no config file reveals).
11
+ * - `omp config get modelRoles --json` → the role→"provider/model[:effort]"
12
+ * map, including the persisted `default` selector (TUI semantics: the
13
+ * default model IS config state, surviving sessions).
14
+ * - `omp config set modelRoles --json <obj>` → whole-object roundtrip write
15
+ * (dotted keys are rejected; the entire modelRoles object is the unit).
16
16
  *
17
- * The sidecar is a SHARED SINGLETON (src/sdk-client.ts acquire/release) — this
18
- * module goes through the same instance via `callShared`, never spawning a
19
- * second sidecar process.
17
+ * All calls run with cwd = `OMP_HOME` (omp writes its own state db next to
18
+ * the agent dir) and inherit the daemon environment. The binary is resolved
19
+ * per call ({@link resolveOmpExecutable}), so `$OMP_EXECUTABLE` and PATH
20
+ * changes take effect without a reload.
20
21
  *
21
- * Sync variants (`*Sync`) cannot block on sidecar IPC, so they return the last
22
- * cache and warm it asynchronously: the first boot-time call returns
23
- * `undefined` (the caller's on-disk fallback engages) and a later background
24
- * fetch repopulates the cache. The async variants always fetch fresh.
25
- *
26
- * Fail-soft: any sidecar failure (not started / crashed) yields `undefined`
27
- * (reads) or `false` (write), so the provider never fails to boot on a down
28
- * sidecar.
22
+ * Fail-soft: a missing, broken or unresponsive `omp` yields `undefined`
23
+ * (reads) or `false` (write) — the provider never fails to boot because the
24
+ * CLI is down.
29
25
  *
30
26
  * @module agent-omp/omp-cli
31
27
  */
32
- import { callShared, callSharedIfLive } from "./sdk-client.js";
28
+ import { execFile, execFileSync } from "node:child_process";
29
+ import { promisify } from "node:util";
30
+ import { OMP_HOME } from "./knobs.js";
31
+ import { resolveOmpExecutable } from "./rpc-client.js";
32
+ const execFileAsync = promisify(execFile);
33
+ const TIMEOUT_MS = 15_000;
34
+ const MAX_BUFFER = 64 * 1024 * 1024;
33
35
  // ---------------------------------------------------------------------------
34
- // Sidecar plumbing (fail-soft: any failure → undefined/false)
36
+ // Subprocess plumbing (fail-soft: any failure → undefined/false)
35
37
  // ---------------------------------------------------------------------------
36
- /** `models.list` over the shared sidecar → validated OmpCliModel[], undefined on failure. */
37
- async function sidecarModelsList() {
38
- // LIVE-GATED: resolves only when the shared sidecar already exists (a live
39
- // session or an on-demand op). Cold → undefined → the disk fallback engages;
40
- // the reconcile tick can never spawn a sidecar (sidecar-decoupling ruling).
38
+ /** One `omp <args>` invocation parsed as JSON; undefined on any failure. */
39
+ async function ompCliJson(args) {
41
40
  try {
42
- const data = await callSharedIfLive("models.list", { refresh: true });
43
- if (data === null || typeof data !== "object")
44
- return undefined;
45
- const models = data.models;
46
- if (!Array.isArray(models))
47
- return undefined;
48
- const out = [];
49
- for (const model of models) {
50
- if (model !== null &&
51
- typeof model === "object" &&
52
- typeof model.provider === "string" &&
53
- typeof model.id === "string") {
54
- out.push(model);
55
- }
56
- }
57
- return out.length > 0 ? out : undefined;
41
+ const { stdout } = await execFileAsync(resolveOmpExecutable(), args, {
42
+ cwd: OMP_HOME,
43
+ timeout: TIMEOUT_MS,
44
+ maxBuffer: MAX_BUFFER,
45
+ });
46
+ return JSON.parse(stdout);
58
47
  }
59
48
  catch {
60
49
  return undefined;
61
50
  }
62
51
  }
63
- /** `settings.modelRoles.get` over the shared sidecar → validated OmpModelRoles, undefined on failure. */
64
- async function sidecarModelRolesGet() {
52
+ /** Synchronous variant of {@link ompCliJson} — the boot-time path. */
53
+ function ompCliJsonSync(args) {
65
54
  try {
66
- const data = await callShared("settings.modelRoles.get", {});
67
- const value = data?.modelRoles;
68
- if (value === null || typeof value !== "object" || Array.isArray(value))
69
- return undefined;
70
- const out = {};
71
- for (const [role, selector] of Object.entries(value)) {
72
- if (typeof selector === "string" && selector !== "")
73
- out[role] = selector;
74
- }
75
- return Object.keys(out).length > 0 ? out : undefined;
55
+ const stdout = execFileSync(resolveOmpExecutable(), args, {
56
+ cwd: OMP_HOME,
57
+ timeout: TIMEOUT_MS,
58
+ encoding: "utf8",
59
+ maxBuffer: MAX_BUFFER,
60
+ });
61
+ return JSON.parse(stdout);
76
62
  }
77
63
  catch {
78
64
  return undefined;
79
65
  }
80
66
  }
81
- /** `settings.modelRoles.set` over the shared sidecar → true only on a durable write. */
82
- async function sidecarModelRolesSet(roles) {
83
- try {
84
- const data = await callShared("settings.modelRoles.set", { modelRoles: roles });
85
- return data?.ok === true;
67
+ /** Validate an `omp models --json` payload; undefined when empty or invalid. */
68
+ function parseModels(json) {
69
+ if (json === null || typeof json !== "object")
70
+ return undefined;
71
+ const models = json.models;
72
+ if (!Array.isArray(models))
73
+ return undefined;
74
+ const out = [];
75
+ for (const model of models) {
76
+ if (model !== null && typeof model === "object" && typeof model.provider === "string") {
77
+ out.push(model);
78
+ }
86
79
  }
87
- catch {
88
- return false;
80
+ return out.length > 0 ? out : undefined;
81
+ }
82
+ /**
83
+ * Validate a `modelRoles` payload; undefined when unset, empty or invalid.
84
+ *
85
+ * `omp config get <key> --json` prints `{key, value, type, description}`, but
86
+ * the bare value map appears in older builds, so both shapes are accepted. A
87
+ * wrapped payload for a different key is rejected rather than misread as a
88
+ * role map.
89
+ */
90
+ function parseModelRoles(json) {
91
+ if (json === null || typeof json !== "object" || Array.isArray(json))
92
+ return undefined;
93
+ const record = json;
94
+ let value = record;
95
+ if ("key" in record || "value" in record) {
96
+ if (typeof record.key === "string" && record.key !== "modelRoles")
97
+ return undefined;
98
+ value = record.value;
99
+ }
100
+ if (value === null || typeof value !== "object" || Array.isArray(value))
101
+ return undefined;
102
+ const out = {};
103
+ for (const [role, selector] of Object.entries(value)) {
104
+ if (typeof selector === "string" && selector !== "")
105
+ out[role] = selector;
89
106
  }
107
+ return Object.keys(out).length > 0 ? out : undefined;
90
108
  }
91
109
  // ---------------------------------------------------------------------------
92
110
  // Model catalog
93
111
  // ---------------------------------------------------------------------------
94
- let modelsCache;
95
112
  /**
96
- * Synchronous catalog read — MEMO-ONLY by design (sidecar-decoupling): no boot
97
- * warm, no sidecar touch. `undefined` until something live swaps the cache in
98
- * ({@link ompAvailableModels} on an already-live sidecar), so the boot path in
99
- * `models.ts` falls back to the on-disk registry without spawning anything.
113
+ * Synchronous `omp models --json` — the boot-time variant (the adapter's
114
+ * provider route list must be known before `registerAdapter` returns).
115
+ * Blocks up to the timeout; prefer the async variant everywhere else.
100
116
  */
101
117
  export function ompAvailableModelsSync() {
102
- return modelsCache;
118
+ return parseModels(ompCliJsonSync(["models", "--json"]));
103
119
  }
104
120
  /**
105
- * `models.list` — the credential-resolved catalog, LIVE-GATED: fetched only
106
- * when the shared sidecar already exists (a live session); `undefined` and the
107
- * cache untouched when cold. Callers fall back to the on-disk sources.
121
+ * `omp models --json` — the AVAILABLE model catalog (credential-resolved,
122
+ * `disabledProviders`-aware). `undefined` when the CLI fails; callers fall
123
+ * back to the on-disk sources.
108
124
  */
109
125
  export async function ompAvailableModels() {
110
- const models = await sidecarModelsList();
111
- if (models !== undefined)
112
- modelsCache = models;
113
- return models;
126
+ return parseModels(await ompCliJson(["models", "--json"]));
114
127
  }
115
128
  // ---------------------------------------------------------------------------
116
129
  // modelRoles
117
130
  // ---------------------------------------------------------------------------
118
- let rolesCache;
119
131
  /**
120
- * Synchronous roles read — memo-only, same contract as
121
- * {@link ompAvailableModelsSync}. The boot path reads modelRoles from
122
- * `config.yml` on disk (omp-store); this cache only ever reflects what a live
123
- * sidecar reported. The write path (`ompModelRoles`/`ompSetModelRoles`) may
124
- * spawn on real need — a default-model change is a user action, not a tick.
132
+ * Synchronous `omp config get modelRoles --json` — boot-time variant of
133
+ * {@link ompModelRoles}.
125
134
  */
126
135
  export function ompModelRolesSync() {
127
- return rolesCache;
136
+ return parseModelRoles(ompCliJsonSync(["config", "get", "modelRoles", "--json"]));
128
137
  }
129
138
  /**
130
- * `settings.modelRoles.get` — the current modelRoles object, fetched fresh
131
- * from the sidecar on every call (the OMP TUI may have rewritten config.yml).
132
- * `undefined` when unset or the sidecar fails.
139
+ * `omp config get modelRoles --json` — the current modelRoles object, read
140
+ * fresh on every call (the OMP TUI may have rewritten config.yml). `undefined`
141
+ * when unset or the CLI fails.
133
142
  */
134
143
  export async function ompModelRoles() {
135
- const roles = await sidecarModelRolesGet();
136
- if (roles !== undefined)
137
- rolesCache = roles;
138
- return roles;
144
+ return parseModelRoles(await ompCliJson(["config", "get", "modelRoles", "--json"]));
139
145
  }
140
146
  /**
141
- * `settings.modelRoles.set` — the TUI-grade default-model write: the sidecar
142
- * replaces the whole modelRoles object (matching `omp config set modelRoles`
143
- * whole-object semantics) and confirms the write landed before returning true.
147
+ * `omp config set modelRoles --json <obj>` — the TUI-grade default-model
148
+ * write: OMP persists `modelRoles.default` itself, so every new session
149
+ * (TUI or Web) inherits the selection. Whole-object roundtrip: dotted keys
150
+ * are rejected by the CLI. `false` on any failure (fail-soft).
144
151
  */
145
152
  export async function ompSetModelRoles(roles) {
146
- return sidecarModelRolesSet(roles);
153
+ try {
154
+ await execFileAsync(resolveOmpExecutable(), ["config", "set", "modelRoles", "--json", JSON.stringify(roles)], {
155
+ cwd: OMP_HOME,
156
+ timeout: TIMEOUT_MS,
157
+ maxBuffer: MAX_BUFFER,
158
+ });
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
147
164
  }
@@ -434,9 +434,10 @@ function isDescendantOf(pid, ancestor) {
434
434
  * OMP has no session-level exclusivity: neither the TUI nor `--mode rpc`
435
435
  * locks its transcript, so two live writers interleave appends silently
436
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.
437
+ * one file with plain `w` descriptors and no advisory locks). The intended
438
+ * enforcement is to refuse resuming a transcript another process already writes
439
+ * (the TUI case), closing the split-brain at the only layer that can see it —
440
+ * but see {@link foreignWriterPid}: that hold is NOT wired in this round.
440
441
  * Own descendants (a just-torn-down bridge child whose fds linger) are
441
442
  * exempt to keep teardown races from wedging re-resume.
442
443
  */
@@ -504,7 +505,14 @@ export function scanForeignWriters() {
504
505
  /**
505
506
  * Pid of a FOREIGN process holding `file` open for writing, if any.
506
507
  * Convenience wrapper over {@link scanForeignWriters} for the single-file
507
- * resume gate; the supervisor uses the shared collector directly per tick.
508
+ * resume gate.
509
+ *
510
+ * UNWIRED (2026-10-03): this helper has no caller — the `resume` path never
511
+ * invokes it, so nothing currently refuses a transcript another process (the
512
+ * TUI) is writing. Spec §3.4 of `docs/specs/omp-rpc-transport/spec.md`
513
+ * registers that accepted exposure and the planned follow-up: a fail-loud
514
+ * `foreignWriterPid(<sessionFile>)` check wired into `resume`, to be decided
515
+ * by the operator. Helper kept exported for that follow-up.
508
516
  */
509
517
  export function foreignWriterPid(file) {
510
518
  let canonical;