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.
- package/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- 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();
|