super-dsh 0.1.6 → 0.1.10

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 (248) hide show
  1. package/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-codex/README.md +141 -36
  41. package/agent-codex/cordis.patch.yml +14 -10
  42. package/agent-codex/dist/adapter.js +64 -28
  43. package/agent-codex/dist/agent.js +255 -81
  44. package/agent-codex/dist/app-server-approvals.js +165 -0
  45. package/agent-codex/dist/app-server-client.js +723 -0
  46. package/agent-codex/dist/app-server-events.js +432 -0
  47. package/agent-codex/dist/app-server-protocol.js +95 -0
  48. package/agent-codex/dist/app-server-wire.js +299 -0
  49. package/agent-codex/dist/codex-launch.js +78 -0
  50. package/agent-codex/dist/codex-native-config.js +392 -0
  51. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  52. package/agent-codex/dist/codex-providers.js +376 -0
  53. package/agent-codex/dist/codex-route.js +86 -0
  54. package/agent-codex/dist/index.js +256 -115
  55. package/agent-codex/dist/permission.js +75 -1
  56. package/agent-codex/package.json +1 -2
  57. package/agent-codex-sdk/README.md +79 -0
  58. package/agent-codex-sdk/cordis.patch.yml +81 -0
  59. package/agent-codex-sdk/dist/adapter.js +91 -0
  60. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  61. package/agent-codex-sdk/dist/agent.js +1085 -0
  62. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  63. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  65. package/agent-codex-sdk/dist/codex-route.js +86 -0
  66. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  67. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  68. package/agent-codex-sdk/dist/codex-store.js +180 -0
  69. package/agent-codex-sdk/dist/inbox.js +53 -0
  70. package/agent-codex-sdk/dist/index.js +739 -0
  71. package/agent-codex-sdk/dist/permission.js +71 -0
  72. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  73. package/agent-codex-sdk/dist/session-map.js +167 -0
  74. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  75. package/agent-codex-sdk/package.json +79 -0
  76. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  77. package/agent-omp/README.md +72 -55
  78. package/agent-omp/dist/agent.js +36 -11
  79. package/agent-omp/dist/index.js +92 -52
  80. package/agent-omp/dist/lazy-rpc.js +11 -4
  81. package/agent-omp/dist/omp-cli.js +114 -97
  82. package/agent-omp/dist/omp-store.js +12 -4
  83. package/agent-omp/dist/permission.js +70 -5
  84. package/agent-omp/dist/rpc-client.js +409 -0
  85. package/agent-omp/dist/world-plugin.js +36 -3
  86. package/agent-omp/package.json +2 -6
  87. package/agent-omp-sdk/README.md +103 -0
  88. package/agent-omp-sdk/cordis.patch.yml +108 -0
  89. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  90. package/agent-omp-sdk/dist/adapter.js +36 -0
  91. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  92. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  93. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  94. package/agent-omp-sdk/dist/agent.js +1178 -0
  95. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  96. package/agent-omp-sdk/dist/discovery.js +116 -0
  97. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  98. package/agent-omp-sdk/dist/inbox.js +53 -0
  99. package/agent-omp-sdk/dist/index.d.ts +60 -0
  100. package/agent-omp-sdk/dist/index.js +632 -0
  101. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  102. package/agent-omp-sdk/dist/knobs.js +50 -0
  103. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  104. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  105. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  106. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  107. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  108. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  109. package/agent-omp-sdk/dist/models.d.ts +39 -0
  110. package/agent-omp-sdk/dist/models.js +299 -0
  111. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  112. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  113. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  114. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  115. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  116. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  117. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  118. package/agent-omp-sdk/dist/omp-store.js +526 -0
  119. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  120. package/agent-omp-sdk/dist/pairing.js +45 -0
  121. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  122. package/agent-omp-sdk/dist/permission.js +161 -0
  123. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  124. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  125. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  126. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  127. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  128. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  129. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  130. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  131. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  132. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  133. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  134. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  135. package/agent-omp-sdk/dist/store/db.js +210 -0
  136. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  137. package/agent-omp-sdk/dist/store/index.js +59 -0
  138. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  139. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  140. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  141. package/agent-omp-sdk/dist/store/schema.js +40 -0
  142. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  143. package/agent-omp-sdk/dist/transport.js +35 -0
  144. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  145. package/agent-omp-sdk/dist/world-home.js +23 -0
  146. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  147. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  148. package/agent-omp-sdk/package.json +87 -0
  149. package/client/index.js +1794 -287
  150. package/cordis.patch.yml +80 -0
  151. package/package.json +23 -8
  152. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  153. package/superd-hub/dist/a2a-delivery.js +240 -0
  154. package/superd-hub/dist/a2a-host.d.ts +76 -0
  155. package/superd-hub/dist/a2a-host.js +145 -0
  156. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  157. package/superd-hub/dist/a2a-ingest.js +636 -0
  158. package/superd-hub/dist/a2a-log.d.ts +17 -0
  159. package/superd-hub/dist/a2a-log.js +42 -0
  160. package/superd-hub/dist/a2a-row.d.ts +65 -0
  161. package/superd-hub/dist/a2a-row.js +162 -0
  162. package/superd-hub/dist/a2a-service.d.ts +357 -0
  163. package/superd-hub/dist/a2a-service.js +980 -0
  164. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  165. package/superd-hub/dist/a2a-wire.js +305 -0
  166. package/superd-hub/dist/agent-ids.d.ts +10 -0
  167. package/superd-hub/dist/agent-ids.js +68 -0
  168. package/superd-hub/dist/agent-roster.d.ts +29 -18
  169. package/superd-hub/dist/agent-roster.js +54 -15
  170. package/superd-hub/dist/agent-status.d.ts +119 -0
  171. package/superd-hub/dist/agent-status.js +386 -0
  172. package/superd-hub/dist/client/agent-settings.d.ts +156 -0
  173. package/superd-hub/dist/client/agent-settings.js +229 -0
  174. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  175. package/superd-hub/dist/client/mesh-api.js +219 -0
  176. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  177. package/superd-hub/dist/client/mesh-groups.js +66 -0
  178. package/superd-hub/dist/client/roster.d.ts +48 -0
  179. package/superd-hub/dist/client/roster.js +102 -0
  180. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  181. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  182. package/superd-hub/dist/client/switch.d.ts +165 -0
  183. package/superd-hub/dist/client/switch.js +295 -0
  184. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  185. package/superd-hub/dist/exec-pty-channel.js +162 -0
  186. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  187. package/superd-hub/dist/exec-pty-framing.js +81 -0
  188. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  189. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  190. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  191. package/superd-hub/dist/exec-pty-session.js +234 -0
  192. package/superd-hub/dist/exec.d.ts +56 -0
  193. package/superd-hub/dist/exec.js +225 -0
  194. package/superd-hub/dist/gateway.d.ts +39 -2
  195. package/superd-hub/dist/gateway.js +62 -3
  196. package/superd-hub/dist/hub-tool.d.ts +174 -0
  197. package/superd-hub/dist/hub-tool.js +464 -0
  198. package/superd-hub/dist/index.d.ts +15 -1
  199. package/superd-hub/dist/index.js +16 -0
  200. package/superd-hub/dist/machines.d.ts +45 -0
  201. package/superd-hub/dist/machines.js +125 -0
  202. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  203. package/superd-hub/dist/mesh-federate.js +223 -0
  204. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  205. package/superd-hub/dist/mesh-gateway.js +408 -0
  206. package/superd-hub/dist/mesh-link.d.ts +274 -0
  207. package/superd-hub/dist/mesh-link.js +441 -0
  208. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  209. package/superd-hub/dist/mesh-rpc.js +250 -0
  210. package/superd-hub/dist/mux-client.d.ts +71 -0
  211. package/superd-hub/dist/mux-client.js +281 -0
  212. package/superd-hub/dist/pack-version.d.ts +2 -0
  213. package/superd-hub/dist/pack-version.js +30 -0
  214. package/superd-hub/dist/roster.d.ts +6 -0
  215. package/superd-hub/dist/rpc.d.ts +18 -2
  216. package/superd-hub/dist/rpc.js +34 -4
  217. package/superd-hub/dist/spawn-world.js +3 -1
  218. package/superd-hub/dist/switch-core.d.ts +62 -25
  219. package/superd-hub/dist/switch-core.js +53 -30
  220. package/superd-hub/dist/system-head.d.ts +22 -12
  221. package/superd-hub/dist/system-head.js +100 -84
  222. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  223. package/superd-hub/dist/wire-endpoints.js +180 -0
  224. package/superd-hub/lib/client/index.js +1794 -287
  225. package/superd-hub/package.json +11 -2
  226. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  227. package/agent-agy/dist/agent-preset-projection.js +0 -33
  228. package/agent-claude/dist/agent-preset-projection.js +0 -30
  229. package/agent-codex/dist/agent-preset-projection.js +0 -37
  230. package/agent-codex/dist/app-home.js +0 -65
  231. package/agent-codex/dist/models.js +0 -152
  232. package/agent-codex/dist/spike.js +0 -46
  233. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  234. package/agent-omp/dist/agent-preset-projection.js +0 -36
  235. package/agent-omp/dist/app-home.js +0 -86
  236. package/agent-omp/dist/sidecar-client.js +0 -195
  237. package/agent-pi/dist/agent-preset-projection.js +0 -29
  238. package/superd-hub/dist/client-shim.d.ts +0 -28
  239. package/superd-hub/dist/client-shim.js +0 -251
  240. package/superd-hub/dist/index-pass.d.ts +0 -22
  241. package/superd-hub/dist/index-pass.js +0 -47
  242. package/superd-hub/dist/labels.d.ts +0 -45
  243. package/superd-hub/dist/labels.js +0 -83
  244. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  245. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  246. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  247. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  248. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -0,0 +1,174 @@
