super-dsh 0.1.11-a → 0.1.11-b

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 (37) hide show
  1. package/README.md +34 -1
  2. package/agent-agy/dist/agent.d.ts +2 -0
  3. package/agent-agy/dist/agent.js +14 -0
  4. package/agent-agy/dist/agy-cli-client.d.ts +9 -0
  5. package/agent-agy/dist/agy-cli-client.js +56 -8
  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 +24 -2
  14. package/agent-codex/dist/codex-native-config.js +6 -1
  15. package/agent-codex/dist/index.js +6 -1
  16. package/agent-hermes/dist/hermes-client.js +44 -5
  17. package/agent-hermes/dist/index.js +6 -2
  18. package/agent-omp/dist/agent.js +36 -5
  19. package/agent-omp/dist/index.js +23 -3
  20. package/agent-omp/dist/lazy-rpc.js +11 -2
  21. package/agent-omp/dist/rpc-client.js +6 -1
  22. package/agent-pi/dist/agent.js +36 -9
  23. package/agent-pi/dist/index.js +6 -2
  24. package/agent-pi/dist/pi-client.js +13 -3
  25. package/package.json +2 -2
  26. package/superd-hub/dist/a2a-row.js +20 -3
  27. package/superd-hub/dist/foreign-agy.d.ts +3 -0
  28. package/superd-hub/dist/foreign-agy.js +110 -0
  29. package/superd-hub/dist/foreign-claude.d.ts +4 -0
  30. package/superd-hub/dist/foreign-claude.js +160 -0
  31. package/superd-hub/dist/foreign-extension.d.ts +10 -0
  32. package/superd-hub/dist/foreign-extension.js +27 -0
  33. package/superd-hub/dist/foreign-hub.d.ts +27 -0
  34. package/superd-hub/dist/foreign-hub.js +243 -0
  35. package/superd-hub/dist/foreign-native.d.ts +4 -0
  36. package/superd-hub/dist/foreign-native.js +115 -0
  37. package/superd-hub/package.json +9 -1
package/README.md CHANGED
@@ -45,12 +45,45 @@ and a LAN relay. The test launcher enables this mode without `--host` or
45
45
  | Label | Runtime | Integration |
46
46
  |---|---|---|
47
47
  | OMP | OMP (`~/.omp`) | host `omp` binary over RPC (`omp --mode rpc`, NDJSON v1), lazy per session |
48
- | Codex | Codex app-server (`~/.codex`) | desktop core first, CLI and bundled core fallback |
48
+ | Codex | Codex app-server (`~/.codex`) | CLI first, desktop core and lazy world-home fetch fallback |
49
49
  | Claude | Claude Code (`~/.claude`) | standalone adapter, joined by the hub |
50
50
  | Pi | pi coding agent (`~/.pi`) | in-process SDK |
51
51
  | Hermes | Hermes (`~/.hermes`) | gateway child |
52
52
  | Antigravity | agy SDK | Python bridge (`bridge/agy_bridge.py`) |
53
53
 
