super-dsh 0.1.11 → 0.1.12-a

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 (125) 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/client/index.js +434 -307
  32. package/cordis.patch.yml +10 -9
  33. package/package.json +8 -5
  34. package/superd-hub/assets/icons/README.md +20 -0
  35. package/superd-hub/assets/icons/amp.png +0 -0
  36. package/superd-hub/assets/icons/ante.png +0 -0
  37. package/superd-hub/assets/icons/antigravity.png +0 -0
  38. package/superd-hub/assets/icons/aug.png +0 -0
  39. package/superd-hub/assets/icons/autohand.png +0 -0
  40. package/superd-hub/assets/icons/claude.svg +1 -0
  41. package/superd-hub/assets/icons/cline.png +0 -0
  42. package/superd-hub/assets/icons/codebuddy.png +0 -0
  43. package/superd-hub/assets/icons/codebuff.png +0 -0
  44. package/superd-hub/assets/icons/codex.svg +1 -0
  45. package/superd-hub/assets/icons/command-code.png +0 -0
  46. package/superd-hub/assets/icons/continue.png +0 -0
  47. package/superd-hub/assets/icons/copilot.png +0 -0
  48. package/superd-hub/assets/icons/crush.png +0 -0
  49. package/superd-hub/assets/icons/cursor.png +0 -0
  50. package/superd-hub/assets/icons/devin.png +0 -0
  51. package/superd-hub/assets/icons/droid.png +0 -0
  52. package/superd-hub/assets/icons/dsh-fish.svg +3 -0
  53. package/superd-hub/assets/icons/dsh.png +0 -0
  54. package/superd-hub/assets/icons/freebuff.png +0 -0
  55. package/superd-hub/assets/icons/gemini.png +0 -0
  56. package/superd-hub/assets/icons/goose.png +0 -0
  57. package/superd-hub/assets/icons/grok.png +0 -0
  58. package/superd-hub/assets/icons/hermes.png +0 -0
  59. package/superd-hub/assets/icons/jcode.png +0 -0
  60. package/superd-hub/assets/icons/kilo.png +0 -0
  61. package/superd-hub/assets/icons/kimi.png +0 -0
  62. package/superd-hub/assets/icons/kiro.png +0 -0
  63. package/superd-hub/assets/icons/manifest.json +20 -0
  64. package/superd-hub/assets/icons/mimo-code.png +0 -0
  65. package/superd-hub/assets/icons/mistral-vibe.png +0 -0
  66. package/superd-hub/assets/icons/muse.png +0 -0
  67. package/superd-hub/assets/icons/omp.svg +1 -0
  68. package/superd-hub/assets/icons/openclaude.png +0 -0
  69. package/superd-hub/assets/icons/openclaw.png +0 -0
  70. package/superd-hub/assets/icons/opencode.png +0 -0
  71. package/superd-hub/assets/icons/pi.svg +1 -0
  72. package/superd-hub/assets/icons/prime-agent.png +0 -0
  73. package/superd-hub/assets/icons/qoder-cn.png +0 -0
  74. package/superd-hub/assets/icons/qoder.png +0 -0
  75. package/superd-hub/assets/icons/qwen-code.png +0 -0
  76. package/superd-hub/assets/icons/rovo.png +0 -0
  77. package/superd-hub/assets/icons/trae.png +0 -0
  78. package/superd-hub/assets/icons/zcode.png +0 -0
  79. package/superd-hub/dist/a2a-row.js +21 -4
  80. package/superd-hub/dist/agent-roster.d.ts +8 -3
  81. package/superd-hub/dist/agent-roster.js +19 -7
  82. package/superd-hub/dist/client/icon-assets.gen.d.ts +16 -0
  83. package/superd-hub/dist/client/icon-assets.gen.js +54 -0
  84. package/superd-hub/dist/client/mesh-api.d.ts +28 -17
  85. package/superd-hub/dist/client/mesh-api.js +44 -31
  86. package/superd-hub/dist/client/mesh-groups.d.ts +13 -11
  87. package/superd-hub/dist/client/mesh-groups.js +30 -10
  88. package/superd-hub/dist/client/roster.d.ts +7 -16
  89. package/superd-hub/dist/client/roster.js +5 -3
  90. package/superd-hub/dist/connection-trust.d.ts +8 -0
  91. package/superd-hub/dist/connection-trust.js +16 -0
  92. package/superd-hub/dist/foreign-agy.d.ts +3 -0
  93. package/superd-hub/dist/foreign-agy.js +110 -0
  94. package/superd-hub/dist/foreign-claude.d.ts +4 -0
  95. package/superd-hub/dist/foreign-claude.js +160 -0
  96. package/superd-hub/dist/foreign-extension.d.ts +10 -0
  97. package/superd-hub/dist/foreign-extension.js +27 -0
  98. package/superd-hub/dist/foreign-hub.d.ts +27 -0
  99. package/superd-hub/dist/foreign-hub.js +243 -0
  100. package/superd-hub/dist/foreign-native.d.ts +4 -0
  101. package/superd-hub/dist/foreign-native.js +115 -0
  102. package/superd-hub/dist/fusion-transport.d.ts +5 -2
  103. package/superd-hub/dist/fusion-transport.js +15 -3
  104. package/superd-hub/dist/machines.d.ts +6 -3
  105. package/superd-hub/dist/machines.js +15 -3
  106. package/superd-hub/dist/mesh-federate.d.ts +18 -8
  107. package/superd-hub/dist/mesh-federate.js +14 -9
  108. package/superd-hub/dist/mesh-link.d.ts +37 -3
  109. package/superd-hub/dist/mesh-link.js +49 -4
  110. package/superd-hub/dist/mesh-rpc.d.ts +48 -16
  111. package/superd-hub/dist/mesh-rpc.js +139 -14
  112. package/superd-hub/dist/roster.d.ts +20 -0
  113. package/superd-hub/dist/roster.js +24 -0
  114. package/superd-hub/dist/switch-core.d.ts +3 -2
  115. package/superd-hub/dist/switch-core.js +1 -1
  116. package/superd-hub/dist/web-trust.d.ts +14 -42
  117. package/superd-hub/dist/web-trust.js +18 -43
  118. package/superd-hub/lib/client/index.js +434 -307
  119. package/superd-hub/package.json +17 -3
  120. package/agent-codex/README.md +0 -191
  121. package/agent-codex-sdk/README.md +0 -79
  122. package/agent-hermes/README.md +0 -145
  123. package/agent-omp/README.md +0 -94
  124. package/agent-omp-sdk/README.md +0 -103
  125. package/agent-pi/README.md +0 -67