1
+ import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
2
+ import type { A2aDeliveryCtx } from './a2a-delivery.js';
3
+ import type { AgentLink } from './agent-roster.js';
4
+ import type { AgentStatusFile } from './agent-status.js';
5
+ import { type OneShotExecInput, type OneShotExecResult } from './exec.js';
6
+ /** P4 exec face (spec §8.1): the executing side + the wire leg, closed over the hub home by a2a-row. */
7
+ export interface HubToolExecFace {
8
+ /** This machine's m4 (the default exec target). */
9
+ selfM4(): string;
10
+ /** Run one command on THIS machine. */
11
+ run(input: OneShotExecInput): Promise<OneShotExecResult>;
12
+ /** Run one command on a paired machine over the wire. Throws typed Errors: unknown machine / exec disabled / peer unreachable. */
13
+ post(machineId: string, input: OneShotExecInput): Promise<OneShotExecResult>;
14
+ /**
15
+ * P4 §8.2 persistent PTY: dispatch one command into a per-(session,
16
+ * machine) shell that KEEPS its cwd/env across dispatches. Throws typed
17
+ * Errors: unknown machine / exec disabled / transport failure.
18
+ */
19
+ pty(sessionKey: string, machine: string, cmd: string, opts?: {
20
+ timeoutSec?: number;
21
+ }): Promise<import('./exec-pty-session.js').PtyDispatchResult>;
22
+ }
23
+ import type { A2aAddress, A2aInitiator, A2aService } from './a2a-service.js';
24
+ /** The service face the tool consumes (structural subset of T2/T3's A2aService). */
25
+ export type HubToolService = Pick<A2aService, 'send' | 'subscribeCompletion' | 'stop' | 'read' | 'listConversations'>;
26
+ /** Per-agent recent-conversation bound (spec §7.2: alive-first, ≤5). */
27
+ /** R6: retired from the agents listing (the strip); kept exported for compat — agent_status windows AGENT_STATUS_WINDOW_MAX rows instead. */
28
+ export declare const RECENT_CONVERSATIONS_MAX = 5;
29
+ /** The exec placeholder note (P4 owns the leg; spec §9 placeholder discipline). */
30
+ export declare const EXEC_PLACEHOLDER_NOTE = "not yet implemented: exec (P4)";
31
+ /** One addressable agent row in the listing (R6: THIN — the conversations embedding is retired; per-agent session state lives on the agent_status leg). */
32
+ export type HubAgentEntry = {
33
+ /** Agent id (a6). */
34
+ id: string;
35
+ /** Chip label (`DSH`, `Codex · dev3`, …). */
36
+ label: string;
37
+ /** Full tool address `<m4>-<a6>` — what the `agent` parameter wants. */
38
+ address: string;
39
+ /** Whether the agent's data plane is up. */
40
+ ready: boolean;
41
+ /** Whether the agent's spawn rejected (row stays for diagnosis). */
42
+ failed: boolean;
43
+ };
44
+ /** One machine block: its agents grouped under it. */
45
+ export type HubMachineEntry = {
46
+ /** Machine id (m4). */
47
+ machine: string;
48
+ /** Machine display label. */
49
+ label: string;
50
+ /** Agents running on this machine (self's machine first, roster order). */
51
+ agents: HubAgentEntry[];
52
+ };
53
+ /**
54
+ * The roster view the `agents` listing renders — the `/api/agent-runtime`
55
+ * family of facts (rpc.ts + agent-roster.ts), injected so the factory stays
56
+ * filesystem-free.
57
+ */
58
+ export interface HubRosterView {
59
+ /** THIS machine: owner-minted m4 + display label (machines.ts `ensureSelf`). */
60
+ self: {
61
+ id: string;
62
+ label: string;
63
+ };
64
+ /** Paired remote machines in pairing order (machines.ts `listRemotes`). */
65
+ remotes: {
66
+ id: string;
67
+ label: string;
68
+ }[];
69
+ /** `agentLinks(home)` output: local rows carry `machine: 'local'`, federated rows the owning m4. */
70
+ agents: AgentLink[];
71
+ }
72
+ /** Everything the factory needs, all injected (pure module — see header). */
73
+ export interface HubToolDeps {
74
+ /** The A2A conversation service (T2 core behind T3's host assembly). */
75
+ service: HubToolService;
76
+ /** T3's delivery face: live-agent routing + the bounded unread buffer. */
77
+ delivery: A2aDeliveryCtx;
78
+ /** Roster snapshot for the `agents` listing (row: the real hub home view). */
79
+ roster(): HubRosterView;
80
+ /**
81
+ * P4 exec face (spec §8.1): the local executing side + the cross-machine
82
+ * wire leg, closed over the hub home by a2a-row (self id, remote rows,
83
+ * pairing cookies, journal). `post` throws typed Errors (unknown machine,
84
+ * exec disabled on the row, peer unreachable).
85
+ */
86
+ exec: HubToolExecFace;
87
+ /**
88
+ * Initiator identity for the calling session — the row closes this over
89
+ * T3's `localA2aInitiator(home, 'native', sessionId)`. Called exactly once
90
+ * per session-bound action, with `exec.agent.id`.
91
+ */
92
+ initiatorOf(sessionId: string): A2aInitiator;
93
+ /**
94
+ * Owns one completion-watch disposer per successful send (the row parks it
95
+ * in a fiber `ctx.effect`; requestId-armed watches self-dispose after
96
+ * their turn fires). Absent = disposers dropped (happy path still leaks
97
+ * nothing — the subscription auto-disposes on fire).
98
+ */
99
+ onWatch?(dispose: () => void): void;
100
+ /**
101
+ * R6: the R5 information-status cache reader (the row wires
102
+ * `() => loadAgentStatus(home)`); absent = agent_status answers honestly
103
+ * empty with collectedAt 0.
104
+ */
105
+ status?(): AgentStatusFile;
106
+ }
107
+ /**
108
+ * Parse the `agent` parameter: `<m4>-<a6>`. Both halves are minted over
109
+ * `[a-z0-9]` (machines.ts `ID_ALPHABET`) at fixed lengths (m4 = 4, a6 = 6), so
110
+ * a malformed address is refused HERE (0.1.8 T4) instead of being mis-split
111
+ * into a bogus machine and reported downstream as "unknown machine" — the
112
+ * `no-such-agent-zzz` → machine `no` is not paired trap the 0.1.7 surface
113
+ * analysis flagged.
114
+ * @param agent - the raw `agent` parameter.
115
+ * @returns the address the service consumes.
116
+ * @throws `[E_BAD_ADDRESS]` when the value is not a well-formed `<m4>-<a6>`.
117
+ */
118
+ export declare function parseAgentAddress(agent: unknown): A2aAddress;
119
+ /** The agent_status window size (Orchestra semantics — not-in-window implies nothing). */
120
+ export declare const AGENT_STATUS_WINDOW_MAX = 5;
121
+ /** D18 marker appended to registry-joined titles. */
122
+ export declare const HUB_A2A_TITLE_MARKER = "[hub A2A]";
123
+ /** The honest fallback when neither the registry nor a workspace names the session. */
124
+ export declare const UNHOSTED_TITLE = "[unhosted]";
125
+ /** One shaped agent_status row. */
126
+ export interface AgentStatusEntryRow {
127
+ conversationId?: string;
128
+ /** Display-truncated session id (uuid head; the full id stays in the cache). */
129
+ sessionId: string;
130
+ title: string;
131
+ status: 'busy' | 'idle' | 'unknown';
132
+ agentAvailable: boolean;
133
+ mtime: number;
134
+ /** D30: the peer's last delivered output — present ONLY when the caller is this conversation's initiator (ownership gate). */
135
+ lastOutput?: string;
136
+ /** D30: epoch ms of lastOutput. */
137
+ lastOutputAt?: number;
138
+ }
139
+ /**
140
+ * Shape one agent's window from the R5 cache (D18): alive-first (a session
141
+ * owning a live Agent first), mtime desc within each group, ≤5 rows —
142
+ * not-in-window implies nothing. Title: registry join (full session id) →
143
+ * `{conversationTitle} [hub A2A]`; else the owning workspace's title
144
+ * verbatim; else '[unhosted]'. conversationId rides only on a registry hit.
145
+ */
146
+ export declare function shapeAgentStatusWindow(entry: {
147
+ rows: AgentStatusEntrySrcRow[];
148
+ workspaces?: Array<{
149
+ title: string;
150
+ sessionIds: string[];
151
+ }>;
152
+ }, conversations: ReadonlyArray<{
153
+ shortId: string;
154
+ sessionId: string;
155
+ title?: string;
156
+ initiatorSession?: string;
157
+ lastOutput?: string;
158
+ lastOutputAt?: number;
159
+ }>,
160
+ /** D30 ownership gate: the caller's session — lastOutput is surfaced only to the conversation's initiator. */
161
+ callerSessionId?: string): AgentStatusEntryRow[];
162
+ /** The cache row face the shaper consumes (structural subset of AgentStatusRow). */
163
+ export interface AgentStatusEntrySrcRow {
164
+ sessionId: string;
165
+ mtime: number;
166
+ status: 'busy' | 'idle' | 'unknown';
167
+ agentAvailable: boolean;
168
+ }
169
+ /**
170
+ * Build the one SuperD Hub tool.
171
+ * @param deps - service, delivery, roster view, initiator factory, watch owner.
172
+ * @returns the registry-ready `superd-hub` ToolDefinition.
173
+ */
174
+ export declare function createSuperdHubTool(deps: HubToolDeps): ToolDefinition;
@@ -0,0 +1,464 @@
1
+ /**
2
+ * The SuperD Hub tool (mesh P3 Task 4, spec §9 / D13): ONE model-facing
3
+ * surface — `superd-hub` — that makes A2A callable from a DSH session.
4
+ * Sub-capabilities ride a call-shape dispatch (the `remote` tool's
5
+ * better-dash discipline: one attention entry, empty/`agents` as the
6
+ * roster probe, everything else a `{action}` leg).
7
+ *
8
+ * This module is the PURE factory: no cordis, no filesystem — the service,
9
+ * delivery face, roster view, initiator factory, and watch ownership are all
10
+ * injected (`{@link HubToolDeps}`), so the full dispatch is unit-testable
11
+ * against fakes. The row wiring lives in `a2a-row.ts`.
12
+ *
13
+ * Upstream anchors (`upstream/deepseek-harness` @ dsh-v0.1.7-rc.1):
14
+ * - tool registration: `@deepseek-ai/dsh-tools` `defineTool` — parameters
15
+ * as the implicit property map, `output {schema, render}` with the
16
+ * successful value validated against the declared schema; the registry
17
+ * scopes `tools.register` to the calling fiber (better-dsh `plugin.ts`
18
+ * precedent: zero static injects, dormant without a `tools` service).
19
+ * - initiator identity: `ToolExecutionInput.agent?: Agent`
20
+ * (dsh-tools/src/index.ts:365) and `Agent.id: SessionId`
21
+ * (dsh-agent/src/types.ts:16) — the session-backed Agent identity IS the
22
+ * live session id, so `exec.agent.id` threads straight into Task 3's
23
+ * `localA2aInitiator` (unread-buffer key + delivery routing key).
24
+ *
25
+ * Semantics rulings baked in (spec §7.2/§7.3, D9/D13):
26
+ * - NEVER poll: after `send` the hub subscribes the peer conversation's
27
+ * completion (`wireCompletionToDelivery`) and the reply auto-delivers
28
+ * into the calling session; `read` only sweeps what was buffered while
29
+ * the initiator was not live. The description states this in caps.
30
+ * - `delivery` mirrors upstream `SessionPromptRequest.mode`: 'queue'
31
+ * (default) is queued peer input delivered as a waking turn, never interrupting; 'steer' is
32
+ * explicit steering for the peer's nearest step boundary.
33
+ * - `stop` is a hard stop of the peer's ACTIVE TURN; scope 'session'
34
+ * degrades to turn-only (upstream has no session-close RPC) and the
35
+ * conversation stays resumable — send again resumes it.
36
+ * - `exec` is the P4 placeholder leg: `"not yet implemented: exec (P4)"`.
37
+ */
38
+ import { defineTool } from '@deepseek-ai/dsh-tools';
39
+ import { parseOneShotExecBody, EXEC_TAIL_WINDOW } from './exec.js';
40
+ import { tailWindow } from './exec-pty-framing.js';
41
+ // F2 (0.1.7-n, PC 0.1.7-m first-person): the wire/registry sentinel stays
42
+ // '[undefined]', but the DISPLAY surface must not show it to a human — D17
43
+ // already sanctions display-layer beautification.
44
+ import { A2A_UNTITLED } from './a2a-service.js';
45
+ import { A2A_TITLE_PLACEHOLDER } from './a2a-wire.js';
46
+ /** Per-agent recent-conversation bound (spec §7.2: alive-first, ≤5). */
47
+ /** R6: retired from the agents listing (the strip); kept exported for compat — agent_status windows AGENT_STATUS_WINDOW_MAX rows instead. */
48
+ export const RECENT_CONVERSATIONS_MAX = 5;
49
+ /** The exec placeholder note (P4 owns the leg; spec §9 placeholder discipline). */
50
+ export const EXEC_PLACEHOLDER_NOTE = 'not yet implemented: exec (P4)';
51
+ /** agent-roster.ts's LOCAL_MACHINE literal ('local') — local rows' grouping key, not a real m4. */
52
+ const LOCAL_MACHINE = 'local';
53
+ /** `<m4>-<a6>`: exactly 4 then 6 chars over the mint alphabet (machines.ts `ID_ALPHABET`). */
54
+ const AGENT_ADDRESS_PATTERN = /^[a-z0-9]{4}-[a-z0-9]{6}$/;
55
+ /**
56
+ * Parse the `agent` parameter: `<m4>-<a6>`. Both halves are minted over
57
+ * `[a-z0-9]` (machines.ts `ID_ALPHABET`) at fixed lengths (m4 = 4, a6 = 6), so
58
+ * a malformed address is refused HERE (0.1.8 T4) instead of being mis-split
59
+ * into a bogus machine and reported downstream as "unknown machine" — the
60
+ * `no-such-agent-zzz` → machine `no` is not paired trap the 0.1.7 surface
61
+ * analysis flagged.
62
+ * @param agent - the raw `agent` parameter.
63
+ * @returns the address the service consumes.
64
+ * @throws `[E_BAD_ADDRESS]` when the value is not a well-formed `<m4>-<a6>`.
65
+ */
66
+ export function parseAgentAddress(agent) {
67
+ if (typeof agent !== 'string' || !AGENT_ADDRESS_PATTERN.test(agent)) {
68
+ throw new Error(`[E_BAD_ADDRESS] superd-hub: agent must be a "<m4>-<a6>" address — 4 chars, a hyphen, 6 chars, all [a-z0-9] (got ${JSON.stringify(agent)}); call the agents action to list the valid ids`);
69
+ }
70
+ const dash = agent.indexOf('-');
71
+ return { machine: agent.slice(0, dash), id: agent.slice(dash + 1) };
72
+ }
73
+ /** The initiator for one exec — fail-loud without a live session (D9: replies NEED one to land in). */
74
+ function initiatorOfExec(deps, exec) {
75
+ const sessionId = exec.agent?.id;
76
+ if (typeof sessionId !== 'string' || sessionId === '') {
77
+ throw new Error('[E_NO_SESSION] superd-hub: this tool call carries no agent session — A2A replies deliver into the calling session and cannot be routed without one');
78
+ }
79
+ return deps.initiatorOf(sessionId);
80
+ }
81
+ /** The `agents` listing: machines → agents, thin rows (R6 strip — session state lives on agent_status). */
82
+ function agentsListing(deps) {
83
+ const view = deps.roster();
84
+ const agentsByMachine = new Map();
85
+ for (const link of view.agents) {
86
+ const machine = link.machine === LOCAL_MACHINE ? view.self.id : link.machine;
87
+ const list = agentsByMachine.get(machine) ?? [];
88
+ list.push({
89
+ id: link.id,
90
+ label: link.label,
91
+ address: `${machine}-${link.id}`,
92
+ ready: link.ready === true,
93
+ failed: link.failed === true,
94
+ });
95
+ agentsByMachine.set(machine, list);
96
+ }
97
+ // Machine order: self first, then paired remotes in pairing order, then
98
+ // any m4 that still has agents but no machines.json row (degraded pairing
99
+ // state) — the listing never hides addressable agents.
100
+ const machines = [
101
+ { machine: view.self.id, label: view.self.label, agents: agentsByMachine.get(view.self.id) ?? [] },
102
+ ];
103
+ const seen = new Set([view.self.id]);
104
+ for (const remote of view.remotes) {
105
+ seen.add(remote.id);
106
+ machines.push({ machine: remote.id, label: remote.label, agents: agentsByMachine.get(remote.id) ?? [] });
107
+ }
108
+ for (const machine of [...agentsByMachine.keys()].sort()) {
109
+ if (!seen.has(machine))
110
+ machines.push({ machine, label: machine, agents: agentsByMachine.get(machine) ?? [] });
111
+ }
112
+ return { action: 'agents', machines };
113
+ }
114
+ /** The agent_status window size (Orchestra semantics — not-in-window implies nothing). */
115
+ export const AGENT_STATUS_WINDOW_MAX = 5;
116
+ /** D18 marker appended to registry-joined titles. */
117
+ export const HUB_A2A_TITLE_MARKER = '[hub A2A]';
118
+ /** The honest fallback when neither the registry nor a workspace names the session. */
119
+ export const UNHOSTED_TITLE = '[unhosted]';
120
+ /** Display truncation: strip the `session-` prefix, take the uuid head (6 chars); the cache keeps full ids. */
121
+ function displaySessionId(sessionId) {
122
+ const base = sessionId.startsWith('session-') ? sessionId.slice('session-'.length) : sessionId;
123
+ return base.slice(0, 6);
124
+ }
125
+ /**
126
+ * Shape one agent's window from the R5 cache (D18): alive-first (a session
127
+ * owning a live Agent first), mtime desc within each group, ≤5 rows —
128
+ * not-in-window implies nothing. Title: registry join (full session id) →
129
+ * `{conversationTitle} [hub A2A]`; else the owning workspace's title
130
+ * verbatim; else '[unhosted]'. conversationId rides only on a registry hit.
131
+ */
132
+ export function shapeAgentStatusWindow(entry, conversations,
133
+ /** D30 ownership gate: the caller's session — lastOutput is surfaced only to the conversation's initiator. */
134
+ callerSessionId) {
135
+ const bySession = new Map();
136
+ for (const row of conversations)
137
+ bySession.set(row.sessionId, row);
138
+ const workspaceTitleOf = new Map();
139
+ for (const workspace of entry.workspaces ?? []) {
140
+ for (const sessionId of workspace.sessionIds)
141
+ workspaceTitleOf.set(sessionId, workspace.title);
142
+ }
143
+ const sorted = [...entry.rows].sort((a, b) => {
144
+ const alive = Number(b.agentAvailable) - Number(a.agentAvailable);
145
+ return alive !== 0 ? alive : b.mtime - a.mtime;
146
+ });
147
+ return sorted.slice(0, AGENT_STATUS_WINDOW_MAX).map((row) => {
148
+ const conversation = bySession.get(row.sessionId);
149
+ const registryTitle = conversation?.title;
150
+ // F2: the sentinel (absent/empty/'[undefined]') renders as '(untitled)' here —
151
+ // never '[undefined] [hub A2A]' in front of a human.
152
+ const displayTitle = registryTitle === undefined || registryTitle === '' || registryTitle === A2A_TITLE_PLACEHOLDER
153
+ ? A2A_UNTITLED
154
+ : registryTitle;
155
+ const title = conversation !== undefined
156
+ ? `${displayTitle} ${HUB_A2A_TITLE_MARKER}`
157
+ : workspaceTitleOf.get(row.sessionId) ?? UNHOSTED_TITLE;
158
+ // D30: the peer's last output is the conversation's private business —
159
+ // only its INITIATOR may read it through this facility.
160
+ const ownedOutput = conversation !== undefined && callerSessionId !== undefined &&
161
+ conversation.initiatorSession === callerSessionId && conversation.lastOutput !== undefined
162
+ ? { lastOutput: conversation.lastOutput, ...(conversation.lastOutputAt === undefined ? {} : { lastOutputAt: conversation.lastOutputAt }) }
163
+ : {};
164
+ return {
165
+ ...(conversation === undefined ? {} : { conversationId: conversation.shortId }),
166
+ sessionId: displaySessionId(row.sessionId),
167
+ title,
168
+ status: row.status,
169
+ agentAvailable: row.agentAvailable,
170
+ mtime: row.mtime,
171
+ ...ownedOutput,
172
+ };
173
+ });
174
+ }
175
+ /** Render one buffered message for the `read` sweep: the sender as a full address. */
176
+ function readEntry(message) {
177
+ return {
178
+ messageId: message.messageId,
179
+ from: `${message.from.machine}-${message.from.id}`,
180
+ conversationId: message.conversationId,
181
+ text: message.text,
182
+ bufferedAt: message.bufferedAt,
183
+ };
184
+ }
185
+ const DESCRIPTION = 'SuperD Hub — agent-to-agent messaging across the Super D machine mesh.\n' +
186
+ 'Lifecycle: OPEN once (send with no conversationId) → then only `send` with its conversationId → replies auto-deliver; sweep `read` at wake boundaries → `stop` only when a turn is off track. Families: facility/status (`agents`, `agent_status`), messaging (`send`, `read`, `stop`), shell (`exec`).\n' +
187
+ 'Call shapes:\n' +
188
+ ' {} | {action:"agents"} → the thin roster: machines → agents (id, label, address, ready).\n' +
189
+ ' {action:"agent_status", agent} → that agent\'s session window from the sweep cache: {collectedAt, sessions:[{conversationId?, sessionId, title, status, mtime, lastOutput?, lastOutputAt?}], note?}. lastOutput (the peer\'s last delivered output) rides ONLY when you are that conversation\'s initiator; `note` says why the window is empty when empty is not a fact (the last collection failed, or none has happened yet).\n' +
190
+ ' {action:"send", agent?, conversationId?, title?, message, delivery?} → open-or-resume: omit conversationId to OPEN (then agent = the recipient, REQUIRED; title optional; message is the first turn); pass conversationId to RESUME (agent optional — the hub derives the route; a wrong agent is refused). Receipt {outcome: delivered|accepted|failed, messageId, conversationId, sessionId, created, error?, note?}: `delivered` = end-to-end complete; `accepted` = the hub took it and the SAME in-flight request keeps running (its `delivered`/`undelivered` outcome arrives as a notice — do not resend); `failed` = immediate rejection.\n' +
191
+ ' {action:"read"} → sweep messages buffered for this session while it was not live.\n' +
192
+ ' {action:"stop", conversationId, agent?, scope?} → cancel the conversation\'s ACTIVE turn (the session owner is derived from the conversation id; agent is optional). `stopped:true` means this call cancelled a live turn; a turn that finished on its own is IDLE, never `stopped` — so stop answers stopped:false with a note when nothing was running.\n' +
193
+ ' {action:"exec", cmd, machine?, cwd?, timeoutSec?, mode?} → run one shell command on a machine (default: this machine). mode "oneshot" (default): {exit, stdout (last 30k), stderr, durationMs, truncated, timedOut, outcome}; cwd sets its working directory. mode "pty": a PERSISTENT shell per (your session, machine) — the shell OWNS its cwd and env and they SURVIVE across dispatches (cd once inside the command, then keep working); {output (tail 30k), exit, cwd, timedOut, reconnected, durationMs}. cwd is oneshot-only: passing it with mode "pty" is refused (use `cd <dir> && …` inside the command). Prefer oneshot for independent commands; pty when shell state matters.\n' +
194
+ 'Addresses are "<m4>-<a6>" (machine-agent); call agents first to list valid ids. delivery: "queue" (default) is injected into the peer\'s session as a waking turn — it never interrupts an active turn, it queues behind it; "steer" submits steering for the peer\'s nearest step boundary — use it only when urgency demands. NEVER poll for a reply: the peer\'s reply arrives in this conversation as a new message WHEN THE PEER SENDS one (peers that never use the channel get their last output at turn end delivered as their reply instead, form=transcript); a turn that ends with no channel artifact yields one notice whose reason= distinguishes peer-silent (ended, said nothing) / peer-error (stream died or turn aborted) / stopped-by-initiator (you stopped it). read only sweeps what was buffered while this session was down. stop is a hard stop of the peer\'s active turn (the UI stop button); scope "session" degrades to turn-only — no session-close exists upstream — and the conversation stays resumable: send again to resume it. agent_status reads a cache refreshed by the 5-minute sweep (collectedAt says when); the window is alive-first, ≤5 rows, newest first, and not being in it implies nothing; a `note` says why when the window has no collection behind it (the last collection failed, or none has happened yet). MESSAGING HAS NO STATUS QUERY: to check whether a peer is idle or running, call agent_status (a 5-minute cache — never a delivery receipt); if that is not enough and you cannot wait, send another message.';
195
+ /**
196
+ * Build the one SuperD Hub tool.
197
+ * @param deps - service, delivery, roster view, initiator factory, watch owner.
198
+ * @returns the registry-ready `superd-hub` ToolDefinition.
199
+ */
200
+ export function createSuperdHubTool(deps) {
201
+ return defineTool({
202
+ name: 'superd-hub',
203
+ description: DESCRIPTION,
204
+ parameters: {
205
+ action: {
206
+ type: 'string',
207
+ enum: ['agents', 'send', 'read', 'stop', 'agent_status', 'exec'],
208
+ description: 'Omitted/\"agents\" → the roster. The other legs are self-describing call shapes.',
209
+ },
210
+ agent: {
211
+ type: 'string',
212
+ description: 'Address "<m4>-<a6>". REQUIRED on send only to OPEN a new conversation (the recipient). On a continuation (conversationId given) send/stop derive the route themselves — omit agent; if you do pass it, a mismatch with the derived route is refused. agent_status: the agent whose session window you want. Call agents first to list valid ids.',
213
+ },
214
+ conversationId: {
215
+ type: 'string',
216
+ description: 'Conversation shortId (from a receipt or the agents listing). Omit on send to open a new conversation.',
217
+ },
218
+ title: {
219
+ type: 'string',
220
+ description: 'Optional operator-facing conversation title (never an identity); given to new conversations.',
221
+ },
222
+ message: {
223
+ type: 'string',
224
+ description: 'Message body injected as one user turn (the A2A reply-address envelope is added automatically). Required: it is the first turn when opening a new conversation, the follow-up turn when resuming.'
225
+ },
226
+ delivery: {
227
+ type: 'string',
228
+ enum: ['queue', 'steer'],
229
+ description: "'queue' (default): injected as a waking turn — queues behind any active turn, never interrupts. 'steer': steering input for the peer's nearest step boundary.",
230
+ },
231
+ scope: {
232
+ type: 'string',
233
+ enum: ['turn', 'session'],
234
+ description: "'turn' (default): cancel the active turn. 'session': degrades to turn-only — the conversation stays resumable.",
235
+ },
236
+ machine: {
237
+ type: 'string',
238
+ description: 'exec target: an m4 machine id (defaults to this machine). The initiator-side registry row may disable exec per machine (execEnabled).',
239
+ },
240
+ cmd: {
241
+ type: 'string',
242
+ description: 'exec: the shell command run via bash -lc (non-empty, ≤4096 chars).',
243
+ },
244
+ cwd: {
245
+ type: 'string',
246
+ description: 'exec oneshot: optional absolute working directory. Not accepted with mode "pty" (the persistent shell keeps its own session cwd — pass `cd <dir> && …` in the command).',
247
+ },
248
+ timeoutSec: {
249
+ type: 'integer',
250
+ description: 'exec: timeout in seconds, 1–600 (default 120). SIGTERM then SIGKILL to the process group.',
251
+ },
252
+ mode: {
253
+ type: 'string',
254
+ enum: ['oneshot', 'pty'],
255
+ description: "exec mode: 'oneshot' (default, independent). 'pty' keeps one shell per (session, machine) — cwd/env persist across dispatches.",
256
+ },
257
+ },
258
+ output: {
259
+ schema: {
260
+ type: 'object',
261
+ additionalProperties: false,
262
+ properties: {
263
+ action: { type: 'string', required: true },
264
+ machines: { type: 'json', description: 'agents listing: machines → agents (thin rows — no conversations; use agent_status)' },
265
+ collectedAt: { type: 'integer', description: 'agent_status: when the machine bucket was last collected (epoch ms; 0 = never)' },
266
+ sessions: { type: 'json', description: 'agent_status: window rows {conversationId?, sessionId (truncated), title, status, agentAvailable, mtime, lastOutput?, lastOutputAt?} — lastOutput rides ONLY when you are that conversation\'s initiator (D30 ownership gate)' },
267
+ conversationId: { type: 'string' },
268
+ sessionId: { type: 'string' },
269
+ created: { type: 'boolean', description: "send receipt: true when conversationId was omitted (this send OPENED the conversation); absent/false on resume" },
270
+ agent: { type: 'string' },
271
+ machine: { type: 'string' },
272
+ outcome: { type: 'string', description: "send receipt: 'delivered' (end-to-end complete) | 'accepted' (hub took it; the same in-flight request keeps running and the outcome arrives as a notice) | 'failed' (immediate rejection); exec: why the process ended — 'exited' | 'signaled' | 'timeout' | 'spawn-error' (diagnostic rides stderr on spawn-error)" },
273
+ output: { type: 'json', description: 'exec pty: the session output, tail-windowed (a truncation banner carries the exact counts)' },
274
+ cwd: { type: 'json', description: 'exec pty: the session working directory after the command (null when unknown)' },
275
+ mode: { type: 'string', description: "exec pty: present with value 'pty' — the result is a persistent-shell dispatch (absent on oneshot). 0.1.7-h D1: the harness output validator rejects undeclared fields, so this MUST stay declared in lockstep with the pty result shape." },
276
+ reconnected: { type: 'boolean', description: 'exec pty: the transport died since the last dispatch and a fresh shell answered' },
277
+ messageId: { type: 'string' },
278
+ error: { type: 'json', description: 'wire error body {code, message, details?} on failed sends' },
279
+ note: { type: 'string', description: "model-facing note explaining what a result cannot say in its fields: stop's idle answer (nothing was running), send's reply guidance, and agent_status's reason for an empty window (the collection failed, or none has happened yet) — an empty window is never a guess" },
280
+ stopped: { type: 'boolean' },
281
+ scope: { type: 'string' },
282
+ reason: { type: 'string', description: 'the turn-only degradation note on session-scope stops' },
283
+ count: { type: 'integer' },
284
+ messages: { type: 'json', description: 'read sweep: buffered messages with from as "<m4>-<a6>"' },
285
+ exit: { type: 'json', description: 'exec: the command exit code (null when killed by timeout/signal)' },
286
+ stdout: { type: 'json', description: 'exec: the last 30k of stdout' },
287
+ stderr: { type: 'json', description: 'exec: the last 30k of stderr' },
288
+ durationMs: { type: 'integer', description: 'exec: wall-clock duration' },
289
+ truncated: { type: 'boolean', description: 'exec: a stream\'s total output exceeded the returned tail window' },
290
+ timedOut: { type: 'boolean', description: 'exec: the timeout fired (SIGTERM→SIGKILL to the group)' },
291
+ },
292
+ },
293
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
294
+ },
295
+ async execute(args, exec) {
296
+ // One uniform gate first: every leg is session-bound A2A (D9 — replies
297
+ // deliver into the calling session), so a session-less call fails loud
298
+ // before touching the service, whatever the action.
299
+ const initiator = initiatorOfExec(deps, exec);
300
+ const action = args.action ?? 'agents';
301
+ switch (action) {
302
+ case 'agents':
303
+ return agentsListing(deps);
304
+ case 'send': {
305
+ if (typeof args.message !== 'string' || args.message === '') {
306
+ throw new Error('[E_MISSING_MESSAGE] superd-hub: send requires a non-empty message');
307
+ }
308
+ // D24/T3: `agent` is required only to OPEN a conversation; on a
309
+ // continuation the hub derives the route from the conversation id.
310
+ if (args.conversationId === undefined && (args.agent === undefined || args.agent === '')) {
311
+ throw new Error('[E_MISSING_AGENT] superd-hub: send requires agent ("<m4>-<a6>") to open a new conversation');
312
+ }
313
+ const to = args.agent === undefined ? undefined : parseAgentAddress(args.agent);
314
+ const receipt = await deps.service.send({
315
+ ...(to === undefined ? {} : { to }),
316
+ ...(args.conversationId === undefined ? {} : { conversationId: args.conversationId }),
317
+ ...(args.title === undefined ? {} : { title: args.title }),
318
+ message: args.message,
319
+ ...(args.delivery === undefined ? {} : { delivery: args.delivery }),
320
+ }, initiator);
321
+ // R4: the cross-wire completion watch is RETIRED — replies come back
322
+ // as reply/notice envelopes through the ingest (in-process for
323
+ // self-hosted conversations, POSTed by the peer's hub otherwise).
324
+ // The receipt still answers inbox acceptance; the reply arrives later
325
+ // as an auto-delivered message. The vestigial deps (delivery/onWatch)
326
+ // stay on the interface for wiring compatibility.
327
+ return {
328
+ action,
329
+ outcome: receipt.outcome,
330
+ messageId: receipt.messageId,
331
+ ...(receipt.conversationId === undefined ? {} : { conversationId: receipt.conversationId }),
332
+ sessionId: receipt.sessionId,
333
+ // O2 (0.1.8-b): `created` is a WORLD fact (did a conversation come into
334
+ // being), not a request-shape echo — a failed send created nothing.
335
+ created: receipt.outcome !== 'failed' && args.conversationId === undefined,
336
+ // A2aReceiptError carries details?: unknown (T2's verbatim wire body);
337
+ // the body crossed an envelope, so it is JSON by construction —
338
+ // one boundary cast onto the schema's json-typed field.
339
+ ...(receipt.error === undefined ? {} : { error: receipt.error }),
340
+ ...(receipt.outcome === 'delivered'
341
+ ? { note: 'reply auto-delivers as a new message in this conversation — never poll; read only sweeps buffered leftovers' }
342
+ : receipt.outcome === 'accepted' && receipt.note !== undefined
343
+ ? { note: receipt.note }
344
+ : {}),
345
+ };
346
+ }
347
+ case 'read': {
348
+ const swept = deps.service.read(initiator.sessionId);
349
+ return {
350
+ action,
351
+ count: swept.length,
352
+ messages: swept.map(readEntry),
353
+ };
354
+ }
355
+ case 'stop': {
356
+ if (args.conversationId === undefined || args.conversationId === '') {
357
+ throw new Error('[E_MISSING_CONVERSATION] superd-hub: stop requires conversationId');
358
+ }
359
+ // D24/T3: the session owner is derived from the conversation id;
360
+ // `agent`, when supplied, is only a checked assertion.
361
+ const to = args.agent === undefined ? undefined : parseAgentAddress(args.agent);
362
+ const result = await deps.service.stop({ conversationId: args.conversationId, ...(to === undefined ? {} : { to }) }, args.scope ?? 'turn');
363
+ return {
364
+ action,
365
+ stopped: result.cancelled === true,
366
+ scope: result.scope,
367
+ ...(result.scope === 'turn-only' ? { reason: result.reason } : {}),
368
+ // D5: the honest idle answer — the note rides along instead of a bare stopped:false
369
+ ...(result.cancelled === true ? {} : { note: result.note }),
370
+ };
371
+ }
372
+ case 'agent_status': {
373
+ if (args.agent === undefined || args.agent === '') {
374
+ throw new Error('[E_MISSING_AGENT] superd-hub: agent_status requires agent ("<m4>-<a6>")');
375
+ }
376
+ const addr = parseAgentAddress(args.agent);
377
+ const file = deps.status?.() ?? { machines: {} };
378
+ const bucket = file.machines[addr.machine];
379
+ const entry = bucket?.agents.find((a) => a.a6 === addr.id);
380
+ const sessions = entry === undefined ? [] : shapeAgentStatusWindow(entry, deps.service.listConversations(), initiator.sessionId);
381
+ // O1b (0.1.8-e): an empty window must never be passed off as "no
382
+ // sessions" — a per-agent collection failure, a machine-level one,
383
+ // and a collection that has not happened yet each say so here.
384
+ let note;
385
+ if (entry !== undefined && entry.error !== undefined) {
386
+ note = `collection failed for this agent — ${entry.error}`;
387
+ }
388
+ else if (bucket !== undefined && bucket.error !== undefined) {
389
+ note = `collection failed for machine "${addr.machine}" — ${bucket.error}`;
390
+ }
391
+ else if (entry === undefined) {
392
+ note = 'no collected status for this agent yet — the 5-minute sweep fills the cache; call agents for the roster';
393
+ }
394
+ return {
395
+ action,
396
+ agent: addr.id,
397
+ machine: addr.machine,
398
+ collectedAt: bucket?.collectedAt ?? 0,
399
+ // the rows crossed no envelope, so they are JSON by construction —
400
+ // one boundary cast onto the schema's json-typed field (the error leg's pattern)
401
+ sessions: sessions,
402
+ ...(note === undefined ? {} : { note }),
403
+ };
404
+ }
405
+ case 'exec': {
406
+ if (args.mode === 'pty') {
407
+ if (typeof args.cmd !== 'string' || args.cmd.trim() === '')
408
+ throw new Error('[E_BAD_EXEC] superd-hub: exec: cmd must be a non-empty string');
409
+ // F1 (0.1.7-n, PC 0.1.7-m first-person): cwd was silently ignored on
410
+ // this leg — the persistent shell owns its cwd. Refuse it loudly and
411
+ // teach the workaround instead of a silent parameter no-op.
412
+ if (args.cwd !== undefined) {
413
+ throw new Error('[E_BAD_EXEC] superd-hub: exec: cwd is not supported with mode "pty" — the persistent shell keeps its own cwd; use `cd <dir> && …` inside the command');
414
+ }
415
+ const sessionKey = initiatorOfExec(deps, exec).sessionId;
416
+ const machine = args.machine ?? deps.exec.selfM4();
417
+ try {
418
+ const r = await deps.exec.pty(sessionKey, machine, args.cmd, { ...(args.timeoutSec === undefined ? {} : { timeoutSec: args.timeoutSec }) });
419
+ const out = tailWindow(r.output, EXEC_TAIL_WINDOW);
420
+ return {
421
+ action,
422
+ mode: 'pty',
423
+ machine,
424
+ output: out.text,
425
+ exit: r.exit,
426
+ cwd: r.cwd,
427
+ timedOut: r.timedOut,
428
+ reconnected: r.reconnected,
429
+ durationMs: r.durationMs,
430
+ ...(out.truncated !== undefined ? { truncated: true } : {}), // char counts ride tailWindow's own banner in `output`
431
+ };
432
+ }
433
+ catch (err) {
434
+ throw new Error(`[E_EXEC_FAILED] superd-hub: ${err instanceof Error ? err.message : String(err)}`);
435
+ }
436
+ }
437
+ const parsed = parseOneShotExecBody({
438
+ cmd: args.cmd,
439
+ ...(args.cwd === undefined ? {} : { cwd: args.cwd }),
440
+ ...(args.timeoutSec === undefined ? {} : { timeoutSec: args.timeoutSec }),
441
+ });
442
+ if (!parsed.ok)
443
+ throw new Error(`[E_BAD_EXEC] superd-hub: ${parsed.error}`);
444
+ const machine = args.machine ?? deps.exec.selfM4();
445
+ try {
446
+ const result = machine === deps.exec.selfM4()
447
+ ? await deps.exec.run(parsed.value)
448
+ : await deps.exec.post(machine, parsed.value);
449
+ return { action, machine, ...result };
450
+ }
451
+ catch (err) {
452
+ throw new Error(`[E_EXEC_FAILED] superd-hub: ${err instanceof Error ? err.message : String(err)}`);
453
+ }
454
+ }
455
+ default:
456
+ // Unreachable through the tool surface (the input schema's action
457
+ // enum rejects unknowns before execute) — kept as the switch's
458
+ // totality guard: if the enum and this dispatch ever drift, this
459
+ // names the gap instead of silently returning undefined.
460
+ throw new Error(`[E_BAD_ACTION] superd-hub: unknown action ${JSON.stringify(action)}`);
461
+ }
462
+ },
463
+ });
464
+ }