super-dsh 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +54 -6
  2. package/agent-agy/dist/agent.d.ts +4 -0
  3. package/agent-agy/dist/agent.js +41 -7
  4. package/agent-agy/dist/agy-cli-client.d.ts +12 -0
  5. package/agent-agy/dist/agy-cli-client.js +81 -13
  6. package/agent-agy/dist/index.js +13 -7
  7. package/agent-agy/package.json +5 -1
  8. package/agent-claude/dist/agent.js +67 -4
  9. package/agent-claude/dist/claude-client.js +46 -1
  10. package/agent-claude/dist/index.js +3 -0
  11. package/agent-claude/dist/sdk-runtime.js +2 -2
  12. package/agent-codex/dist/agent.js +33 -8
  13. package/agent-codex/dist/app-server-client.js +26 -3
  14. package/agent-codex/dist/app-server-protocol.js +6 -2
  15. package/agent-codex/dist/app-server-wire.js +23 -6
  16. package/agent-codex/dist/codex-native-config.js +6 -1
  17. package/agent-codex/dist/codex-runtime-fetch.js +293 -0
  18. package/agent-codex/dist/codex-runtime.js +12 -12
  19. package/agent-codex/dist/index.js +10 -4
  20. package/agent-codex/package.json +8 -3
  21. package/agent-codex-sdk/dist/sdk-runtime.js +1 -1
  22. package/agent-hermes/dist/hermes-client.js +44 -5
  23. package/agent-hermes/dist/index.js +10 -5
  24. package/agent-omp/dist/agent.js +36 -5
  25. package/agent-omp/dist/index.js +29 -6
  26. package/agent-omp/dist/lazy-rpc.js +11 -2
  27. package/agent-omp/dist/rpc-client.js +6 -1
  28. package/agent-pi/dist/agent.js +36 -9
  29. package/agent-pi/dist/index.js +6 -2
  30. package/agent-pi/dist/pi-client.js +13 -3
  31. package/cordis.patch.yml +10 -9
  32. package/package.json +6 -4
  33. package/superd-hub/dist/a2a-row.js +20 -3
  34. package/superd-hub/dist/connection-trust.d.ts +8 -0
  35. package/superd-hub/dist/connection-trust.js +16 -0
  36. package/superd-hub/dist/foreign-agy.d.ts +3 -0
  37. package/superd-hub/dist/foreign-agy.js +110 -0
  38. package/superd-hub/dist/foreign-claude.d.ts +4 -0
  39. package/superd-hub/dist/foreign-claude.js +160 -0
  40. package/superd-hub/dist/foreign-extension.d.ts +10 -0
  41. package/superd-hub/dist/foreign-extension.js +27 -0
  42. package/superd-hub/dist/foreign-hub.d.ts +27 -0
  43. package/superd-hub/dist/foreign-hub.js +243 -0
  44. package/superd-hub/dist/foreign-native.d.ts +4 -0
  45. package/superd-hub/dist/foreign-native.js +115 -0
  46. package/superd-hub/dist/web-trust.d.ts +14 -42
  47. package/superd-hub/dist/web-trust.js +18 -43
  48. package/superd-hub/package.json +14 -2
  49. package/agent-codex/README.md +0 -191
  50. package/agent-codex-sdk/README.md +0 -79
  51. package/agent-hermes/README.md +0 -145
  52. package/agent-omp/README.md +0 -94
  53. package/agent-omp-sdk/README.md +0 -103
  54. package/agent-pi/README.md +0 -67