package/README.md CHANGED
@@ -14,7 +14,7 @@ the foreign store.
14
14
  ## Install
15
15
 
16
16
  ```bash
17
- dsh plugin --profile <name> add super-dsh@0.1.11
17
+ dsh plugin --profile <name> add super-dsh@0.1.12
18
18
  ```
19
19
 
20
20
  That single package composes the whole line (`@pgmi-builds/superd-hub` plus the
@@ -22,17 +22,63 @@ six primary `@pgmi-builds/agent-*` packages and three OPTIONAL SDK halves).
22
22
  Restart the profile and the
23
23
  home under `<DSH_HOME>/agents/<label>/` on first boot.
24
24
 
25
+ ## Remote web access
26
+
27
+ The shipped bundle enables `superd-web-trust` with `trustAllHosts: true`.
28
+ Authenticated requests may use any serving Host or Origin, and the browser's
29
+ native trusted-host / loopback checks see the page as Host-owned. No `--host`,
30
+ `--trusted-host`, or trusted-host environment variables are needed for trust.
31
+ Native token-to-cookie login, cookie expiry, and authority binding remain in effect.
32
+
33
+ This does not change the listening address: use the native loopback listener
34
+ behind your reverse proxy or LAN relay. Remove older manual host/trusted-host
35
+ patches and any profile override disabling `superd-web-trust`. To restore the
36
+ native address fence, explicitly set this row's `trustAllHosts: false`.
37
+
25
38
  ## Runtimes
26
39
 
27
40
  | Label | Runtime | Integration |
28
41
  |---|---|---|
29
42
  | OMP | OMP (`~/.omp`) | host `omp` binary over RPC (`omp --mode rpc`, NDJSON v1), lazy per session |
30
- | Codex | Codex app-server (`~/.codex`) | desktop core first, CLI and bundled core fallback |
43
+ | Codex | Codex app-server (`~/.codex`) | CLI first, desktop core and lazy world-home fetch fallback |
31
44
  | Claude | Claude Code (`~/.claude`) | standalone adapter, joined by the hub |