54
+ ### Foreign-agent Hub facility
55
+
56
+ Super D sessions in Pi, OMP, and Codex can use the same `superd-hub` facility
57
+ for agent roster/status, messaging, and shell execution. Tool descriptions,
58
+ argument validation, receipts, and reply guidance come from the existing Hub.
59
+ Replies wake the initiating session automatically; do not poll for them.
60
+
61
+ Pi and OMP share one native extension. It is loaded incrementally on the first
62
+ prompt, preserving normal extension discovery and native tools. Its per-session
63
+ wrapper lives under the Super D world home (`hub-extensions`, mode 0600), with a
64
+ session capability for a loopback-only HTTP listener. No global extension is
65
+ installed in `~/.pi` or `~/.omp`. Some OMP versions present registered extension
66
+ tools as devices: read `xd://superd-hub` for instructions and write action JSON
67
+ to that device.
68
+
69
+ Codex uses the official experimental app-server `dynamicTools` interface on
70
+ new threads. Callbacks bind to that thread and the Super D session; existing
71
+ native tools/plugins remain available. No global plugin is installed in
72
+ `~/.codex`. Resuming a thread created through this integration restores its
73
+ saved tool definition. Older threads without that definition need a new
74
+ Super D conversation to gain the facility. A native Codex client resuming one
75
+ of these threads does not supply Super D's callback host.
76
+
77
+ Pi/OMP extension closures cannot transparently renew an already-loaded listener
78
+ capability when the Hub row is unmounted/remounted. Restart Super D and resume
79
+ the conversation after that operation; the tool reports this recovery path
80
+ when its connection becomes unavailable. Whole-app restart/resume is tested.
81
+
82
+ Draft creation and passive history viewing create no extension file, listener,
83
+ or agent process. Closing a session invalidates its capability. Interrupted
84
+ tool operations retain conservative upstream recovery guidance about unknown
85
+ outcomes rather than encouraging blind retries.
86
+
54
87
  ### Codex in 0.1.11
55
88
 
56
89
  Codex discovers the local OpenAI OAuth login as a read-only provider in the
@@ -35,6 +35,7 @@ import { type WithSystemHead } from "../../superd-hub/dist/index.js";
35
35
  /** The CLI stream transport the agent drives (SDK bridge moved to agent-agy-sdk). */
36
36
  export type LiveAgyClient = Pick<AgyCliClient, "conversationId" | "lastUsage" | "onThinkingDelta" | "onToolEvent" | "setApprovalHandler" | "turn" | "close"> & {
37
37
  setSystemMessageHandler?(handler: (text: string) => void): void;
38
+ interrupt?: () => Promise<void>;
38
39
  };
39
40
  /** Classify a bridge turn failure into the stable {message, code} UI chip. */
