super-dsh 0.3.4-a → 0.3.4-c

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.
@@ -43,6 +43,7 @@ export function apply(ctx) {
43
43
  key: KEY,
44
44
  adapterPkg: '@pgmi-builds/agent-agy',
45
45
  worldHome,
46
+ host: ctx,
46
47
  });
47
48
  const world = ownWorld(ctx, { key: KEY, label: 'Antigravity' }, (ownerId) => spawnWorld({
48
49
  appName: `superd-${KEY}`,
@@ -48,6 +48,7 @@ export function apply(ctx) {
48
48
  key: KEY,
49
49
  adapterPkg: '@pgmi-builds/agent-agy-sdk',
50
50
  worldHome,
51
+ host: ctx,
51
52
  });
52
53
  const world = ownWorld(ctx, { key: KEY, label: 'Antigravity (SDK)' }, (ownerId) => spawnWorld({
53
54
  appName: `superd-${KEY}`,
@@ -42,6 +42,7 @@ export function apply(ctx) {
42
42
  key: KEY,
43
43
  adapterPkg: '@pgmi-builds/agent-codex',
44
44
  worldHome,
45
+ host: ctx,
45
46
  });
46
47
  const world = ownWorld(ctx, { key: KEY, label: 'Codex' }, (ownerId) => spawnWorld({
47
48
  appName: `superd-${KEY}`,
@@ -43,6 +43,7 @@ export function apply(ctx) {
43
43
  key: KEY,
44
44
  adapterPkg: '@pgmi-builds/agent-codex-sdk',
45
45
  worldHome,
46
+ host: ctx,
46
47
  });
47
48
  const world = ownWorld(ctx, { key: KEY, label: 'Codex (SDK)' }, (ownerId) => spawnWorld({
48
49
  appName: `superd-${KEY}`,
@@ -2,10 +2,11 @@
2
2
  * HermesGatewayClient — the reusable TUI-gateway client seam (AW-H Task 3).
3
3
  *
4
4
  * One client = one gateway child bound to one gateway session. Transports:
5
- * `stdio` (legacy default) spawns `python -m tui_gateway.entry` (owns the
6
- * Hermes home); `attach` (SUPERD_HERMES_TRANSPORT=attach) spawns the
5
+ * `stdio` spawns `python -m tui_gateway.entry` (owns the Hermes home
6
+ * exclusively); `attach` (SUPERD_HERMES_TRANSPORT=attach) spawns the
7
7
  * WS-attach bridge — same stdio ndjson JSON-RPC contract, frames relayed to
8
- * the profile's canonical gateway WebSocket. Spawn/env/ready patterns
8
+ * the profile's canonical gateway WebSocket; `auto` (default) = stdio first,
9
+ * flips to attach on the gateway ownership refusal (resolveHermesTransportMode).
9
10
  * follow the upstream reference client `ui-tui/src/gatewayClient.ts` and the
10
11
  * Task 1 spike (`src/spike.ts`, live-pinned protocol facts):
11
12
  *
@@ -170,20 +171,20 @@ export function loadHermesDotEnv() {
170
171
  return {};
171
172
  }
172
173
  }
173
- /** Gateway child transport. `stdio` = the legacy in-child `tui_gateway.entry`
174
- * server (owns the Hermes home exclusively). `attach` = the WS-attach bridge
175
- * (`bridge/hermes-ws-bridge.py`): frames ride the profile's canonical gateway
176
- * WebSocket via `hermes_cli.gateway_client.connect_gateway`, which attaches
177
- * to the running owner or starts an idle-exit one — REQUIRED whenever the
178
- * user's native Hermes gateway already owns `~/.hermes` (2026-11-02: every
179
- * stdio spawn died with `[gateway-exit] refused: ... owned by a running
180
- * gateway`, and the reserved-turn failure window corrupted session logs).
181
- * Selection: `SUPERD_HERMES_TRANSPORT` process env wins, then the native
182
- * `~/.hermes/.env` key (hot-editable without touching the host process),
183
- * else `stdio`. Unknown values fall back to `stdio`. */
184
- export function resolveHermesTransport(dotEnv = {}) {
185
- const raw = process.env.SUPERD_HERMES_TRANSPORT?.trim() || dotEnv.SUPERD_HERMES_TRANSPORT?.trim() || "stdio";
186
- return raw === "attach" ? "attach" : "stdio";
174
+ export function resolveHermesTransportMode(dotEnv = {}) {
175
+ const raw = (process.env.SUPERD_HERMES_TRANSPORT?.trim() || dotEnv.SUPERD_HERMES_TRANSPORT?.trim() || "auto").toLowerCase();
176
+ return raw === "stdio" || raw === "attach" ? raw : "auto";
177
+ }
178
+ /** The hermes-side refusal that makes stdio impossible while a resident
179
+ * gateway holds the home: `... is owned by a running gateway; attach to it
180
+ * instead` (hermes' own exit text, observed 2026-11-02 on PC + dev3). */
181
+ const GATEWAY_OWNERSHIP_REFUSAL = /owned by a running gateway/i;
182
+ /** Process-wide sticky: a resident gateway owns `~/.hermes`; every future
183
+ * spawn in this process (any client) skips the doomed stdio attempt. */
184
+ let gatewayOwnershipDetected = false;
185
+ /** Test-only: clear the sticky auto-attach flip between tests. */
186
+ export function resetHermesAutoAttachForTests() {
187
+ gatewayOwnershipDetected = false;
187
188
  }
188
189
  /** The WS-attach bridge script, shipped at the package root (`bridge/`). */
189
190
  export function hermesBridgeScriptPath() {
@@ -222,6 +223,8 @@ export class HermesGatewayClient {
222
223
  #readySettled = false;
223
224
  /** Pre-ready respawn attempts (full-lazy ruling: bounded at SPAWN_ATTEMPTS). */
224
225
  #spawnAttempts = 0;
226
+ /** Transport used by the CURRENT child attempt (drives the auto-attach flip). */
227
+ #transport = "stdio";
225
228
  #readyTimer = null;
226
229
  #killTimer = null;
227
230
  #failure = null;
@@ -271,7 +274,12 @@ export class HermesGatewayClient {
271
274
  const root = resolveHermesRoot();
272
275
  const python = resolveHermesPython(root);
273
276
  const dotEnv = loadHermesDotEnv();
274
- const transport = resolveHermesTransport(dotEnv);
277
+ const mode = resolveHermesTransportMode(dotEnv);
278
+ // auto: stdio first; once a resident gateway's ownership refusal is seen,
279
+ // the whole process flips to attach (sticky — later clients skip the
280
+ // doomed stdio attempt entirely). Explicit stdio/attach never flip.
281
+ const transport = mode === "attach" || (mode === "auto" && gatewayOwnershipDetected) ? "attach" : "stdio";
282
+ this.#transport = transport;
275
283
  const env = { ...(opts.env ?? process.env) };
276
284
  if (this.#hubSession !== undefined)
277
285
  env.SUPERD_HUB_SESSION = this.#hubSession;
@@ -953,6 +961,10 @@ export class HermesGatewayClient {
953
961
  this.#fail(error);
954
962
  return;
955
963
  }
964
+ if (!gatewayOwnershipDetected && this.#transport === "stdio" && GATEWAY_OWNERSHIP_REFUSAL.test(error.message)) {
965
+ gatewayOwnershipDetected = true;
966
+ this.#trace("gateway ownership refusal in spawn failure — auto-attach engaged for future spawns");
967
+ }
956
968
  if (this.#spawnAttempts < HERMES_SPAWN_ATTEMPTS) {
957
969
  this.#trace(`pre-ready failure on spawn attempt ${this.#spawnAttempts}/${HERMES_SPAWN_ATTEMPTS} — respawning: ${error.message.slice(0, 200)}`);
958
970
  this.#discardChildQuietly();
@@ -1048,6 +1060,10 @@ export class HermesGatewayClient {
1048
1060
  if (this.#stderrLines.length > STDERR_RING)
1049
1061
  this.#stderrLines.shift();
1050
1062
  this.#trace(`stderr: ${trimmed.slice(0, 240)}`);
1063
+ if (!gatewayOwnershipDetected && this.#transport === "stdio" && GATEWAY_OWNERSHIP_REFUSAL.test(trimmed)) {
1064
+ gatewayOwnershipDetected = true;
1065
+ this.#trace("gateway ownership refusal on stderr — auto-attach engaged for future spawns");
1066
+ }
1051
1067
  }
1052
1068
  #stderrTail() {
1053
1069
  return this.#stderrLines.slice(-20).join("\n");
@@ -46,6 +46,7 @@ export function apply(ctx) {
46
46
  key: KEY,
47
47
  adapterPkg: '@pgmi-builds/agent-hermes',
48
48
  worldHome,
49
+ host: ctx,
49
50
  });
50
51
  const world = ownWorld(ctx, { key: KEY, label: 'Hermes' }, (ownerId) => spawnWorld({
51
52
  appName: `superd-${KEY}`,
@@ -62,6 +62,7 @@ export function apply(ctx) {
62
62
  key: KEY,
63
63
  adapterPkg: '@pgmi-builds/agent-omp',
64
64
  worldHome,
65
+ host: ctx,
65
66
  });
66
67
  const world = ownWorld(ctx, { key: KEY, label: 'OMP' }, (ownerId) => spawnWorld({
67
68
  appName: `superd-${KEY}`,
@@ -79,6 +79,7 @@ export function apply(ctx) {
79
79
  key: KEY,
80
80
  adapterPkg: '@pgmi-builds/agent-omp-sdk',
81
81
  worldHome,
82
+ host: ctx,
82
83
  });
83
84
  const world = ownWorld(ctx, { key: KEY, label: 'OMP (SDK)' }, (ownerId) => spawnWorld({
84
85
  appName: `superd-${KEY}`,
@@ -43,6 +43,7 @@ export function apply(ctx) {
43
43
  key: KEY,
44
44
  adapterPkg: '@pgmi-builds/agent-pi',
45
45
  worldHome,
46
+ host: ctx,
46
47
  });
47
48
  const world = ownWorld(ctx, { key: KEY, label: 'Pi' }, (ownerId) => spawnWorld({
48
49
  appName: `superd-${KEY}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-dsh",
3
- "version": "0.3.4-a",
3
+ "version": "0.3.4-c",
4
4
  "description": "Super D for the DeepSeek Harness (DSH) — one dsh plugin that mounts the superd hub (runtime selector, roster, per-runtime agents) and joins local foreign-agent runtimes (OMP, Codex, Claude, Pi, Hermes, Antigravity) into the DSH Web UI. Each runtime keeps its native home and session store; the hub only bridges.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,6 +62,7 @@ export function apply(ctx, config) {
62
62
  key,
63
63
  adapterPkg: '@pgmi-builds/agent-claude',
64
64
  worldHome,
65
+ host: ctx,
65
66
  });
66
67
  const world = ownWorld(ctx, { key, label }, (ownerId) => spawnWorld({
67
68
  appName: `superd-${key}`,
@@ -23,6 +23,10 @@ export interface ProvisionWorldOptions {
23
23
  worldHome: string;
24
24
  /** Profile directory name under `<worldHome>/profiles`. Default `'web'`. */
25
25
  profileName?: string;
26
+ /** Host surface (the caller's ctx0 context) — carries `profileContext` for CTX0 mirrors. */
27
+ host?: {
28
+ get(name: string): unknown;
29
+ };
26
30
  }
27
31
  /**
28
32
  * Provision one world's nested profile (idempotent: rewrites profile files,
@@ -104,6 +104,36 @@ function resolveHostPackageDir(spec) {
104
104
  }
105
105
  throw new Error(`super-dsh: cannot resolve ${spec} from the running host (set SUPERD_DSH_ANCHOR)`);
106
106
  }
107
+ /**
108
+ * Copy the CTX0 profile patch's `ui-settings-general` row (id..next-row) verbatim
109
+ * into a world's patch template. Best-effort: no CTX0 facts or no row → no lines.
110
+ */
111
+ function mirrorCtx0SettingsGeneralRow(host) {
112
+ try {
113
+ const launcher = host?.get('profileContext');
114
+ if (launcher === undefined || typeof launcher.home !== 'string' || typeof launcher.name !== 'string')
115
+ return [];
116
+ const patch = readFileSync(join(launcher.home, 'profiles', launcher.name, 'cordis.patch.yml'), 'utf8');
117
+ const lines = patch.split('\n');
118
+ const start = lines.findIndex((line) => line.trim() === '- id: ui-settings-general');
119
+ if (start < 0)
120
+ return [];
121
+ let end = start + 1;
122
+ while (end < lines.length && !lines[end].startsWith('- id:'))
123
+ end++;
124
+ while (end > start && lines[end - 1].trim() === '')
125
+ end--;
126
+ return [
127
+ '# Mirror of the CTX0 profile patch (ui-settings-general): worlds project the',
128
+ '# same settings namespace values — the welcome-notice ack rides this row.',
129
+ ...lines.slice(start, end),
130
+ '',
131
+ ];
132
+ }
133
+ catch {
134
+ return []; // best-effort mirror: the notice degrades to today's behavior
135
+ }
136
+ }
107
137
  /**
108
138
  * Provision one world's nested profile (idempotent: rewrites profile files,
109
139
  * re-links packages — lstat-safe against dangling links). Returns whether
@@ -171,6 +201,13 @@ function provisionWorldProfileUnchecked(opts) {
171
201
  ' disabled: true',
172
202
  '- id: client-hmr',
173
203
  ' disabled: true',
204
+ // Native settings namespaces project their values from the composition
205
+ // row, not a mutable store: without this mirror every world reads
206
+ // `ui-settings-general` as UNACKNOWLEDGED and the native Preview Notice
207
+ // mounts (its Continue cannot save — the read-back projects the same
208
+ // composition). Mirror the CTX0 row verbatim so worlds see what ctx0
209
+ // sees; upstream version bumps flow through automatically.
210
+ ...mirrorCtx0SettingsGeneralRow(opts.host),
174
211
  '',
175
212
  ].join('\n'));
176
213
  // @pgmi-builds links, plugin-add shape: adapter + hub into the world