32
45
  | Pi | pi coding agent (`~/.pi`) | in-process SDK |
33
46
  | Hermes | Hermes (`~/.hermes`) | gateway child |
34
47
  | Antigravity | agy SDK | Python bridge (`bridge/agy_bridge.py`) |
35
48
 
49
+ ### Foreign-agent Hub facility
50
+
51
+ Super D sessions in Pi, OMP, and Codex can use the same `superd-hub` facility
52
+ for agent roster/status, messaging, and shell execution. Tool descriptions,
53
+ argument validation, receipts, and reply guidance come from the existing Hub.
54
+ Replies wake the initiating session automatically; do not poll for them.
55
+
56
+ Pi and OMP share one native extension. It is loaded incrementally on the first
57
+ prompt, preserving normal extension discovery and native tools. Its per-session
58
+ wrapper lives under the Super D world home (`hub-extensions`, mode 0600), with a
59
+ session capability for a loopback-only HTTP listener. No global extension is
60
+ installed in `~/.pi` or `~/.omp`. Some OMP versions present registered extension
61
+ tools as devices: read `xd://superd-hub` for instructions and write action JSON
62
+ to that device.
63
+
64
+ Codex uses the official experimental app-server `dynamicTools` interface on
65
+ new threads. Callbacks bind to that thread and the Super D session; existing
66
+ native tools/plugins remain available. No global plugin is installed in
67
+ `~/.codex`. Resuming a thread created through this integration restores its
68
+ saved tool definition. Older threads without that definition need a new
69
+ Super D conversation to gain the facility. A native Codex client resuming one
70
+ of these threads does not supply Super D's callback host.
71
+
72
+ Pi/OMP extension closures cannot transparently renew an already-loaded listener
73
+ capability when the Hub row is unmounted/remounted. Restart Super D and resume
74
+ the conversation after that operation; the tool reports this recovery path
75
+ when its connection becomes unavailable. Whole-app restart/resume is tested.
76
+
77
+ Draft creation and passive history viewing create no extension file, listener,
78
+ or agent process. Closing a session invalidates its capability. Interrupted
79
+ tool operations retain conservative upstream recovery guidance about unknown
80
+ outcomes rather than encouraging blind retries.
81
+
36
82
  ### Codex in 0.1.11
37
83
 
38
84
  Codex discovers the local OpenAI OAuth login as a read-only provider in the
@@ -44,10 +90,12 @@ The adapter prefers the installed desktop core, then the CLI, then the bundled
44
90
  0.160.0 core (`CODEX_BIN` overrides discovery). Desktop host browser and computer
45
91
  use services are separate and are not attached by this adapter.
46
92
 
47
- **Known limitation:** returning a session with third-party reasoning history to
48
- OpenAI OAuth can be rejected by the official endpoint. Use a separate official
49
- session for now. Compaction before switching was proven in an isolated probe;
50
- automatic compaction is not included in this release.
93
+ **Provider compatibility:** each provider defines its own message, reasoning,
94
+ and encrypted-state formats. Selecting another provider does not guarantee
95
+ that it can consume the existing session history. Successful continuation
96
+ across endpoints is an observed convenience, not an adapter guarantee.
97
+ The adapter surfaces provider errors; if the selected provider rejects the
98
+ history, start a new session with that provider.
51
99
 
52
100
  ### Opt-in SDK halves
53
101
 