40
41
  export declare function classifyTurnError(error: unknown): {
@@ -119,6 +120,7 @@ export declare class AgyAgent implements Agent {
119
120
  /** Flush ONE parked follow-up as a fresh prompt (never submit while busy). */
120
121
  private flushFollowUps;
121
122
  private pendingThinking;
123
+ private stopInFlight?;
122
124
  /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
123
125
  private unpairedCalls;
124
126
  private armTurnDeadline;
@@ -293,6 +293,8 @@ export class AgyAgent {
293
293
  this.ensureStreamBridge().push({ type: "text-delta", index: full.length, text: chunk });
294
294
  full += chunk;
295
295
  }
296
+ await this.stopInFlight;
297
+ this.stopInFlight = undefined;
296
298
  this.clearTurnDeadline();
297
299
  // SDK 0.1.17 streams ToolCall but NEVER ToolResult (verified live and
298
300
  // in SDK source: receive_chunks yields Thought/Text/ToolCall only; the
@@ -327,6 +329,8 @@ export class AgyAgent {
327
329
  this.flushFollowUps();
328
330
  }
329
331
  catch (error) {
332
+ await this.stopInFlight;
333
+ this.stopInFlight = undefined;
330
334
  this.clearTurnDeadline();
331
335
  this.abandonStreamBridge();
332
336
  const failure = classifyTurnError(error);
@@ -345,8 +349,10 @@ export class AgyAgent {
345
349
  if (this.turnOpen && this.step > 0)
346
350
  this.closeStep();
347
351
  this.closeTurn({ kind: "error", error: failure });
352
+ this.cancelCause = null;
348
353
  this.markIdle();
349
354
  this.endActivity();
355
+ this.flushFollowUps();
350
356
  }
351
357
  }
352
358
  appendSystemNotice(text) {
@@ -452,6 +458,7 @@ export class AgyAgent {
452
458
  this.deliverPrompt(entry.message);
453
459
  }
454
460
  pendingThinking = [];
461
+ stopInFlight;
455
462
  /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
456
463
  unpairedCalls = [];
457
464
  armTurnDeadline() {
@@ -462,6 +469,13 @@ export class AgyAgent {
462
469
  if (!this.turnOpen)
463
470
  return;
464
471
  trace(`turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms — failing open turn`);
472
+ if (this.client.interrupt !== undefined) {
473
+ // Keep ownership until the old CLI stops. Its catch pairs unresolved
474
+ // calls and only then starts the next queued message.
475
+ this.cancelCause = { kind: "hook", reason: `agy turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms` };
476
+ this.stopInFlight = this.client.interrupt().catch(error => trace(`deadline interrupt failed: ${String(error)}`));
477
+ return;
478
+ }
465
479
  this.abandonStreamBridge();
466
480
  if (this.step > 0)
467
481
  this.closeStep();
@@ -15,6 +15,7 @@
15
15
  *
16
16
  * Same surface as AgyBridgeClient so AgyAgent needs no changes.
17
17
  */
18
+ import type { ForeignHubBinding } from "@pgmi-builds/superd-hub/foreign-hub";
18
19
  import { type AgyAuthMode, type AgySettingsMutation } from "./agy-auth.js";
19
20
  export interface AgyCliOptions {
20
21
  /** Key or a resolver (onboarding-saved keys become visible at first spawn). */
@@ -49,6 +50,9 @@ export interface AgyCliOptions {
49
50
  authMode?: AgyAuthMode;
50
51
  spawnAttempts?: number;
51
52
  trace?: (...parts: unknown[]) => void;
53
+ hubBinding?: ForeignHubBinding;
54
+ hubStagingDirectory?: string;
55
+ prepareHub?: () => Promise<string | undefined>;
52
56
  }
53
57
  export type CliEvent = {
54
58
  event: "ready";
@@ -110,6 +114,9 @@ export declare class AgyCliClient {
110
114
  private dead;
111
115
  private conversationIdValue;
112
116
  private toolsSeen;
117
+ private hubPreparation?;
118
+ private hubSession?;
119
+ private prepare;
113
120
  onThinkingDelta?: (text: string) => void;
114
121
  onToolEvent?: (ev: {
115
122
  event: "tool_call";
@@ -163,5 +170,7 @@ export declare class AgyCliClient {
163
170
  private readyReceived;
164
171
  private expectReady;
165
172
  ping(): Promise<void>;
173
+ /** Stop one CLI execution, preserving its conversation and Hub capability. */
174
+ interrupt(): Promise<void>;
166
175
  close(): Promise<void>;
167
176
  }
@@ -16,6 +16,9 @@
16
16
  * Same surface as AgyBridgeClient so AgyAgent needs no changes.
17
17
  */
18
18
  import { spawn } from "node:child_process";
19
+ import { execFile } from "node:child_process";
20
+ import { promisify } from "node:util";
21
+ import { prepareAgyNativePlugin } from "@pgmi-builds/superd-hub/foreign-agy";
19
22
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
23
  import { join } from "node:path";
21
24
  import { authChildEnv, authSettingsMutation, nativeSettingsPath, } from "./agy-auth.js";
@@ -63,6 +66,24 @@ export class AgyCliClient {
63
66
  dead = false;
64
67
  conversationIdValue = "";
65
68
  toolsSeen = 0;
69
+ hubPreparation;
70
+ hubSession;
71
+ async prepare() {
72
+ if (this.dead)
73
+ throw new Error("agy client is closed");
74
+ this.hubPreparation ??= (async () => {
75
+ if (this.opts.hubBinding === undefined)
76
+ return;
77
+ this.hubSession = await (this.opts.prepareHub?.() ?? prepareAgyNativePlugin(this.opts.hubBinding, join(this.realHome(), ".gemini", "antigravity-cli"), this.opts.hubStagingDirectory, async (directory) => {
78
+ const exec = promisify(execFile);
79
+ await exec(this.opts.agyBin ?? "agy", ["plugin", "install", directory], { timeout: 60_000 });
80
+ await exec(this.opts.agyBin ?? "agy", ["plugin", "enable", "superd-hub"], { timeout: 60_000 });
81
+ }));
82
+ })();
83
+ await this.hubPreparation;
84
+ if (this.dead)
85
+ throw new Error("agy client is closed");
86
+ }
66
87
  onThinkingDelta;
67
88
  onToolEvent;
68
89
  setApprovalHandler(_handler) {
@@ -90,6 +111,8 @@ export class AgyCliClient {
90
111
  spawnCli() {
91
112
  if (this.child || this.dead)
92
113
  return;
114
+ this.pending.length = 0;
115
+ this.buffer = "";
93
116
  const mode = this.opts.authMode ?? "apiKey";
94
117
  // Only key mode patches settings (`modelProvider: "gemini"`); ADC and OAuth
95
118
  // need the DEFAULT backend, where that key must be ABSENT — the binary
@@ -107,8 +130,9 @@ export class AgyCliClient {
107
130
  "--input-format=stream-json", "--output-format=stream-json",
108
131
  "--disable-slash-commands",
109
132
  ];
110
- if (this.opts.conversationId)
111
- args.push(`--conversation=${this.opts.conversationId}`);
133
+ const conversationId = this.conversationIdValue || this.opts.conversationId;
134
+ if (conversationId)
135
+ args.push(`--conversation=${conversationId}`);
112
136
  if (this.opts.model)
113
137
  args.push(`--model=${this.opts.model}`);
114
138
  if (this.opts.effort)
@@ -127,6 +151,9 @@ export class AgyCliClient {
127
151
  // ADC, the key for key mode, and the auto-update pin on EVERY mode.
128
152
  // NO_PROXY keeps the dev3 loopback bridge reachable.
129
153
  const env = { ...process.env, NO_PROXY: "127.0.0.1,localhost", ...authChildEnv(mode, key) };
154
+ delete env.SUPERD_HUB_SESSION;
155
+ if (this.hubSession !== undefined)
156
+ env.SUPERD_HUB_SESSION = this.hubSession;
130
157
  // No HOME override, ever: the settings we just maintained live at
131
158
  // `$HOME/.gemini`, ADC at `$HOME/.config/gcloud`, and the sign-in token
132
159
  // in the keyring — isolating HOME destroys all three. That was the
@@ -155,6 +182,8 @@ export class AgyCliClient {
155
182
  });
156
183
  child.on("exit", (code) => {
157
184
  this.child = undefined;
185
+ this.readyReceived = false;
186
+ this.buffer = "";
158
187
  // The CLI reports its refusals on stderr / as non-JSON stdout lines and
159
188
  // then exits non-zero. Those lines ARE the diagnosis ("--effort is not
160
189
  // supported for model …", "Eligibility check failed: …"), so the exit
@@ -206,6 +235,8 @@ export class AgyCliClient {
206
235
  continue;
207
236
  }
208
237
  if (parsed.event === "init") {
238
+ if (typeof parsed.conversation_id === "string")
239
+ this.conversationIdValue = parsed.conversation_id;
209
240
  const init = parsed.init;
210
241
  this.toolsSeen = Array.isArray(init?.tools) ? init.tools.length : 0;
211
242
  this.dispatch({ event: "ready", tools: this.toolsSeen, permissionMode: init?.permission_mode });
@@ -262,7 +293,6 @@ export class AgyCliClient {
262
293
  event: "tool_call", id, name,
263
294
  args: JSON.stringify(su.tool_info?.parameters ?? {}),
264
295
  });
265
- this.onToolEvent?.({ event: "tool_call", id, name, args: JSON.stringify(su.tool_info?.parameters ?? {}) });
266
296
  }
267
297
  else if (su.state === "DONE" || su.state === "ERROR") {
268
298
  const output = su.tool_info?.output ?? "";
@@ -271,10 +301,6 @@ export class AgyCliClient {
271
301
  event: "tool_result", id, name, is_error: isError,
272
302
  result: output || (isError ? "tool failed" : "[agy CLI reported no output summary]"),
273
303
  });
274
- this.onToolEvent?.({
275
- event: "tool_result", id, name, is_error: isError,
276
- result: output || (isError ? "tool failed" : "[agy CLI reported no output summary]"),
277
- });
278
304
  }
279
305
  }
280
306
  }
@@ -298,6 +324,7 @@ export class AgyCliClient {
298
324
  }
299
325
  /** One turn. First call spawns the CLI (lazy per §14-i) and keeps it resident. */
300
326
  async *turn(text) {
327
+ await this.prepare();
301
328
  this.spawnCli();
302
329
  await this.expectReady();
303
330
  this.send({ event: "user", message: { content: text } });
@@ -343,13 +370,34 @@ export class AgyCliClient {
343
370
  }
344
371
  }
345
372
  async ping() {
373
+ await this.prepare();
346
374
  this.spawnCli();
347
375
  await this.expectReady();
348
376
  }
377
+ /** Stop one CLI execution, preserving its conversation and Hub capability. */
378
+ async interrupt() {
379
+ const child = this.child;
380
+ if (child === undefined)
381
+ return;
382
+ this.readyReceived = false;
383
+ const exited = new Promise(resolve => child.once("exit", () => resolve()));
384
+ child.kill("SIGTERM");
385
+ const timer = setTimeout(() => child.kill("SIGKILL"), 2000);
386
+ timer.unref();
387
+ try {
388
+ await exited;
389
+ }
390
+ finally {
391
+ clearTimeout(timer);
392
+ }
393
+ }
349
394
  async close() {
350
- if (!this.child)
395
+ if (this.dead)
351
396
  return;
352
397
  this.dead = true;
398
+ await this.opts.hubBinding?.dispose();
399
+ if (!this.child)
400
+ return;
353
401
  const child = this.child;
354
402
  try {
355
403
  child.stdin.end();
@@ -18,6 +18,7 @@
18
18
  * `sessionPersistence` service (hermes create/resume transaction shape).
19
19
  */
20
20
  import { realpathSync, statSync } from "node:fs";
21
+ import { bindForeignHub } from "@pgmi-builds/superd-hub/foreign-hub";
21
22
  import { join } from "node:path";
22
23
  import { Service } from "@deepseek-ai/cordis";
23
24
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
@@ -149,6 +150,8 @@ export class AgyProvider extends Service {
149
150
  hasAuthEvidence() {
150
151
  if (this.resolveApiKey() !== undefined)
151
152
  return true;
153
+ if (hasSigninCredential(this.opts.realHome ?? process.env.HOME ?? ""))
154
+ return true;
152
155
  return hasAdcEvidence(process.env, process.env.HOME ?? "");
153
156
  }
154
157
  /** Default model id: adapter state → native TUI default (slug-normalized) → catalog head. */
@@ -260,7 +263,8 @@ export class AgyProvider extends Service {
260
263
  trace(`create id=${id} spawnCwd=${spawnCwd}`);
261
264
  // Full-lazy (§14-i): construct the client (spawns NOTHING); the bridge
262
265
  // child materializes on the first real prompt.
263
- const client = this.createLiveClient(spawnCwd, agentOptionsModel(options.agentOptions), options.agentOptions?.reasoningEffort);
266
+ const agentRef = {};
267
+ const client = this.createLiveClient(spawnCwd, agentOptionsModel(options.agentOptions), options.agentOptions?.reasoningEffort, bindForeignHub("agy", id, this.home, () => agentRef.current));
264
268
  const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
265
269
  ...(options.seed === undefined ? {} : { seed: options.seed }),
266
270
  ...(meta === undefined ? {} : { meta }),
@@ -273,7 +277,7 @@ export class AgyProvider extends Service {
273
277
  upsertSession(this.home, String(id), { conversationId: null, cwd: spawnCwd, createdAt: Date.now(), preset: null });
274
278
  handedOff = true;
275
279
  const agentOptions = await this.sessionAgentOptions(options.agentOptions);
276
- return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(spawnCwd), () => this.followConversationId(id, client));
280
+ return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(spawnCwd), () => this.followConversationId(id, client), agentRef);
277
281
  }
278
282
  catch (error) {
279
283
  trace(`create ${id} THREW: ${String(error)}`);
@@ -318,7 +322,8 @@ export class AgyProvider extends Service {
318
322
  }
319
323
  return verified;
320
324
  })();
321
- const client = this.createLiveClient(spawnCwd, agentOptionsModel(options.agentOptions), options.agentOptions?.reasoningEffort);
325
+ const agentRef = {};
326
+ const client = this.createLiveClient(spawnCwd, agentOptionsModel(options.agentOptions), options.agentOptions?.reasoningEffort, bindForeignHub("agy", id, this.home, () => agentRef.current), blank ? undefined : record.conversationId);
322
327
  let handle;
323
328
  let stored;
324
329
  let preparation;
@@ -340,7 +345,7 @@ export class AgyProvider extends Service {
340
345
  handle = undefined;
341
346
  handedOff = true;
342
347
  const agentOptions = await this.sessionAgentOptions(options.agentOptions);
343
- return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(spawnCwd), () => this.followConversationId(id, client));
348
+ return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(spawnCwd), () => this.followConversationId(id, client), agentRef);
344
349
  }
345
350
  catch (error) {
346
351
  trace(`resume ${id} THREW: ${String(error)}`);
@@ -363,8 +368,8 @@ export class AgyProvider extends Service {
363
368
  * environment for free while the SDK is an opt-in extra. Auth-form selection
364
369
  * still rides the agy-auth ladder.
365
370
  */
366
- createLiveClient(cwd, modelId, reasoningEffort) {
367
- return new AgyCliClient(this.cliOptions(cwd, modelId, reasoningEffort));
371
+ createLiveClient(cwd, modelId, reasoningEffort, hubBinding, conversationId) {
372
+ return new AgyCliClient({ ...this.cliOptions(cwd, modelId, reasoningEffort), conversationId, hubBinding, hubStagingDirectory: join(this.home, "hub-native-agy") });
368
373
  }
369
374
  /**
370
375
  * The auth form this machine should use: an explicitly selected, actually
@@ -457,7 +462,7 @@ async function appendUnstoredSuffix(stored, session) {
457
462
  stored.storedCount += suffix.length;
458
463
  }
459
464
  /** Shared create/resume transaction (module-level: Cordis tracing proxy). */
460
- async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo, onTurnDone) {
465
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo, onTurnDone, agentRef) {
461
466
  const runtime = { ...runtimeInfo, onTurnDone };
462
467
  let detachSession;
463
468
  let detachAgent;
@@ -465,6 +470,7 @@ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, sessi
465
470
  let idleExit;
466
471
  try {
467
472
  agent = new AgyAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtime);
473
+ agentRef.current = agent;
468
474
  // Pin THIS session to the agy route (static resolution — synchronous).
469
475
  try {
470
476
  const selection = { provider: agentOptions.provider ?? "agy", model: agentOptions.model ?? "" };
@@ -30,7 +30,8 @@
30
30
  "@deepseek-ai/dsh-scope": ">=0.1.5-0 <0.3.0-0",
31
31
  "@deepseek-ai/dsh-session": ">=0.1.5-0 <0.3.0-0",
32
32
  "@deepseek-ai/dsh-session-persistence": ">=0.1.5-0 <0.3.0-0",
33
- "@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0"
33
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0",
34
+ "@pgmi-builds/superd-hub": "*"
34
35
  },
35
36
  "peerDependenciesMeta": {
36
37
  "@deepseek-ai/cordis": {
@@ -51,6 +52,9 @@
51
52
  "@deepseek-ai/dsh-session-persistence": {
52
53
  "optional": true
53
54
  },
55
+ "@pgmi-builds/superd-hub": {
56
+ "optional": true
57
+ },
54
58
  "@deepseek-ai/dsh-typert-protocol": {
55
59
  "optional": true
56
60
  }
@@ -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",