@@ -0,0 +1,293 @@
1
+ /**
2
+ * codex-runtime-fetch.ts — the lazy, durable Codex runtime the app-server line
3
+ * falls back to when the operator has NO codex of their own.
4
+ *
5
+ * 2026-10-06 user ruling (amends the 2026-10-05 core-discovery order): the
6
+ * exact-pin `@openai/codex` package must NEVER ride the installation — it went
7
+ * out as a real `dependencies` entry in 0.1.11, so every registry consumer
8
+ * downloaded the pinned CLI binary package AND its six aliased platform
9
+ * binaries (linux-x64/arm64, darwin-x64/arm64, win32-x64/arm64) at install
10
+ * time. That is the omp-sdk/agy-sdk/codex-sdk rule violated one week after it
11
+ * was written: SDKs and binaries are second-class — anything a line needs, it
12
+ * fetches at RUNTIME into its world home, and installs stay clean (no
13
+ * postinstall, no allow-build, no dependency that ships a binary).
14
+ *
15
+ * So the discovery order in codex-runtime.ts is
16
+ * CODEX_BIN → PATH CLI → desktop core → world-home runtime (this module),
17
+ * and this module is the only supplier of that last tier: on the first real
18
+ * prompt, when override, CLI and desktop all missed, the pinned
19
+ * `@openai/codex@0.160.0` tree is fetched into
20
+ *
21
+ * <worldHome>/codex-runtime/
22
+ * package.json written by us (private, never published)
23
+ * node_modules/ `npm install --exact --ignore-scripts --omit=dev`
24
+ * node_modules/@openai/codex/bin/codex.js the entry discovery returns
25
+ * <worldHome>/codex-runtime.json marker — written LAST, after the smoke run
26
+ * <worldHome>/codex-runtime.lock atomic (mkdir) bootstrap lock
27
+ * <worldHome>/codex-runtime.error.log written ONLY by a failed fetch
28
+ *
29
+ * Why the world home rather than the package root: the package root is the
30
+ * installed tarball — read-only in every real consumer install, and shared by
31
+ * every world. The world home (`<dshHome>/agents/codex`) is per-runtime,
32
+ * writable, and already the home of this adapter's own DSH state.
33
+ *
34
+ * Readiness is the ENTRY FILE (checked on every discovery, zero network) plus
35
+ * the marker's package version: a pin bump in a future adapter must refetch,
36
+ * and the sync resolver alone would happily keep using the old tree.
37
+ *
38
+ * The npm invocation is deliberately `--ignore-scripts`: the codex packages
39
+ * ship their binaries inside the tarballs, so a fetch needs zero lifecycle
40
+ * scripts — exactly the property this ruling exists to protect.
41
+ */
42
+ import { execFile } from "node:child_process";
43
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
44
+ import { dirname, join, resolve } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+ import { promisify } from "node:util";
47
+ import { resolveCodexRuntime } from "./codex-runtime.js";
48
+ /** The pinned Codex CLI package this line fetches (ships the app-server binary). */
49
+ export const CODEX_RUNTIME_PACKAGE = "@openai/codex";
50
+ /** Pin of {@link CODEX_RUNTIME_PACKAGE}; bumping it must refetch the tree. */
51
+ export const CODEX_RUNTIME_VERSION = "0.160.0";
52
+ /** The entry `resolveCodexRuntime` returns, relative to the runtime dir. */
53
+ const CODEX_ENTRY_RELATIVE = ["node_modules", CODEX_RUNTIME_PACKAGE, "bin", "codex.js"];
54
+ export function codexRuntimeLayout(worldHome) {
55
+ const dir = join(worldHome, "codex-runtime");
56
+ return {
57
+ dir,
58
+ manifest: join(dir, "package.json"),
59
+ entry: join(dir, ...CODEX_ENTRY_RELATIVE),
60
+ marker: join(worldHome, "codex-runtime.json"),
61
+ lock: join(worldHome, "codex-runtime.lock"),
62
+ errorLog: join(worldHome, "codex-runtime.error.log"),
63
+ };
64
+ }
65
+ /** Read the install marker; undefined when absent or unreadable. */
66
+ export function readCodexRuntimeRecord(worldHome) {
67
+ const { marker } = codexRuntimeLayout(worldHome);
68
+ if (!existsSync(marker))
69
+ return undefined;
70
+ try {
71
+ const parsed = JSON.parse(readFileSync(marker, "utf8"));
72
+ return typeof parsed === "object" && parsed !== null ? parsed : undefined;
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ /**
79
+ * Fast path: a fetched runtime exists, matches `identity`, and its entry file
80
+ * is still on disk. Everything else must (re)fetch or (re)discover.
81
+ */
82
+ export function codexRuntimeCurrent(worldHome, identity) {
83
+ const record = readCodexRuntimeRecord(worldHome);
84
+ if (record === undefined)
85
+ return undefined;
86
+ if (record.pluginVersion !== identity.pluginVersion)
87
+ return undefined;
88
+ if (record.packageVersion !== identity.packageVersion)
89
+ return undefined;
90
+ if (!existsSync(codexRuntimeLayout(worldHome).entry))
91
+ return undefined;
92
+ return record;
93
+ }
94
+ /** The npm argv the runtime tree is installed with (kept pure for the test). */
95
+ export function codexInstallArgv(packageVersion = CODEX_RUNTIME_VERSION) {
96
+ return [
97
+ "install",
98
+ "--save-exact",
99
+ "--ignore-scripts",
100
+ "--omit=dev",
101
+ `${CODEX_RUNTIME_PACKAGE}@${packageVersion}`,
102
+ ];
103
+ }
104
+ /** A held lock older than this is treated as abandoned and reclaimed. */
105
+ const LOCK_STALE_MS = 30 * 60 * 1000;
106
+ /** The smoke run's wall-clock cap: `--version` must never wedge a turn. */
107
+ const SMOKE_TIMEOUT_MS = 60_000;
108
+ const execFileAsync = promisify(execFile);
109
+ /**
110
+ * The PRODUCTION {@link CodexRuntimeFetchDeps}: real npm/Node invocations plus
111
+ * the marker identity read from this package's own manifest. Kept IN THE
112
+ * MODULE so the manual pre-warm script and the first-prompt lazy fetch share
113
+ * ONE implementation — a second copy is exactly how the two paths would drift.
114
+ */
115
+ export function createCodexRuntimeDeps(options = {}) {
116
+ const pluginDir = options.pluginDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
117
+ return {
118
+ run: async (cmd, args, opts) => {
119
+ await execFileAsync(cmd, args, { cwd: opts?.cwd, env: options.env, maxBuffer: 64 * 1024 * 1024 });
120
+ },
121
+ smoke: async (cmd, args, opts) => {
122
+ const { stdout } = await execFileAsync(cmd, args, {
123
+ cwd: opts?.cwd,
124
+ env: options.env,
125
+ timeout: opts?.timeoutMs,
126
+ maxBuffer: 1024 * 1024,
127
+ });
128
+ return stdout;
129
+ },
130
+ pluginVersion: readPluginVersion(pluginDir),
131
+ log: (line) => process.stderr.write(`[agent-codex] ${line}\n`),
132
+ ...(options.env === undefined ? {} : { env: options.env }),
133
+ };
134
+ }
135
+ /** The version of the adapter package at `pluginDir` (the marker identity half). */
136
+ export function readPluginVersion(pluginDir) {
137
+ const raw = readFileSync(join(pluginDir, "package.json"), "utf8");
138
+ const parsed = JSON.parse(raw);
139
+ if (typeof parsed.version !== "string" || parsed.version === "") {
140
+ throw new Error(`codex: ${join(pluginDir, "package.json")} carries no version`);
141
+ }
142
+ return parsed.version;
143
+ }
144
+ /**
145
+ * Sync discovery across the whole ladder INCLUDING the fetched runtime; a
146
+ * miss throws (which is the fetch trigger, never surfaced to the user).
147
+ */
148
+ function discoverWith(worldHome, deps) {
149
+ return resolveCodexRuntime({
150
+ ...(deps.discovery ?? {}),
151
+ ...(deps.env === undefined ? {} : { env: deps.env }),
152
+ packageEntryCandidates: [codexRuntimeLayout(worldHome).entry],
153
+ });
154
+ }
155
+ /**
156
+ * Return a usable Codex runtime for `<worldHome>`, fetching the pinned
157
+ * package first when the operator's own CODEX_BIN / PATH CLI / desktop core
158
+ * all miss.
159
+ *
160
+ * Idempotent: an operator-provided runtime short-circuits without touching
161
+ * the network, and so does a complete, matching fetched runtime. Concurrent
162
+ * callers: the bootstrap is guarded by a `mkdir` lock — a second caller that
163
+ * finds a FRESH lock throws a clear "fetch in progress" error, while an
164
+ * abandoned lock (older than {@link LOCK_STALE_MS}) is reclaimed so a crashed
165
+ * fetch cannot brick the line.
166
+ *
167
+ * Failures append to `<worldHome>/codex-runtime.error.log` and throw a single
168
+ * actionable error — never a partial runtime behind a success marker.
169
+ */
170
+ export async function ensureCodexRuntime(worldHome, deps) {
171
+ const usable = (() => {
172
+ try {
173
+ return discoverWith(worldHome, deps);
174
+ }
175
+ catch {
176
+ return undefined;
177
+ }
178
+ })();
179
+ // An operator runtime (override/CLI/desktop) is always taken as-is; a fetched
180
+ // runtime is taken only when its pin still matches — a bumped pin refetches.
181
+ if (usable !== undefined && usable.source !== "package")
182
+ return usable;
183
+ const identity = { pluginVersion: deps.pluginVersion, packageVersion: CODEX_RUNTIME_VERSION };
184
+ if (usable !== undefined && codexRuntimeCurrent(worldHome, identity) !== undefined)
185
+ return usable;
186
+ const layout = codexRuntimeLayout(worldHome);
187
+ mkdirSync(worldHome, { recursive: true });
188
+ acquireLock(layout, deps);
189
+ try {
190
+ // Re-check inside the lock: a peer may have completed the fetch in the
191
+ // window between our check and the lock, in which case we pay nothing.
192
+ const raced = (() => {
193
+ try {
194
+ return discoverWith(worldHome, deps);
195
+ }
196
+ catch {
197
+ return undefined;
198
+ }
199
+ })();
200
+ if (raced !== undefined && (raced.source !== "package" || codexRuntimeCurrent(worldHome, identity) !== undefined))
201
+ return raced;
202
+ try {
203
+ await buildRuntime(layout, deps, identity);
204
+ }
205
+ catch (error) {
206
+ const detail = error instanceof Error ? error.message : String(error);
207
+ const stamp = new Date().toISOString();
208
+ try {
209
+ appendFileSync(layout.errorLog, `[${stamp}] codex runtime fetch failed: ${detail}\n`);
210
+ }
211
+ catch {
212
+ /* the trace is best-effort; the throw below is the contract */
213
+ }
214
+ throw new Error(`codex: could not fetch the runtime (${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION}) at ${layout.dir}: ${detail}. ` +
215
+ `Pre-warm it by hand with \`npm run fetch-codex-runtime\`, or install a Codex CLI and set CODEX_BIN. Trace: ${layout.errorLog}`);
216
+ }
217
+ const resolved = discoverWith(worldHome, deps);
218
+ deps.log(`codex: runtime ready at ${layout.dir} (${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION})`);
219
+ return resolved;
220
+ }
221
+ finally {
222
+ rmSync(layout.lock, { recursive: true, force: true });
223
+ }
224
+ }
225
+ /** The idempotent build steps; the marker is written last, after the smoke run. */
226
+ async function buildRuntime(layout, deps, identity) {
227
+ mkdirSync(layout.dir, { recursive: true });
228
+ // npm needs a manifest to own the tree; `private` + a throwaway version keep
229
+ // it unambiguously not-a-publishable-package, and an existing one is left
230
+ // alone so a later install can still be reproduced by hand.
231
+ if (!existsSync(layout.manifest)) {
232
+ writeFileSync(layout.manifest, JSON.stringify({ name: "codex-runtime", private: true, version: "0.0.0" }, null, 2) + "\n");
233
+ }
234
+ deps.log(`codex: fetching ${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION} into ${layout.dir}`);
235
+ await deps.run("npm", codexInstallArgv(), { cwd: layout.dir });
236
+ if (!existsSync(layout.entry)) {
237
+ throw new Error(`npm install produced no ${layout.entry} (the package layout changed?)`);
238
+ }
239
+ // Smoke step: run the fetched entry's `--version`. This exercises the real
240
+ // platform-binary pick the entry performs at run time — a tree whose binary
241
+ // does not match this host fails HERE, at the fetch, with a trace on disk,
242
+ // instead of at the first prompt inside the agent's cold-start error path.
243
+ // The entry is resolved ABSOLUTE before the cwd switch — the smoke run
244
+ // changes the working directory, and a relative worldHome would otherwise
245
+ // re-resolve against it (same trap as the `-c model_catalog_json` lesson).
246
+ const smokeEntry = resolve(layout.entry);
247
+ deps.log(`codex: smoke-running ${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION} --version`);
248
+ const stdout = await deps.smoke(process.execPath, [smokeEntry, "--version"], { cwd: layout.dir, timeoutMs: SMOKE_TIMEOUT_MS });
249
+ const smokeVersion = stdout.trim().split("\n")[0]?.trim() ?? "";
250
+ if (smokeVersion === "") {
251
+ throw new Error(`smoke run of ${layout.entry} --version produced no output`);
252
+ }
253
+ // Marker LAST — a crash before this point leaves an incomplete runtime that
254
+ // the next call heals, never a false success.
255
+ const record = {
256
+ ...identity,
257
+ entryPath: layout.entry,
258
+ smokeVersion,
259
+ installedAt: new Date().toISOString(),
260
+ };
261
+ writeFileSync(layout.marker, JSON.stringify(record, null, 2) + "\n");
262
+ rmSync(layout.errorLog, { force: true });
263
+ }
264
+ /**
265
+ * mkdir-based mutex. Returns owning the lock; throws when a fresh lock is held
266
+ * by someone else; reclaims a lock older than {@link LOCK_STALE_MS}.
267
+ */
268
+ function acquireLock(layout, deps) {
269
+ for (;;) {
270
+ try {
271
+ mkdirSync(layout.lock);
272
+ return;
273
+ }
274
+ catch (error) {
275
+ if (error.code !== "EEXIST")
276
+ throw error;
277
+ }
278
+ let ageMs;
279
+ try {
280
+ ageMs = Date.now() - statSync(layout.lock).mtimeMs;
281
+ }
282
+ catch {
283
+ continue; // released between the mkdir attempt and the stat — retry now
284
+ }
285
+ if (ageMs > LOCK_STALE_MS) {
286
+ deps.log(`codex: reclaiming stale runtime lock (age ${Math.round(ageMs / 1000)}s)`);
287
+ rmSync(layout.lock, { recursive: true, force: true });
288
+ continue;
289
+ }
290
+ throw new Error(`codex: another runtime fetch is already in progress (lock ${layout.lock}). ` +
291
+ `Retry once it finishes; if no fetch is running, remove that lock directory.`);
292
+ }
293
+ }
@@ -1,6 +1,5 @@
1
1
  /** Resolve an installed Codex runtime at first use, without modifying its home. */
2
2
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
- import { createRequire } from "node:module";
4
3
  import { homedir } from "node:os";
5
4
  import { posix, win32 } from "node:path";
6
5
  function readableFile(path) {
@@ -66,6 +65,7 @@ export function resolveCodexRuntime(options = {}) {
66
65
  env,
67
66
  homeDir: options.homeDir ?? homedir(),
68
67
  ...(options.desktopCandidates === undefined ? {} : { desktopCandidates: options.desktopCandidates }),
68
+ ...(options.packageEntryCandidates === undefined ? {} : { packageEntryCandidates: options.packageEntryCandidates }),
69
69
  isFile,
70
70
  isExecutable,
71
71
  realpath: options.realpath ?? realpathSync,
@@ -84,7 +84,6 @@ export function resolveCodexRuntime(options = {}) {
84
84
  }
85
85
  return undefined;
86
86
  }),
87
- resolvePackageEntry: options.resolvePackageEntry ?? (() => createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")),
88
87
  };
89
88
  const usable = (command) => !shellWrapper(command) && isFile(command) && (isNodeScript(command, discovery) || isExecutable(command));
90
89
  const selected = (command, source) => ({ command, nodeScript: isNodeScript(command, discovery), source });
@@ -97,22 +96,23 @@ export function resolveCodexRuntime(options = {}) {
97
96
  throw new Error(`CODEX_BIN ${override} is not a readable JavaScript entry or executable Codex runtime. Fix CODEX_BIN or remove the override.`);
98
97
  return selected(command, "override");
99
98
  }
99
+ const cli = discovery.findCommand("codex");
100
+ if (cli !== undefined && usable(cli))
101
+ return selected(cli, "cli");
100
102
  for (const candidate of desktopPaths(discovery)) {
101
103
  if (!shellWrapper(candidate) && isFile(candidate) && isExecutable(candidate))
102
104
  return selected(candidate, "desktop");
103
105
  }
104
- const cli = discovery.findCommand("codex");
105
- if (cli !== undefined && usable(cli))
106
- return selected(cli, "cli");
107
- try {
108
- const entry = discovery.resolvePackageEntry();
109
- if (isFile(entry))
106
+ // Last resort only: the runtime the fetch layer materialized into the world
107
+ // home. It cannot pre-exist on a clean install — the fetch only runs when the
108
+ // override, CLI and desktop all missed, so checking it last is exact.
109
+ for (const entry of discovery.packageEntryCandidates ?? []) {
110
+ if (!shellWrapper(entry) && isFile(entry))
110
111
  return { command: entry, nodeScript: true, source: "package" };
111
112
  }
112
- catch {
113
- // Installation fallback only; authentication/generation never reaches here.
114
- }
115
- throw new Error("cannot locate a Codex runtime: install Codex desktop or a user CLI on PATH, restore the exact-pin @openai/codex package, or set CODEX_BIN to its executable or JavaScript entry.");
113
+ throw new Error("cannot locate a Codex runtime: install a Codex CLI (or the desktop app) on PATH, " +
114
+ "set CODEX_BIN to its executable or JavaScript entry, " +
115
+ "or let the adapter fetch the pinned @openai/codex package into the world home at first prompt.");
116
116
  }
117
117
  /** Compatibility interface for callers that need only the selected path. */
118
118
  export function resolveCodexCommand(options = {}) {
@@ -20,6 +20,7 @@ import { OPENAI_OAUTH_ID, readEffectiveProviderStore } from "./codex-oauth.js";
20
20
  * record and fails closed when the thread is unknown. The rollout scan, the
21
21
  * adapter-owned session list and the transcript replay are gone.
22
22
  */
23
+ import { bindForeignHub } from '@pgmi-builds/superd-hub/foreign-hub';
23
24
  import { existsSync, realpathSync, statSync } from "node:fs";
24
25
  import { join } from "node:path";
25
26
  import { Service } from "@deepseek-ai/cordis";
@@ -411,6 +412,9 @@ export class CodexProvider extends Service {
411
412
  this.routes.replace([...ids]);
412
413
  trace(`routes replaced: ${ids.join(", ") || "none"}`);
413
414
  }
415
+ hubFor(sessionId) {
416
+ return bindForeignHub('codex', sessionId, this.home, () => this.runtime.ctx.agents.get(sessionId));
417
+ }
414
418
  async createAgent(ownerCtx, options) {
415
419
  const loopCtx = this.runtime.ctx;
416
420
  const id = options.sessionId;
@@ -444,7 +448,7 @@ export class CodexProvider extends Service {
444
448
  // app-server child is NOT spawned here (lazy-spawn gate, rules §14-i) —
445
449
  // create() is pure construction, and the process materializes on the first
446
450
  // real prompt.
447
- const client = CodexAppServerClient.create({ cwd: spawnCwd, approvalPolicy, sandboxMode });
451
+ const client = CodexAppServerClient.create({ cwd: spawnCwd, approvalPolicy, sandboxMode, worldHome: this.home, hub: this.hubFor(id) });
448
452
  // The session's route is NOT pinned here any more (2026-10-04): the agent
449
453
  // resolves it per turn from DSH's own model-selection state, and a launch
450
454
  // costs nothing until the first real prompt. `setupAndPublish` still seeds a
@@ -499,7 +503,7 @@ export class CodexProvider extends Service {
499
503
  if (record === undefined) {
500
504
  throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
501
505
  }
502
- if (record.threadId === null) {
506
+ if (record.threadId === null && (await readStoredEvents(persistence, id, options.signal)).some(event => event.type === "turn/start" || event.type === "user/message" || event.type === "assistant/message")) {
503
507
  // C2: never auto-start a fresh thread — the session's history is in the
504
508
  // (missing) native thread, not in a blank one.
505
509
  throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
@@ -514,7 +518,7 @@ export class CodexProvider extends Service {
514
518
  // `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
515
519
  // Without this gate the failure would surface only at the first turn
516
520
  // (resumeThread is zero-IO).
517
- if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
521
+ if (record.threadId !== null && readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
518
522
  throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
519
523
  }
520
524
  // Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
@@ -541,9 +545,11 @@ export class CodexProvider extends Service {
541
545
  // rollout). No process is spawned here — lazy-spawn gate, rules §14-i.
542
546
  const client = CodexAppServerClient.create({
543
547
  cwd: spawnCwd,
544
- resumeThreadId: record.threadId,
548
+ ...(record.threadId === null ? {} : { resumeThreadId: record.threadId }),
545
549
  approvalPolicy,
546
550
  sandboxMode,
551
+ worldHome: this.home,
552
+ hub: this.hubFor(id),
547
553
  });
548
554
  let handle;
549
555
  let stored;
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@pgmi-builds/agent-codex",
3
3
  "version": "0.1.0",
4
- "description": "Codex provider for DeepSeek Harness (dsh) — an AgentFactory that drives a `codex app-server` child process (stdio JSONL JSON-RPC) per session and bridges its thread/turn surface, approvals and permission policy into Dash Agent/Session contracts.",
4
+ "description": "Codex provider for DeepSeek Harness (dsh) — an AgentFactory that drives a `codex app-server` child process (stdio JSONL JSON-RPC) per session and bridges its thread/turn surface, approvals and permission policy into Dash Agent/Session contracts. Declares NO codex dependency and ships NO postinstall (2026-10-06 ruling): the operator's own CODEX_BIN / PATH CLI / desktop core is discovered at first prompt, and only when all three miss is the pinned `@openai/codex` package fetched at RUNTIME into the world home (src/codex-runtime-fetch.ts, `npm run fetch-codex-runtime`).",
5
+
5
6
  "type": "module",
6
7
  "exports": {
7
8
  ".": "./dist/index.js",
@@ -9,9 +10,11 @@
9
10
  },
10
11
  "files": [
11
12
  "dist",
13
+ "scripts",
12
14
  "cordis.patch.yml",
13
15
  "README.md"
14
16
  ],
17
+
15
18
  "dsh": {
16
19
  "bundle": {
17
20
  "patch": "./cordis.patch.yml"
@@ -19,12 +22,14 @@
19
22
  },
20
23
  "scripts": {
21
24
  "build": "tsc -p tsconfig.json",
22
- "test": "node --test test/*.test.mjs"
25
+ "test": "node --test test/*.test.mjs",
26
+ "fetch-codex-runtime": "node scripts/fetch-codex-runtime.mjs"
23
27
  },
28
+
24
29
  "dependencies": {
25
- "@openai/codex": "0.160.0",
26
30
  "zod": "4.4.3"
27
31
  },
32
+
28
33
  "peerDependencies": {
29
34
  "@deepseek-ai/cordis": "^4.0.1",
30
35
  "@deepseek-ai/dsh-agent": ">=0.1.5-0 <0.3.0-0",
@@ -110,7 +110,7 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
110
110
  export function sdkInstallArgv(packageVersion = SDK_PACKAGE_VERSION) {
111
111
  return [
112
112
  "install",
113
- "--exact",
113
+ "--save-exact",
114
114
  "--ignore-scripts",
115
115
  "--omit=dev",
116
116
  `${SDK_PACKAGE}@${packageVersion}`,
@@ -60,10 +60,12 @@
60
60
  * as are `HERMES_CWD`/`HERMES_TUI_GATEWAY_URL`/`HERMES_TUI_SIDECAR_URL`
61
61
  * (keep the spawned stdio transport the only transport).
62
62
  */
63
- import { spawn } from "node:child_process";
63
+ import { spawn, execFile } from "node:child_process";
64
64
  import { existsSync, readFileSync } from "node:fs";
65
65
  import { homedir } from "node:os";
66
- import { delimiter, resolve } from "node:path";
66
+ import { delimiter, resolve, join } from "node:path";
67
+ import { promisify } from "node:util";
68
+ import { prepareHermesNativePlugin } from "@pgmi-builds/superd-hub/foreign-native";
67
69
  import { createInterface } from "node:readline";
68
70
  import { projectGatewayEvent } from "./hermes-events.js";
69
71
  const READY_TIMEOUT_MS = 15_000; // TUI parity (STARTUP_TIMEOUT_MS)
@@ -217,13 +219,19 @@ export class HermesGatewayClient {
217
219
  * materialized within the grace. Agent-held clients never arm it (ruling 4).
218
220
  */
219
221
  #spawnGraceTimer = null;
222
+ #hubPreparation;
223
+ #hubSession;
220
224
  constructor(opts) {
221
225
  this.#requestTimeoutMs = opts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
222
226
  this.#approvalTimeoutMs = opts.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS;
223
227
  this.#lazy = opts.lazy === true;
224
228
  this.#spawnOpts = opts;
225
- if (!this.#lazy)
226
- this.#materializeChild();
229
+ if (!this.#lazy) {
230
+ if (opts.hub === undefined)
231
+ this.#materializeChild();
232
+ else
233
+ void this.ensureStarted().catch(() => { });
234
+ }
227
235
  }
228
236
  /** Captured spawn options for deferred materialization (lazy clients). */
229
237
  #spawnOpts;
@@ -237,6 +245,10 @@ export class HermesGatewayClient {
237
245
  const root = resolveHermesRoot();
238
246
  const python = resolveHermesPython(root);
239
247
  const env = { ...(opts.env ?? process.env) };
248
+ if (this.#hubSession !== undefined)
249
+ env.SUPERD_HUB_SESSION = this.#hubSession;
250
+ else
251
+ delete env.SUPERD_HUB_SESSION;
240
252
  // Native `.env` fill (dotenv convention: existing env WINS). The omp and
241
253
  // pi runtimes load their own native `.env`; the hermes gateway child does
242
254
  // not, and the harness env carries no provider keys — without this fill
@@ -399,6 +411,23 @@ export class HermesGatewayClient {
399
411
  * A lazy client materializes its python child HERE (full-lazy ruling:
400
412
  * view-time resume spawns nothing). */
401
413
  ensureStarted() {
414
+ const hub = this.#spawnOpts.hub;
415
+ if (hub !== undefined) {
416
+ if (this.#hubPreparation === undefined) {
417
+ this.#hubPreparation = (async () => {
418
+ if (this.#closed)
419
+ throw new Error('hermes gateway client closed');
420
+ const prepare = this.#spawnOpts.prepareHub ?? prepareHermesHub;
421
+ this.#hubSession = await prepare(hub);
422
+ if (this.#closed)
423
+ throw new Error('hermes gateway client closed');
424
+ })().catch(error => { this.#fail(error instanceof Error ? error : new Error(String(error))); throw error; });
425
+ }
426
+ return this.#hubPreparation.then(() => this.#startPrepared());
427
+ }
428
+ return this.#startPrepared();
429
+ }
430
+ #startPrepared() {
402
431
  if (this.#readyPromise === null) {
403
432
  if (this.#closed || this.#failure !== null) {
404
433
  return Promise.reject(this.#failure ?? new Error("hermes gateway client closed"));
@@ -528,6 +557,7 @@ export class HermesGatewayClient {
528
557
  if (this.#closed)
529
558
  return;
530
559
  this.#closed = true;
560
+ this.#spawnOpts.hub?.dispose();
531
561
  this.#clearSpawnGrace();
532
562
  // Fire-and-forget best effort: write the request directly (the RPC path
533
563
  // would refuse under #closed and nothing will read the response anyway).
@@ -933,7 +963,7 @@ export class HermesGatewayClient {
933
963
  clearTimeout(this.#readyTimer);
934
964
  this.#readyTimer = null;
935
965
  }
936
- if (!this.#readySettled) {
966
+ if (!this.#readySettled && this.#readyPromise !== null) {
937
967
  this.#readySettled = true;
938
968
  this.#readyReject(error);
939
969
  }
@@ -999,3 +1029,12 @@ export class HermesGatewayClient {
999
1029
  }
1000
1030
  }
1001
1031
  }
1032
+ async function prepareHermesHub(binding) {
1033
+ const root = resolveHermesRoot();
1034
+ const python = resolveHermesPython(root);
1035
+ return prepareHermesNativePlugin(binding, join(homedir(), '.hermes'), async (key) => {
1036
+ const env = { ...process.env, PYTHONPATH: root };
1037
+ delete env.HERMES_HOME;
1038
+ await promisify(execFile)(python, ['-m', 'hermes_cli.main', 'plugins', 'enable', key], { cwd: root, env, timeout: 30_000 });
1039
+ });
1040
+ }
@@ -26,6 +26,7 @@ import { join } from "node:path";
26
26
  import { Service } from "@deepseek-ai/cordis";
27
27
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
28
28
  import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
29
+ import { bindForeignHub } from "@pgmi-builds/superd-hub/foreign-hub";
29
30
  import { HermesGatewayClient, HERMES_SPAWN_GRACE_MS } from "./hermes-client.js";
30
31
  import { HermesAgent } from "./agent.js";
31
32
  import { HermesLlmAdapter, HERMES_PROVIDER_ID } from "./adapter.js";
@@ -296,6 +297,9 @@ export class HermesProvider extends Service {
296
297
  this.runtime.ctx.logger.warn(`hermes-provider: model route swap failed: ${String(error)}`);
297
298
  });
298
299
  }
300
+ hubFor(sessionId) {
301
+ return bindForeignHub('hermes', sessionId, this.home, () => this.runtime.ctx.agents.get(sessionId));
302
+ }
299
303
  async createAgent(ownerCtx, options) {
300
304
  const loopCtx = this.runtime.ctx;
301
305
  const id = options.sessionId;
@@ -316,7 +320,7 @@ export class HermesProvider extends Service {
316
320
  // Agent-held client: the spawn-grace reaper is NEVER armed here (2026-09-17
317
321
  // ruling 4 — the session may legitimately materialize far beyond any
318
322
  // grace; disposal is the agent lifecycle's job, not a timer's).
319
- const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
323
+ const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true, hub: this.hubFor(id) }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
320
324
  const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
321
325
  ...(options.seed === undefined ? {} : { seed: options.seed }),
322
326
  ...(meta === undefined ? {} : { meta }),
@@ -360,7 +364,9 @@ export class HermesProvider extends Service {
360
364
  if (record === undefined) {
361
365
  throw new Error(`cannot resume session "${id}": no Hermes gateway session is recorded for this Dash session id`);
362
366
  }
363
- if (record.gatewayStoredSessionId === null || record.gatewayStoredSessionId === undefined) {
367
+ const persistence = this.requirePersistence("resume");
368
+ if ((record.gatewayStoredSessionId === null || record.gatewayStoredSessionId === undefined)
369
+ && (await readStoredEvents(persistence, id, options.signal)).some(event => event.type === "turn/start" || event.type === "user/message" || event.type === "assistant/message")) {
364
370
  throw new Error(`cannot resume session "${id}": no durable gateway session id is recorded (this record predates durable-id capture; re-create the session)`);
365
371
  }
366
372
  // Spawn cwd comes from the recorded pairing — re-realpath and re-verify.
@@ -368,7 +374,6 @@ export class HermesProvider extends Service {
368
374
  if (spawnCwd === undefined) {
369
375
  throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
370
376
  }
371
- const persistence = this.requirePersistence("resume");
372
377
  // Preset (record-only): env override, else the recorded preset, else the
373
378
  // persisted session log, else the permission service's default.
374
379
  const envMode = envApprovalMode();
@@ -380,7 +385,7 @@ export class HermesProvider extends Service {
380
385
  trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"}`);
381
386
  // Spawn the gateway child (no session bound); the FIRST prompt re-attaches
382
387
  // via resumeSession(record.gatewayStoredSessionId) — the DURABLE key.
383
- const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
388
+ const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true, hub: this.hubFor(id) }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
384
389
  let handle;
385
390
  let stored;
386
391
  let preparation;
@@ -403,7 +408,7 @@ export class HermesProvider extends Service {
403
408
  this.followGatewaySessionId(id, client);
404
409
  handedOff = true;
405
410
  const agentOptions = await this.sessionAgentOptions(options.agentOptions);
406
- return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId));
411
+ return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId ?? undefined));
407
412
  }
408
413
  catch (error) {
409
414
  trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);