@@ -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): {
@@ -100,6 +101,8 @@ export declare class AgyAgent implements Agent {
100
101
  private deliver;
101
102
  private deliverPrompt;
102
103
  private sessionIdentityCommitted;
104
+ private seenToolCalls;
105
+ private stepOpen;
103
106
  /** Stamp the single-preset selection once per session (projection seed). */
104
107
  private bootstrapSessionIdentity;
105
108
  private startTurn;
@@ -119,6 +122,7 @@ export declare class AgyAgent implements Agent {
119
122
  /** Flush ONE parked follow-up as a fresh prompt (never submit while busy). */
120
123
  private flushFollowUps;
121
124
  private pendingThinking;
125
+ private stopInFlight?;
122
126
  /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
123
127
  private unpairedCalls;
124
128
  private armTurnDeadline;
@@ -44,7 +44,7 @@ const trace = (...parts) => {
44
44
  /** Classify a bridge turn failure into the stable {message, code} UI chip. */
45
45
  export function classifyTurnError(error) {
46
46
  const text = error instanceof Error ? error.message : String(error);
47
- if (/429|prepayment|quota|rate limit/i.test(text))
47
+ if (/429|prepayment|quota|rate limit|billing/i.test(text))
48
48
  return { message: text, code: "QUOTA" };
49
49
  if (/api key|authentication|unauthenticated|API_KEY/i.test(text))
50
50
  return { message: text, code: "AUTH" };
@@ -125,9 +125,15 @@ export class AgyAgent {
125
125
  if (!this.turnOpen)
126
126
  return;
127
127
  if (ev.event === "tool_call") {
128
- if (this.step > 0)
129
- this.closeStep();
130
- this.commitStepStart();
128
+ if (this.seenToolCalls.has(ev.id))
129
+ return;
130
+ this.seenToolCalls.add(ev.id);
131
+ // Concurrent tools share their originating step until every call resolves.
132
+ if (this.unpairedCalls.length === 0) {
133
+ if (this.step > 0)
134
+ this.closeStep();
135
+ this.commitStepStart();
136
+ }
131
137
  const callId = ToolCallId(ev.id || `agy-${this.dashTurn}-${this.step}`);
132
138
  const argumentsJson = ev.args || "{}";
133
139
  // Advertisement-before-execution (validator invariant, 2026-10-04 dev3
@@ -156,6 +162,8 @@ export class AgyAgent {
156
162
  else {
157
163
  // ToolResult chunks: the SDK yields none today; if a future version
158
164
  // does, pair them off immediately so no placeholder is written.
165
+ if (!this.unpairedCalls.some(c => c.callId === ev.id))
166
+ return;
159
167
  this.unpairedCalls = this.unpairedCalls.filter((c) => c.callId !== (ev.id || ""));
160
168
  this.session.append("tool/result", {
161
169
  turn: this.dashTurn, step: this.step,
@@ -254,6 +262,8 @@ export class AgyAgent {
254
262
  void this.startTurn(message);
255
263
  }
256
264
  sessionIdentityCommitted = false;
265
+ seenToolCalls = new Set();
266
+ stepOpen = false;
257
267
  /** Stamp the single-preset selection once per session (projection seed). */
258
268
  bootstrapSessionIdentity() {
259
269
  if (this.sessionIdentityCommitted)
@@ -264,6 +274,7 @@ export class AgyAgent {
264
274
  }
265
275
  }
266
276
  async startTurn(message) {
277
+ this.seenToolCalls.clear();
267
278
  this.bootstrapSessionIdentity();
268
279
  this.session.append("turn/start", { turn: this.dashTurn });
269
280
  this.session.append("user/message", message, { surfaceOp: "append" });
@@ -277,7 +288,13 @@ export class AgyAgent {
277
288
  trace("onboarding: key stored");
278
289
  }
279
290
  else {
280
- this.appendSystemNotice(ONBOARDING_PROMPT);
291
+ this.appendAssistantMessage(ONBOARDING_PROMPT);
292
+ this.closeStep();
293
+ this.closeTurn({ kind: "error", error: { code: "AUTH", message: ONBOARDING_PROMPT } });
294
+ this.markIdle();
295
+ this.endActivity();
296
+ this.flushFollowUps();
297
+ return;
281
298
  }
282
299
  this.closeTurn({ kind: "completed" });
283
300
  this.markIdle();
@@ -293,6 +310,8 @@ export class AgyAgent {
293
310
  this.ensureStreamBridge().push({ type: "text-delta", index: full.length, text: chunk });
294
311
  full += chunk;
295
312
  }
313
+ await this.stopInFlight;
314
+ this.stopInFlight = undefined;
296
315
  this.clearTurnDeadline();
297
316
  // SDK 0.1.17 streams ToolCall but NEVER ToolResult (verified live and
298
317
  // in SDK source: receive_chunks yields Thought/Text/ToolCall only; the
@@ -327,6 +346,8 @@ export class AgyAgent {
327
346
  this.flushFollowUps();
328
347
  }
329
348
  catch (error) {
349
+ await this.stopInFlight;
350
+ this.stopInFlight = undefined;
330
351
  this.clearTurnDeadline();
331
352
  this.abandonStreamBridge();
332
353
  const failure = classifyTurnError(error);
@@ -345,8 +366,10 @@ export class AgyAgent {
345
366
  if (this.turnOpen && this.step > 0)
346
367
  this.closeStep();
347
368
  this.closeTurn({ kind: "error", error: failure });
369
+ this.cancelCause = null;
348
370
  this.markIdle();
349
371
  this.endActivity();
372
+ this.flushFollowUps();
350
373
  }
351
374
  }
352
375
  appendSystemNotice(text) {
@@ -404,18 +427,21 @@ export class AgyAgent {
404
427
  closeTurn(reason) {
405
428
  if (!this.turnOpen)
406
429
  return;
430
+ this.closeStep();
407
431
  this.turnOpen = false;
408
432
  this.clearTurnDeadline();
409
433
  this.session.append("turn/end", { turn: this.dashTurn, reason });
410
434
  }
411
435
  closeStep() {
412
- if (!this.turnOpen || this.step <= 0)
436
+ if (!this.turnOpen || !this.stepOpen)
413
437
  return;
438
+ this.stepOpen = false;
414
439
  this.session.append("step/end", { turn: this.dashTurn, step: this.step });
415
440
  }
416
441
  commitStepStart() {
417
- if (!this.turnOpen)
442
+ if (!this.turnOpen || this.stepOpen)
418
443
  return;
444
+ this.stepOpen = true;
419
445
  this.step += 1;
420
446
  this.session.append("step/start", { turn: this.dashTurn, step: this.step });
421
447
  }
@@ -452,6 +478,7 @@ export class AgyAgent {
452
478
  this.deliverPrompt(entry.message);
453
479
  }
454
480
  pendingThinking = [];
481
+ stopInFlight;
455
482
  /** Calls from this turn still lacking a result (SDK 0.1.17 exposes none). */
456
483
  unpairedCalls = [];
457
484
  armTurnDeadline() {
@@ -462,6 +489,13 @@ export class AgyAgent {
462
489
  if (!this.turnOpen)
463
490
  return;
464
491
  trace(`turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms — failing open turn`);
492
+ if (this.client.interrupt !== undefined) {
493
+ // Keep ownership until the old CLI stops. Its catch pairs unresolved
494
+ // calls and only then starts the next queued message.
495
+ this.cancelCause = { kind: "hook", reason: `agy turn deadline exceeded after ${AGY_TURN_DEADLINE_MS}ms` };
496
+ this.stopInFlight = this.client.interrupt().catch(error => trace(`deadline interrupt failed: ${String(error)}`));
497
+ return;
498
+ }
465
499
  this.abandonStreamBridge();
466
500
  if (this.step > 0)
467
501
  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,12 @@ export declare class AgyCliClient {
110
114
  private dead;
111
115
  private conversationIdValue;
112
116
  private toolsSeen;
117
+ private toolCalls;
118
+ private toolResults;
119
+ private stepFailure?;
120
+ private hubPreparation?;
121
+ private hubSession?;
122
+ private prepare;
113
123
  onThinkingDelta?: (text: string) => void;
114
124
  onToolEvent?: (ev: {
115
125
  event: "tool_call";
@@ -163,5 +173,7 @@ export declare class AgyCliClient {
163
173
  private readyReceived;
164
174
  private expectReady;
165
175
  ping(): Promise<void>;
176
+ /** Stop one CLI execution, preserving its conversation and Hub capability. */
177
+ interrupt(): Promise<void>;
166
178
  close(): Promise<void>;
167
179
  }
@@ -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,27 @@ export class AgyCliClient {
63
66
  dead = false;
64
67
  conversationIdValue = "";
65
68
  toolsSeen = 0;
69
+ toolCalls = new Set();
70
+ toolResults = new Set();
71
+ stepFailure;
72
+ hubPreparation;
73
+ hubSession;
74
+ async prepare() {
75
+ if (this.dead)
76
+ throw new Error("agy client is closed");
77
+ this.hubPreparation ??= (async () => {
78
+ if (this.opts.hubBinding === undefined)
79
+ return;
80
+ this.hubSession = await (this.opts.prepareHub?.() ?? prepareAgyNativePlugin(this.opts.hubBinding, join(this.realHome(), ".gemini", "antigravity-cli"), this.opts.hubStagingDirectory, async (directory) => {
81
+ const exec = promisify(execFile);
82
+ await exec(this.opts.agyBin ?? "agy", ["plugin", "install", directory], { timeout: 60_000 });
83
+ await exec(this.opts.agyBin ?? "agy", ["plugin", "enable", "superd-hub"], { timeout: 60_000 });
84
+ }));
85
+ })();
86
+ await this.hubPreparation;
87
+ if (this.dead)
88
+ throw new Error("agy client is closed");
89
+ }
66
90
  onThinkingDelta;
67
91
  onToolEvent;
68
92
  setApprovalHandler(_handler) {
@@ -90,6 +114,8 @@ export class AgyCliClient {
90
114
  spawnCli() {
91
115
  if (this.child || this.dead)
92
116
  return;
117
+ this.pending.length = 0;
118
+ this.buffer = "";
93
119
  const mode = this.opts.authMode ?? "apiKey";
94
120
  // Only key mode patches settings (`modelProvider: "gemini"`); ADC and OAuth
95
121
  // need the DEFAULT backend, where that key must be ABSENT — the binary
@@ -107,8 +133,9 @@ export class AgyCliClient {
107
133
  "--input-format=stream-json", "--output-format=stream-json",
108
134
  "--disable-slash-commands",
109
135
  ];
110
- if (this.opts.conversationId)
111
- args.push(`--conversation=${this.opts.conversationId}`);
136
+ const conversationId = this.conversationIdValue || this.opts.conversationId;
137
+ if (conversationId)
138
+ args.push(`--conversation=${conversationId}`);
112
139
  if (this.opts.model)
113
140
  args.push(`--model=${this.opts.model}`);
114
141
  if (this.opts.effort)
@@ -127,6 +154,9 @@ export class AgyCliClient {
127
154
  // ADC, the key for key mode, and the auto-update pin on EVERY mode.
128
155
  // NO_PROXY keeps the dev3 loopback bridge reachable.
129
156
  const env = { ...process.env, NO_PROXY: "127.0.0.1,localhost", ...authChildEnv(mode, key) };
157
+ delete env.SUPERD_HUB_SESSION;
158
+ if (this.hubSession !== undefined)
159
+ env.SUPERD_HUB_SESSION = this.hubSession;
130
160
  // No HOME override, ever: the settings we just maintained live at
131
161
  // `$HOME/.gemini`, ADC at `$HOME/.config/gcloud`, and the sign-in token
132
162
  // in the keyring — isolating HOME destroys all three. That was the
@@ -155,6 +185,8 @@ export class AgyCliClient {
155
185
  });
156
186
  child.on("exit", (code) => {
157
187
  this.child = undefined;
188
+ this.readyReceived = false;
189
+ this.buffer = "";
158
190
  // The CLI reports its refusals on stderr / as non-JSON stdout lines and
159
191
  // then exits non-zero. Those lines ARE the diagnosis ("--effort is not
160
192
  // supported for model …", "Eligibility check failed: …"), so the exit
@@ -206,6 +238,8 @@ export class AgyCliClient {
206
238
  continue;
207
239
  }
208
240
  if (parsed.event === "init") {
241
+ if (typeof parsed.conversation_id === "string")
242
+ this.conversationIdValue = parsed.conversation_id;
209
243
  const init = parsed.init;
210
244
  this.toolsSeen = Array.isArray(init?.tools) ? init.tools.length : 0;
211
245
  this.dispatch({ event: "ready", tools: this.toolsSeen, permissionMode: init?.permission_mode });
@@ -217,8 +251,8 @@ export class AgyCliClient {
217
251
  const r = parsed.result;
218
252
  if (r?.conversation_id)
219
253
  this.conversationIdValue = r.conversation_id;
220
- if (r?.status !== "SUCCESS") {
221
- this.dispatch({ event: "error", error: this.withDiagnostics(r?.error ?? `agy result status=${r?.status}`), recoverable: false });
254
+ if (r?.status !== "SUCCESS" || this.stepFailure !== undefined) {
255
+ this.dispatch({ event: "error", error: this.withDiagnostics(r?.error ?? this.stepFailure ?? `agy result status=${r?.status}`), recoverable: false });
222
256
  }
223
257
  else {
224
258
  this.dispatch({ event: "done", conversation_id: this.conversationIdValue, turn_ms: 0 });
@@ -228,6 +262,12 @@ export class AgyCliClient {
228
262
  }
229
263
  consumeStep(su) {
230
264
  const st = su.step_type;
265
+ if (su.conversation_id)
266
+ this.conversationIdValue = su.conversation_id;
267
+ if (st === "error_message") {
268
+ this.stepFailure = su.text_delta || "Antigravity reported an error without details";
269
+ return;
270
+ }
231
271
  if (st === "system_message") {
232
272
  this.onSystemMessage?.(su.text_delta ?? "");
233
273
  return;
@@ -255,26 +295,28 @@ export class AgyCliClient {
255
295
  return;
256
296
  }
257
297
  if (st === "tool") {
258
- const name = su.tool_name ?? su.tool_info?.name ?? "unknown";
298
+ const wireName = String(su.tool_name ?? "");
299
+ const name = su.tool_info?.name || (/^\d+$/.test(wireName) ? "unknown" : wireName) || "unknown";
259
300
  const id = `${this.conversationIdValue}:${su.step_index ?? 0}`;
260
- if (su.state === "ACTIVE") {
301
+ if (su.state === "ACTIVE" && !this.toolCalls.has(id)) {
302
+ this.toolCalls.add(id);
261
303
  this.dispatch({
262
304
  event: "tool_call", id, name,
263
305
  args: JSON.stringify(su.tool_info?.parameters ?? {}),
264
306
  });
265
- this.onToolEvent?.({ event: "tool_call", id, name, args: JSON.stringify(su.tool_info?.parameters ?? {}) });
266
307
  }
267
- else if (su.state === "DONE" || su.state === "ERROR") {
308
+ else if ((su.state === "DONE" || su.state === "ERROR") && !this.toolResults.has(id)) {
309
+ if (!this.toolCalls.has(id)) {
310
+ this.toolCalls.add(id);
311
+ this.dispatch({ event: "tool_call", id, name, args: JSON.stringify(su.tool_info?.parameters ?? {}) });
312
+ }
313
+ this.toolResults.add(id);
268
314
  const output = su.tool_info?.output ?? "";
269
315
  const isError = su.state === "ERROR";
270
316
  this.dispatch({
271
317
  event: "tool_result", id, name, is_error: isError,
272
318
  result: output || (isError ? "tool failed" : "[agy CLI reported no output summary]"),
273
319
  });
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
320
  }
279
321
  }
280
322
  }
@@ -298,8 +340,13 @@ export class AgyCliClient {
298
340
  }
299
341
  /** One turn. First call spawns the CLI (lazy per §14-i) and keeps it resident. */
300
342
  async *turn(text) {
343
+ await this.prepare();
301
344
  this.spawnCli();
302
345
  await this.expectReady();
346
+ this.toolCalls.clear();
347
+ this.toolResults.clear();
348
+ this.stepFailure = undefined;
349
+ this.diagnostics = [];
303
350
  this.send({ event: "user", message: { content: text } });
304
351
  let full = "";
305
352
  for (;;) {
@@ -343,13 +390,34 @@ export class AgyCliClient {
343
390
  }
344
391
  }
345
392
  async ping() {
393
+ await this.prepare();
346
394
  this.spawnCli();
347
395
  await this.expectReady();
348
396
  }
397
+ /** Stop one CLI execution, preserving its conversation and Hub capability. */
398
+ async interrupt() {
399
+ const child = this.child;
400
+ if (child === undefined)
401
+ return;
402
+ this.readyReceived = false;
403
+ const exited = new Promise(resolve => child.once("exit", () => resolve()));
404
+ child.kill("SIGTERM");
405
+ const timer = setTimeout(() => child.kill("SIGKILL"), 2000);
406
+ timer.unref();
407
+ try {
408
+ await exited;
409
+ }
410
+ finally {
411
+ clearTimeout(timer);
412
+ }
413
+ }
349
414
  async close() {
350
- if (!this.child)
415
+ if (this.dead)
351
416
  return;
352
417
  this.dead = true;
418
+ await this.opts.hubBinding?.dispose();
419
+ if (!this.child)
420
+ return;
353
421
  const child = this.child;
354
422
  try {
355
423
  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
  }