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