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
@@ -286,6 +286,8 @@ export class ClaudeAgent {
286
286
  observedDefaultModel = "";
287
287
  /** Deliveries parked while idle without a wakeup; flushed at the next prompt. */
288
288
  remoteQueue = [];
289
+ /** Busy peer deliveries start their own turn inside the native Mod. */
290
+ pendingHubTurns = [];
289
291
  /** Serialization chain for async prompt deliveries (attachment prefetch). */
290
292
  deliveryChain = Promise.resolve();
291
293
  sessionIdentityCommitted = false;
@@ -456,13 +458,20 @@ export class ClaudeAgent {
456
458
  }
457
459
  }
458
460
  // ── internals ────────────────────────────────────────────────────────────
461
+ isHubMessage(message) {
462
+ const source = message.source;
463
+ return source?.kind === "a2a-relay" || source?.kind === "a2a-notice"
464
+ || ((source?.kind === "user" || source?.kind === "user-rpc") && source.rpcId?.startsWith("a2a-") === true);
465
+ }
459
466
  deliver(message, target, wakeup) {
460
467
  if (this.disposed)
461
468
  return;
462
469
  const busy = this.turnOpen || this.streaming;
463
470
  trace(`deliver target=${target} wakeup=${wakeup} busy=${busy}`);
464
471
  if (busy) {
465
- this.appendUserMessage(message);
472
+ const peer = this.isHubMessage(message);
473
+ if (!peer)
474
+ this.appendUserMessage(message);
466
475
  this.scheduleDelivery(async () => {
467
476
  const payload = await this.promptContent(message);
468
477
  if (payload.kind === "empty") {
@@ -473,8 +482,22 @@ export class ClaudeAgent {
473
482
  trace(`busy ${target} delivery for turn ${this.dashTurn} has no attachable content — not pushed`);
474
483
  return;
475
484
  }
476
- const sent = target === "next-turn" ? this.client.followUp(payload.content) : this.client.steer(payload.content);
477
- await sent;
485
+ if (peer) {
486
+ const entry = { message, id: undefined, started: false };
487
+ this.pendingHubTurns.push(entry);
488
+ try {
489
+ entry.id = await this.client.receiveHub(payload.content);
490
+ }
491
+ catch (error) {
492
+ const index = this.pendingHubTurns.indexOf(entry);
493
+ if (index >= 0)
494
+ this.pendingHubTurns.splice(index, 1);
495
+ throw error;
496
+ }
497
+ }
498
+ else {
499
+ await (target === "next-turn" ? this.client.followUp(payload.content) : this.client.steer(payload.content));
500
+ }
478
501
  });
479
502
  return;
480
503
  }
@@ -687,7 +710,7 @@ export class ClaudeAgent {
687
710
  this.fail(new Error(payload.reason));
688
711
  return;
689
712
  }
690
- void this.client.prompt(payload.content).catch((error) => this.fail(error));
713
+ void (this.isHubMessage(message) ? this.client.receiveHub(payload.content) : this.client.prompt(payload.content)).catch((error) => this.fail(error));
691
714
  }
692
715
  /**
693
716
  * One-shot first-turn identity commit: the agent-preset stamp plus, when
@@ -1163,8 +1186,47 @@ export class ClaudeAgent {
1163
1186
  trace(`forwarding "${line}" to Claude`);
1164
1187
  this.prompt(createUserMessage({ content: [{ type: "text", text: line }], source: { kind: "user" } }));
1165
1188
  }
1189
+ openStartedHubTurn() {
1190
+ if (this.turnOpen)
1191
+ return;
1192
+ const index = this.pendingHubTurns.findIndex(entry => entry.started);
1193
+ if (index < 0)
1194
+ return;
1195
+ const { message } = this.pendingHubTurns.splice(index, 1)[0];
1196
+ this.openNativeTurn(message);
1197
+ }
1198
+ openNativeTurn(message) {
1199
+ this.reserveTurn();
1200
+ clearTimeout(this.idleExitTimer);
1201
+ this.idleExitTimer = undefined;
1202
+ this.beginActivity();
1203
+ this.markRunning();
1204
+ this.session.append("turn/start", { turn: this.dashTurn });
1205
+ if (this.turnRoute !== undefined)
1206
+ this.commitRequestHeader(this.turnRoute);
1207
+ this.emitRequestContext(this.turnRoute);
1208
+ if (message !== undefined)
1209
+ this.appendUserMessage(message);
1210
+ }
1166
1211
  handleEvent(event) {
1212
+ // Any native plugin may start a model turn without an SDK input. Keep its
1213
+ // observed output inside a valid turn even if its input is absent from
1214
+ // the SDK stream. An explicit Hub receipt alone establishes peer source.
1215
+ if (!this.turnOpen && (event.parentToolUseId ?? null) === null
1216
+ && ["assistant_reasoning", "assistant_text", "assistant_message"].includes(event.type)) {
1217
+ this.openStartedHubTurn();
1218
+ if (!this.turnOpen)
1219
+ this.openNativeTurn();
1220
+ }
1167
1221
  switch (event.type) {
1222
+ case "hub_turn_start": {
1223
+ const entry = this.pendingHubTurns.find(item => item.id === event.messageId);
1224
+ if (entry !== undefined) {
1225
+ entry.started = true;
1226
+ this.openStartedHubTurn();
1227
+ }
1228
+ break;
1229
+ }
1168
1230
  case "session_init":
1169
1231
  trace(`session_init model=${String(event.model)}`);
1170
1232
  this.observeModelCatalog(event);
@@ -1242,6 +1304,7 @@ export class ClaudeAgent {
1242
1304
  : { kind: "completed" });
1243
1305
  this.markIdle();
1244
1306
  this.endActivity();
1307
+ this.openStartedHubTurn();
1245
1308
  break;
1246
1309
  }
1247
1310
  case "todo_write": {
@@ -67,6 +67,8 @@ import { loadClaudeQuery, resolveClaudeBinary } from "./sdk-runtime.js";
67
67
  import { InputQueue } from "./input-queue.js";
68
68
  import { projectClaudeEvent } from "./claude-events.js";
69
69
  import { NATIVE_LAUNCH } from "./claude-launch.js";
70
+ import { resolveClaudeHome } from "./claude-home.js";
71
+ import { prepareClaudeNativePlugin } from "../../superd-hub/dist/foreign-claude.js";
70
72
  /** The default executable resolution: Task 1's `resolveClaudeBinary` —
71
73
  * `$CLAUDE_EXECUTABLE` (only while it exists) > `claude` on PATH >
72
74
  * `~/.local/bin/claude` > `/usr/local/bin/claude`. `undefined` means "no
@@ -158,6 +160,10 @@ export class ClaudeSdkClient {
158
160
  launch;
159
161
  /** True while a user turn is in flight (set on push, cleared on `turn_end`/`agent_end`). */
160
162
  turnActive = false;
163
+ hub;
164
+ prepareHub;
165
+ preparation;
166
+ hubSession;
161
167
  queue;
162
168
  query;
163
169
  pendingControls = [];
@@ -177,6 +183,15 @@ export class ClaudeSdkClient {
177
183
  this.canUseTool = config.canUseTool;
178
184
  this.allowDangerouslySkipPermissions = config.allowDangerouslySkipPermissions;
179
185
  this.launch = config.launch ?? NATIVE_LAUNCH;
186
+ this.hub = config.hub;
187
+ this.hub?.onTurnStarted(id => {
188
+ if (!this.closed) {
189
+ this.turnActive = true;
190
+ for (const listener of this.listeners)
191
+ listener({ type: "hub_turn_start", messageId: id });
192
+ }
193
+ });
194
+ this.prepareHub = config.prepareHub ?? (config.hub === undefined ? undefined : () => prepareClaudeNativePlugin(config.hub, resolveClaudeHome()));
180
195
  refcount += 1;
181
196
  }
182
197
  /**
@@ -259,7 +274,13 @@ export class ClaudeSdkClient {
259
274
  async ensureStarted() {
260
275
  if (this.query !== undefined)
261
276
  return;
277
+ this.ensureUsable();
278
+ if (this.prepareHub !== undefined) {
279
+ this.preparation ??= this.prepareHub().then(value => { this.hubSession = value; });
280
+ await this.preparation;
281
+ }
262
282
  const f = await resolveFactory(this.worldHome); // await BEFORE the sync section
283
+ this.ensureUsable();
263
284
  if (this.query !== undefined)
264
285
  return; // lost the race to a concurrent first prompt — reuse its query
265
286
  const queue = new InputQueue();
@@ -282,6 +303,22 @@ export class ClaudeSdkClient {
282
303
  await this.ensureStarted();
283
304
  this.push(content, undefined);
284
305
  }
306
+ /** Peer messages enter the real Claude runtime through the native Mod inbox. */
307
+ async receiveHub(content) {
308
+ this.ensureUsable();
309
+ await this.ensureStarted();
310
+ if (this.hub === undefined || this.hubSession === undefined) {
311
+ // Standalone adapter has no Hub host and keeps its usual input channel.
312
+ this.push(content, undefined);
313
+ return;
314
+ }
315
+ const text = typeof content === "string" ? content : content.map(block => block.type === "text" ? block.text : "").join("\n\n");
316
+ if (!text)
317
+ throw new Error("[E_HUB_INPUT] Hub messages require text");
318
+ const id = await this.hub.enqueue(text);
319
+ this.turnActive = true;
320
+ return id;
321
+ }
285
322
  /** Surface parity with prompt: a plain queued user turn. */
286
323
  async followUp(content) {
287
324
  this.ensureUsable();
@@ -328,18 +365,23 @@ export class ClaudeSdkClient {
328
365
  this.closed = true;
329
366
  this.queue?.close();
330
367
  this.query?.close();
368
+ this.hub?.dispose();
331
369
  refcount = Math.max(0, refcount - 1);
332
370
  }
333
371
  // --- internals ------------------------------------------------------------
334
372
  buildOptions() {
335
373
  const launchExecutable = this.launch.pathToClaudeCodeExecutable;
336
374
  const executable = launchExecutable !== undefined && launchExecutable !== "" ? launchExecutable : executableResolver();
375
+ const env = { ...process.env, ...this.launch.env };
376
+ delete env.SUPERD_HUB_SESSION;
377
+ if (this.hubSession !== undefined)
378
+ env.SUPERD_HUB_SESSION = this.hubSession;
337
379
  return {
338
380
  cwd: this.cwdPath,
339
381
  // The launch env holds ADDITIONS only; spreading process.env first keeps
340
382
  // PATH/HOME and the user's own key alive (NATIVE_LAUNCH carries an empty
341
383
  // env, so the native shape is exactly `{ ...process.env }`).
342
- env: { ...process.env, ...this.launch.env },
384
+ env,
343
385
  // The `--settings` command-line tier (a JSON string) outranks the user's
344
386
  // native settings.json env, so a provider-pinned launch rides it here
345
387
  // (2026-10-04 F-B: without it the native env pins the endpoint).
@@ -387,6 +429,9 @@ export class ClaudeSdkClient {
387
429
  if (event.type === "session_init" && typeof event.sessionId === "string") {
388
430
  this.threadIdValue = event.sessionId;
389
431
  }
432
+ if ((event.parentToolUseId ?? null) === null
433
+ && ["assistant_text", "assistant_reasoning", "assistant_message"].includes(event.type))
434
+ this.turnActive = true;
390
435
  if (event.type === "turn_end")
391
436
  this.turnActive = false;
392
437
  this.emit(event);
@@ -27,6 +27,7 @@ import { ClaudeSdkClient, probeClaudeModels } from "./claude-client.js";
27
27
  import { ClaudeAgent } from "./agent.js";
28
28
  import { ClaudeLlmAdapter, CLAUDE_PROVIDER_ID, routeIds } from "./adapter.js";
29
29
  import { resolveClaudeHome } from "./claude-home.js";
30
+ import { bindForeignHub } from "../../superd-hub/dist/foreign-hub.js";
30
31
  import { resetProjectionState } from "./claude-events.js";
31
32
  import { claudeSessionIdFromDsh } from "./session-id.js";
32
33
  import { claudePermissionMode, defaultPermissionPreset, modeChangePatch, presetFromEvents, } from "./permission.js";
@@ -308,6 +309,7 @@ export class ClaudeProvider extends Service {
308
309
  cwd: spawnCwd,
309
310
  worldHome: this.home,
310
311
  canUseTool,
312
+ hub: bindForeignHub("claude", String(id), this.home, () => agentRef.current),
311
313
  allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
312
314
  ...overrides,
313
315
  });
@@ -419,6 +421,7 @@ export class ClaudeProvider extends Service {
419
421
  cwd: spawnCwd,
420
422
  worldHome: this.home,
421
423
  canUseTool,
424
+ hub: bindForeignHub("claude", String(id), this.home, () => agentRef.current),
422
425
  allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
423
426
  ...overrides,
424
427
  });
@@ -49,7 +49,7 @@ import { promisify } from "node:util";
49
49
  /** The coding-agent SDK this line drives (a plain npm package, never declared). */
50
50
  export const SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
51
51
  /** The pin, written here rather than in `package.json` — the marker carries it. */
52
- export const SDK_PACKAGE_VERSION = "0.3.263";
52
+ export const SDK_PACKAGE_VERSION = "0.3.291";
53
53
  export function sdkRuntimeLayout(worldHome) {
54
54
  return {
55
55
  dir: join(worldHome, "sdk-runtime"),
@@ -139,7 +139,7 @@ export function resolveClaudeBinary() {
139
139
  export function sdkInstallArgv(version, mode) {
140
140
  const argv = [
141
141
  "install",
142
- "--exact",
142
+ "--save-exact",
143
143
  "--ignore-scripts",
144
144
  "--omit=dev",
145
145
  "--no-audit",
@@ -1,3 +1,4 @@
1
+ import { ToolCallRecovery } from "@deepseek-ai/dsh-session";
1
2
  import { agentEvents } from "@deepseek-ai/dsh-agent";
2
3
  import { Inbox } from "./inbox.js";
3
4
  import { canonicalHeader, headerEquals } from "@deepseek-ai/dsh-session";
@@ -267,6 +268,8 @@ export class CodexAgent {
267
268
  #step = 0;
268
269
  #lastTurn = 0;
269
270
  #turnOpen = false;
271
+ #turnCommitted = false;
272
+ #stepOpen = false;
270
273
  #cancelCause = null;
271
274
  /** Terminal model failure of the open turn (`message_end` stopReason "error"); consumed by `agent_end`. */
272
275
  #pendingFailure = null;
@@ -526,7 +529,7 @@ export class CodexAgent {
526
529
  catch (error) {
527
530
  trace(`#startTurn cold start failed: ${String(error)}`);
528
531
  if (this.#turnOpen) {
529
- this.session.append("turn/start", { turn: this.#dashTurn });
532
+ this.#commitTurnStart();
530
533
  this.session.append("user/message", message, { surfaceOp: "append" });
531
534
  this.#closeTurn({ kind: "error", error: { message: String(error), code: "UNKNOWN" } });
532
535
  }
@@ -535,7 +538,7 @@ export class CodexAgent {
535
538
  }
536
539
  if (this.#disposed)
537
540
  return;
538
- this.session.append("turn/start", { turn: this.#dashTurn });
541
+ this.#commitTurnStart();
539
542
  if (route !== undefined)
540
543
  this.#commitRequestHeader(route);
541
544
  this.#emitRequestContext(route);
@@ -812,7 +815,7 @@ export class CodexAgent {
812
815
  return;
813
816
  this.#reserveTurn();
814
817
  this.#localUserPending = localUser;
815
- this.session.append("turn/start", { turn: this.#dashTurn });
818
+ this.#commitTurnStart();
816
819
  // This path opens a turn for a message the CLIENT queued and is now
817
820
  // generating, so the child is already running: resolve the route for the
818
821
  // log's benefit only. It cannot have moved since the turn it belongs to
@@ -824,9 +827,32 @@ export class CodexAgent {
824
827
  this.#emitRequestContext(route);
825
828
  this.session.append("user/message", message, { surfaceOp: "append" });
826
829
  }
830
+ #commitTurnStart() {
831
+ if (this.#turnCommitted)
832
+ return;
833
+ this.session.append("turn/start", { turn: this.#dashTurn });
834
+ this.#turnCommitted = true;
835
+ }
827
836
  #closeTurn(reason) {
828
837
  if (!this.#turnOpen)
829
838
  return;
839
+ this.#commitTurnStart();
840
+ if (this.#stepOpen) {
841
+ const events = this.session.snapshotEvents();
842
+ const start = events.findLastIndex(event => event.type === "step/start");
843
+ const recovery = new ToolCallRecovery();
844
+ for (const event of events.slice(start))
845
+ recovery.observe(event);
846
+ for (const result of recovery.results()) {
847
+ this.session.append("tool/result", result.data, {
848
+ surfaceOp: "append",
849
+ ...(result.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: result.sourceEventSeqs }),
850
+ });
851
+ }
852
+ this.session.append("step/end", { turn: this.#dashTurn, step: this.#step });
853
+ this.#stepOpen = false;
854
+ }
855
+ this.#turnCommitted = false;
830
856
  this.#turnOpen = false;
831
857
  this.#localUserPending = false;
832
858
  this.session.append("turn/end", { turn: this.#dashTurn, reason });
@@ -848,6 +874,7 @@ export class CodexAgent {
848
874
  this.#stepStartPending = false;
849
875
  this.#step += 1;
850
876
  this.session.append("step/start", { turn: this.#dashTurn, step: this.#step });
877
+ this.#stepOpen = true;
851
878
  trace(`step/start turn=${this.#dashTurn} step=${this.#step}`);
852
879
  }
853
880
  /**
@@ -986,10 +1013,6 @@ export class CodexAgent {
986
1013
  trace("event turn_start");
987
1014
  this.#stepStartPending = true;
988
1015
  this.#pendingFailure = null;
989
- // Open the step immediately (the SDK may never send message_start)
990
- // and try the system prompt now that the thread's rollout exists.
991
- this.#commitStepStart();
992
- this.#emitSystemMessage();
993
1016
  break;
994
1017
  case "message_start": {
995
1018
  const message = event.message;
@@ -1046,6 +1069,7 @@ export class CodexAgent {
1046
1069
  break;
1047
1070
  }
1048
1071
  case "tool_execution_start": {
1072
+ this.#flushDeferredAssistant();
1049
1073
  const callId = String(event.toolCallId ?? "");
1050
1074
  const name = String(event.toolName ?? "");
1051
1075
  const argumentsJson = String(event.argumentsJson ?? JSON.stringify(event.args ?? {}));
@@ -1112,8 +1136,9 @@ export class CodexAgent {
1112
1136
  // DSH turn-token fold requires the sample on a message of the SAME
1113
1137
  // turn. Flushed inside the still-open step, BEFORE step/end.
1114
1138
  this.#flushDeferredAssistant(convertUsage(event.data?.usage));
1115
- if (this.#turnOpen && this.#step > 0) {
1139
+ if (this.#turnOpen && this.#stepOpen) {
1116
1140
  this.session.append("step/end", { turn: this.#dashTurn, step: this.#step });
1141
+ this.#stepOpen = false;
1117
1142
  }
1118
1143
  break;
1119
1144
  case "agent_end": {
@@ -406,6 +406,7 @@ export class CodexAppServerClient {
406
406
  if (this.#closed)
407
407
  return;
408
408
  this.#closed = true;
409
+ this.#options.hub?.dispose();
409
410
  this.#sessionEpoch += 1;
410
411
  this.#transport?.close();
411
412
  this.#transport = undefined;
@@ -449,6 +450,7 @@ export class CodexAppServerClient {
449
450
  transport = transportFactory({
450
451
  ...(this.#options.cwd === undefined ? {} : { cwd: this.#options.cwd }),
451
452
  ...(this.#options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: this.#options.requestTimeoutMs }),
453
+ ...(this.#options.worldHome === undefined ? {} : { worldHome: this.#options.worldHome }),
452
454
  args: ["app-server", ...launch.argv],
453
455
  env: childEnv,
454
456
  onNotification: (notification) => this.#onNotification(notification),
@@ -462,7 +464,7 @@ export class CodexAppServerClient {
462
464
  }
463
465
  this.#transport = transport;
464
466
  if (first) {
465
- transport.start();
467
+ await transport.start();
466
468
  const info = asRecord(await transport.request("initialize", {
467
469
  clientInfo: { name: "superd-agent-codex", title: "Super D agent-codex", version: "0.1.0" },
468
470
  capabilities: { experimentalApi: true, requestAttestation: false },
@@ -488,7 +490,13 @@ export class CodexAppServerClient {
488
490
  ...(this.#options.sandboxMode === undefined ? {} : { sandbox: this.#options.sandboxMode }),
489
491
  };
490
492
  const response = asRecord(resumeId === null
491
- ? await transport.request("thread/start", shared)
493
+ ? await transport.request("thread/start", {
494
+ ...shared,
495
+ ...(() => {
496
+ const definition = this.#options.hub?.definition();
497
+ return definition === undefined ? {} : { dynamicTools: [{ type: 'function', ...definition }] };
498
+ })(),
499
+ })
492
500
  : await transport.request("thread/resume", { threadId: resumeId, ...shared }));
493
501
  const thread = asRecord(response?.["thread"]);
494
502
  const threadId = asString(thread?.["id"]) ?? resumeId;
@@ -658,7 +666,22 @@ export class CodexAppServerClient {
658
666
  // closes the just-finished Dash turn and opens a fresh one.
659
667
  void this.#startTurn(next).catch((error) => this.#fail(error));
660
668
  }
661
- #onServerRequest(request) {
669
+ async #onServerRequest(request) {
670
+ if (request.method === 'item/tool/call') {
671
+ const params = asRecord(request.params);
672
+ try {
673
+ const hub = this.#options.hub;
674
+ if (hub === undefined || params?.tool !== 'superd-hub' ||
675
+ params.threadId !== this.#threadId || (params.namespace !== undefined && params.namespace !== null)) {
676
+ throw new Error('[E_HUB_BINDING] Tool call does not belong to this Super D session; reconnect through Super D');
677
+ }
678
+ const result = await hub.execute(params.arguments);
679
+ return { result: { success: true, contentItems: [{ type: 'inputText', text: JSON.stringify(result) }] } };
680
+ }
681
+ catch (error) {
682
+ return { result: { success: false, contentItems: [{ type: 'inputText', text: error instanceof Error ? error.message : String(error) }] } };
683
+ }
684
+ }
662
685
  const handler = this.#serverRequestHandler;
663
686
  if (handler === undefined)
664
687
  return failClosedResponse(request.method);
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Narrow hand-written types for the codex `app-server` JSONL JSON-RPC wire.
3
3
  *
4
- * DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for the
5
- * `@openai/codex` version this package pins (0.154.0). That tree is ~617 files;
4
+ * DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for
5
+ * `@openai/codex@0.154.0` (the SDK line's tested pair). The app-server line's
6
+ * runtime target is the exact-pin `@openai/codex@0.160.0`, lazily fetched into
7
+ * the world home (codex-runtime-fetch.ts) — never an install-time dependency.
8
+ * That tree is ~617 files;
9
+
6
10
  * this module is a hand-written, deliberately narrow subset covering exactly
7
11
  * the envelopes, requests, notifications and item shapes
8
12
  * `src/app-server-client.ts` and `src/app-server-events.ts` consume. Each
@@ -7,7 +7,10 @@
7
7
  * `{"id":…,"result":…}` / `{"id":…,"error":{…}}`).
8
8
  *
9
9
  * This module owns exactly four things and nothing else:
10
- * 1. runtime selection (override → installed desktop → CLI → pinned package),
10
+ * 1. runtime selection (override → PATH CLI → desktop core → fetched world-home
11
+ * runtime — codex-runtime-fetch.ts; the pinned package is NEVER an
12
+ * install-time dependency, 2026-10-06 ruling),
13
+
11
14
  * 2. process spawn + line framing + per-id request/response correlation,
12
15
  * 3. dispatch of server-initiated notifications and server-requests,
13
16
  * 4. teardown (process GROUP kill — the app-server may spawn stdio MCP
@@ -22,6 +25,7 @@
22
25
  * process, and the client calls it on first real work.
23
26
  */
24
27
  import { spawn } from "node:child_process";
28
+ import { createCodexRuntimeDeps, ensureCodexRuntime } from "./codex-runtime-fetch.js";
25
29
  import { resolveCodexRuntime } from "./codex-runtime.js";
26
30
  export { resolveCodexCommand, resolveCodexRuntime } from "./codex-runtime.js";
27
31
  /** Per-request protocol error surfaced by the server (JSON-RPC `error` object). */
@@ -69,15 +73,14 @@ export class CodexAppServerTransport {
69
73
  }
70
74
  /**
71
75
  * Spawn the child exactly once. Idempotent.
72
- * @throws when the child cannot be spawned at all (bad executable).
76
+ * @throws when no runtime can be discovered or fetched, or the child cannot
77
+ * be spawned at all (bad executable).
73
78
  */
74
- start() {
79
+ async start() {
75
80
  if (this.#child !== undefined)
76
81
  return;
77
82
  const env = this.#options.env ?? { ...process.env };
78
- const runtime = resolveCodexRuntime({
79
- env: this.#options.command === undefined ? env : { ...env, CODEX_BIN: this.#options.command },
80
- });
83
+ const runtime = await this.#resolveRuntime(env);
81
84
  const args = [...(this.#options.args ?? ["app-server"])];
82
85
  // `detached` gives the child its own process group so teardown can kill
83
86
  // the whole tree (stdio MCP descendants included), not just the root.
@@ -99,6 +102,20 @@ export class CodexAppServerTransport {
99
102
  child.on("error", (error) => this.#onExit(null, null, error));
100
103
  child.on("exit", (code, signal) => this.#onExit(code, signal));
101
104
  }
105
+ /**
106
+
107
+ * Resolve the child's runtime. An explicit `command` and world-home-less
108
+ * calls stay on the pure sync ladder; only a world home arms the lazy fetch
109
+ * of the pinned package (2026-10-06 ruling — never an install-time dep).
110
+ */
111
+ async #resolveRuntime(env) {
112
+ if (this.#options.command !== undefined || this.#options.worldHome === undefined) {
113
+ return resolveCodexRuntime({
114
+ env: this.#options.command === undefined ? env : { ...env, CODEX_BIN: this.#options.command },
115
+ });
116
+ }
117
+ return ensureCodexRuntime(this.#options.worldHome, createCodexRuntimeDeps({ env }));
118
+ }
102
119
  /** Send a request and await its result. Rejects on protocol error, timeout, or EOF. */
103
120
  request(method, params, options = {}) {
104
121
  if (this.#closed || this.#child === undefined) {
@@ -127,7 +127,7 @@ export function mergeModelCatalog(existing, absorbed, models, template) {
127
127
  if (seen.has(model.id))
128
128
  continue;
129
129
  seen.add(model.id);
130
- const base = bySlug.get(model.id) ?? fallback ?? {};
130
+ const base = { ...fallback, ...bySlug.get(model.id) };
131
131
  const entry = {
132
132
  ...base,
133
133
  slug: model.id,
@@ -138,6 +138,10 @@ export function mergeModelCatalog(existing, absorbed, models, template) {
138
138
  // carry verified hashes; remove stale generated values too. Context-limit
139
139
  // compaction and its window/threshold metadata remain unchanged.
140
140
  delete entry["comp_hash"];
141
+ // OpenAI's code-mode-only catalog entry cannot establish another
142
+ // endpoint's tool protocol. Omit it so Codex uses its normal tool mode.
143
+ // Also repair stale provider entries previously cloned from that cache.
144
+ delete entry["tool_mode"];
141
145
  if (model.contextWindow !== undefined) {
142
146
  // Codex reads context_window and caps against max_context_window; a stale
143
147
  // template value must not silently cap a larger real window.
@@ -359,6 +363,7 @@ export function catalogTemplate(codexHome, worldHome) {
359
363
  readIfExists(providerCatalogPath(worldHome, "superd-model-catalog")),
360
364
  absorbedPath === undefined ? undefined : readIfExists(absorbedPath),
361
365
  readIfExists(join(codexHome, SUPERD_CATALOG_FILE)),
366
+ readIfExists(join(codexHome, "models_cache.json")),
362
367
  ];
363
368
  for (const text of sources) {
364
369
  const first = catalogEntries(text)[0];